Android 5.0+ (API 21)java
---->[源碼裏讓咱們用camera2]
* @deprecated We recommend using the new
{@link android.hardware.camera2} API for new applications.
複製代碼
這裏的camera2可不是一個類哦android
1.早聽聞camera2很複雜,沒想到這麼複雜,我都有點小hold不住
視圖佈局和上一篇同樣,只是實現變了而已git
開個相機就這麼麻煩...(圖畫出來感受清楚多了)github
HandlerThread handlerThread = new HandlerThread("Camera2");//線程名,隨意
handlerThread.start();
mainHandler = new Handler(getMainLooper());//主線程Handler
childHandler = new Handler(handlerThread.getLooper());//子線程Handler
複製代碼
上來就一個HandlerThread類,它是幹嗎的?又是Handler又是Thread的,名字怪嚇人的
一看源碼,它彷彿是在逗我笑...一共就166行,繼承自Thread
getThreadHandler
方法仍是 * @hide的,這不是明擺着說:快用getLooper方法
嗎?
有了Looper就能根據Looper建立該線程下的Handler,名字起的很到位編程
主要在
CameraDevice.StateCallback
的onOpened回調函數中有CameraDevice的引用
注意,到這裏還只是打開了相機,並無預覽數組
mCameraID = "" + CameraCharacteristics.LENS_FACING_FRONT;//後攝像頭
//獲取攝像頭管理器
mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
//AndroidStudio自動生成...if
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED) {
return;
}
CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
//TODO 預覽方法見下:--startPreview()
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
}
};
mCameraManager.openCamera(mCameraID, stateCallback, mainHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
複製代碼
startPreview
開啓預覽真的挺費勁的bash
/**
* 開啓預覽
*/
private void startPreview() {
try {
// 建立預覽須要的CaptureRequest.Builder
final CaptureRequest.Builder reqBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 將SurfaceView的surface做爲CaptureRequest.Builder的目標
reqBuilder.addTarget(mHolder.getSurface());
//reqBuilder能夠設置參數
reqBuilder.set( // 自動對焦
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
reqBuilder.set(// 打開閃光燈
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 建立CameraCaptureSession,該對象負責管理處理預覽請求和拍照請求
CameraCaptureSession.StateCallback stateCallback =
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
if (null == mCameraDevice) return;
// 當攝像頭已經準備好時,開始顯示預覽
mCameraCaptureSession = cameraCaptureSession;
try {
// 顯示預覽
mCameraCaptureSession.setRepeatingRequest(
reqBuilder.build(), null, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
Toast.makeText(Camera2Activity.this, "配置失敗", Toast.LENGTH_SHORT).show();
}
};
mImageReader = ImageReader.newInstance(
mIdSvVideo.getWidth(), mIdSvVideo.getHeight(),
ImageFormat.JPEG, 1);
mCameraDevice.createCaptureSession(
Arrays.asList(mHolder.getSurface(), mImageReader.getSurface()),
stateCallback,
childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
複製代碼
費了這麼大的勁,而後終於能夠預覽了,結果以下...頭像都變形了,這怎麼能忍
百度了一會,並無找到好的解決方法,而後發揮本身的聰明才智
把SurfaceView寬高比縮放成3:4,並對橫豎屏分別適配,完美解決微信
豎屏 | 橫屏 |
---|---|
![]() |
![]() |
代碼實現起來也很是簡單,根據長寬的大小,setScale,保證比例就好了
注意:SurfaceView在surfaceCreated回調時纔有尺寸,在onCreate時寬高爲0
session
/**
* 適應屏幕
*
* @param surfaceView
*/
private void adjustScreen(View surfaceView) {
int height = surfaceView.getHeight();
int width = surfaceView.getWidth();
if (height > width) {
float justH = width * 4.f / 3;
mIdSvVideo.setScaleX(height / justH);
} else {
float justW = height * 4.f / 3;
mIdSvVideo.setScaleY(width / justW);
}
}
複製代碼
豎屏 | 橫屏 |
---|---|
![]() |
![]() |
Ok,總算完美顯示出來了,良好的開端是成功的一半,繼續app
想拍張照也不簡單啊...
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();//旋轉方向集合
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
/**
* 拍照方法封裝
*
*/
private void takePicture() {
if (mCameraDevice != null) {
try {
CaptureRequest.Builder reqBuilder = mCameraDevice
.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
reqBuilder.addTarget(mImageReader.getSurface());
// 自動對焦
reqBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 自動曝光
reqBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 獲取手機方向
int rotation = getWindowManager().getDefaultDisplay().getRotation();
// 根據設備方向計算設置照片的方向
reqBuilder.set(
CaptureRequest.JPEG_ORIENTATION,
ORIENTATIONS.get(rotation));
//拍照
mCameraCaptureSession.capture(reqBuilder.build(), null, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}
複製代碼
//能夠在這裏處理拍照獲得的臨時照片
mImageReader.setOnImageAvailableListener(reader -> {
// 拿到拍照照片數據
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//由緩衝區存入字節數組
File file = FileHelper.get()
.createFile("camera2/IMG-"+StrUtil.getCurrentTime_yyyyMMddHHmmss()+".jpg");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
fos.write(bytes);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
startPreview();
image.close();
}
}, mainHandler);
複製代碼
當拍一張時,一切挺好,再拍一張竟然崩了...
前面ImageReader的maxImages參數設爲1,maxImages (1) has already been acquired
Process: com.toly1994.video, PID: 11594
java.lang.IllegalStateException: maxImages (1) has already been acquired, call #close before acquiring more.
at android.media.ImageReader.acquireNextImage(ImageReader.java:501)
at com.toly1994.video.cameral2.Camera2Activity.lambda$surfaceCreated$9(Camera2Activity.java:355)
at com.toly1994.video.cameral2.-$$Lambda$Camera2Activity$ip57VOWPzaqDJe_HhvMUPkOS6eo.onImageAvailable(Unknown Source:2)
複製代碼
再看看ImageReader對maxImages的表述
he maximum number of images the user will want to access simultaneously.
This should be as small as possible to limit memory use.
Once maxImages Images are obtained by the user, one of them has to be released
before a new Image will become available for access through
用戶想要同時訪問的最大圖像數量。這應該儘量小,以限制內存的使用。
一旦用戶得到了maxImages圖像,在能夠經過新圖像進行訪問以前,必須先釋放其中一個圖像
複製代碼
因此拍完照後釋放一下還有從新
startPreview()
一下,否則就不動了
image.close();
複製代碼
這個和ImageReader個尺寸
有關
,我只說有關,沒說就是
通過前一篇咱們知道,照片的尺寸都是固定的某些種
看下面,我用1080* 1920
和1080* 1925
結果拍的兩張尺寸同樣
說明傳參只是參考值,內部會本身進行調整,我設成1*1
,結果尺寸144*176
這時應該會想到上一篇中打印的的圖片種類支持狀況,這篇看一下camera2裏怎麼獲取
//mImageReader = ImageReader.newInstance(
// 1080, 1920,
// ImageFormat.JPEG, 1);
//mImageReader = ImageReader.newInstance(
// 1080, 1925,
// ImageFormat.JPEG, 1);
mImageReader = ImageReader.newInstance(
1, 1,
ImageFormat.JPEG, 1);
// 獲取攝像頭支持的配置屬性
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
// 獲取攝像頭支持的最大尺寸
List<Size> sizes = Arrays.asList(
map.getOutputSizes(ImageFormat.JPEG));
for (int i = 0; i < sizes.size(); i++) {
Size pSize = sizes.get(i);
L.d("PictureSize.width = " + pSize.getWidth() + "--------PictureSize.height = " + pSize.getHeight());
}
複製代碼
打印結果:和上一篇獲取的結果同樣
PictureSize.width = 5184--------PictureSize.height = 3880
PictureSize.width = 4608--------PictureSize.height = 3456
PictureSize.width = 4608--------PictureSize.height = 2592
PictureSize.width = 4608--------PictureSize.height = 2304
PictureSize.width = 4608--------PictureSize.height = 2176
PictureSize.width = 4608--------PictureSize.height = 2126
PictureSize.width = 4160--------PictureSize.height = 3120
PictureSize.width = 4160--------PictureSize.height = 2340
PictureSize.width = 4000--------PictureSize.height = 3000
PictureSize.width = 3840--------PictureSize.height = 2160
PictureSize.width = 3264--------PictureSize.height = 2448
PictureSize.width = 3264--------PictureSize.height = 1632
PictureSize.width = 3264--------PictureSize.height = 1552
PictureSize.width = 3264--------PictureSize.height = 1504
PictureSize.width = 3200--------PictureSize.height = 2400
PictureSize.width = 2592--------PictureSize.height = 1944
PictureSize.width = 2592--------PictureSize.height = 1940
PictureSize.width = 2592--------PictureSize.height = 1296
PictureSize.width = 2592--------PictureSize.height = 1232
PictureSize.width = 2592--------PictureSize.height = 1458
PictureSize.width = 2560--------PictureSize.height = 1920
PictureSize.width = 2688--------PictureSize.height = 1512
PictureSize.width = 2304--------PictureSize.height = 1728
PictureSize.width = 2304--------PictureSize.height = 1296
PictureSize.width = 2048--------PictureSize.height = 1536
PictureSize.width = 1920--------PictureSize.height = 1080
PictureSize.width = 1840--------PictureSize.height = 1380
PictureSize.width = 1600--------PictureSize.height = 1200
PictureSize.width = 1600--------PictureSize.height = 900
PictureSize.width = 1440--------PictureSize.height = 1080
PictureSize.width = 1280--------PictureSize.height = 960
PictureSize.width = 1280--------PictureSize.height = 768
PictureSize.width = 1280--------PictureSize.height = 720
PictureSize.width = 1024--------PictureSize.height = 768
PictureSize.width = 800--------PictureSize.height = 600
PictureSize.width = 800--------PictureSize.height = 480
PictureSize.width = 720--------PictureSize.height = 480
PictureSize.width = 640--------PictureSize.height = 480
PictureSize.width = 352--------PictureSize.height = 288
PictureSize.width = 320--------PictureSize.height = 240
PictureSize.width = 176--------PictureSize.height = 144
複製代碼
取容器的最大值而已,打印了一下,無誤
Size maxSize = Collections.max(sizes, (o1, o2) -> //獲取容器最大值
o1.getWidth() * o1.getHeight() - o2.getWidth() * o1.getHeight());
複製代碼
這下知道爲何一開始的時候是變形的了,SurfaceView將Camera的區域所有顯示
而後寬必須變窄才能容下,因此預覽看起來就是變窄了,但排出的照片是好的
1080*1920 | 3880*5184 |
---|---|
![]() |
![]() |
不能所見即所得,但它和界面不變形又兩者不可兼得
沒辦法,寫個函數適配一下吧,根據手機的尺寸來動態計算什麼時候的圖片大小
將屏幕高的2倍進行參考,取差值最小的index,再根據屏幕寬的兩倍,取差值最小的index
橫向縱向比較,就能取到最適合的了。看了一下,我手機自帶的相機拍出來的是2126*4608
/**
* 計算該手機合適的照片尺寸
*/
private void fitPhotoSize() {
// 獲取指定攝像頭的特性
CameraCharacteristics characteristics = null;
try {
characteristics = mCameraManager.getCameraCharacteristics(mCameraID);
// 獲取攝像頭支持的配置屬性
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
// 獲取攝像頭支持的最大尺寸
List<Size> sizes = Arrays.asList(map.getOutputSizes(ImageFormat.JPEG));
int minIndex = 0;//差距最小的索引
int minDx = Integer.MAX_VALUE;
int minDy = Integer.MAX_VALUE;
int[] dxs = new int[sizes.size()];
int justW = mWinSize.getHeight() * 2;//相機默認是橫向的,so
int justH = mWinSize.getWidth() * 2;
for (int i = 0; i < sizes.size(); i++) {
dxs[i] = sizes.get(i).getWidth() - justW;
}
for (int i = 0; i < dxs.length; i++) {
int abs = Math.abs(dxs[i]);
if (abs < minDx) {
minIndex = i;//獲取高的最適索引
minDx = abs;
}
}
for (int i = 0; i < sizes.size(); i++) {
Size size = sizes.get(i);
if (size.getWidth() == sizes.get(minIndex).getWidth()) {
int dy = Math.abs(justH - size.getHeight());
if (dy < minDy) {
minIndex = i;//獲取寬的最適索引
minDy = dy;
}
}
}
justSize = sizes.get(minIndex);
L.d(justSize + L.l());
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
複製代碼
因爲拍照的方法已封裝,因此
延遲拍照
功能和上一篇同樣
圖和上一篇差很少,直接拿來用了,錄個屏也怪麻煩...
這個效果百度找不到...根據套路看CaptureRequest源碼裏支持那些請求
看到SCALER_CROP_REGION
是一個Rect,感受有點像,就用了,而後瞎貓碰到死耗子...
/**
* 縮放封裝
*/
public void setZoom() {
if ((mRate - 1) * 10 / 4 + 1 > 4.6f) {
mRate = 1;
}
String rate = new DecimalFormat("#.0").format((mRate - 1) * 10 / 4 + 1);
mIdIvZoom.setText(rate + "x");
try {
CaptureRequest.Builder reqBuilder = mCameraDevice
.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 將SurfaceView的surface做爲CaptureRequest.Builder的目標
reqBuilder.addTarget(mHolder.getSurface());
reqBuilder.set(
CaptureRequest.SCALER_CROP_REGION,
new Rect(0, 0, (int) (justSize.getWidth() / mRate), (int) (justSize.getHeight() / mRate)));
mCameraCaptureSession.setRepeatingRequest(reqBuilder.build(), null, childHandler);
mRate += 0.15;
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
複製代碼
//開閃光燈
mIdIvSplash.setOnClickListener(v -> {
if (!isFlashLight) {
mIdIvSplash.setImageTintList(ColorStateList.valueOf(0xffEFB90F));
} else {
mIdIvSplash.setImageTintList(ColorStateList.valueOf(0xfffffffF));
}
isFlashLight = !isFlashLight;
try {
CaptureRequest.Builder reqBuilder = mCameraDevice
.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
reqBuilder.addTarget(mHolder.getSurface());
reqBuilder.set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON);
reqBuilder.set(CaptureRequest.FLASH_MODE,
isFlashLight?CameraMetadata.FLASH_MODE_TORCH:CameraMetadata.FLASH_MODE_OFF);
mCameraCaptureSession.setRepeatingRequest(reqBuilder.build(), null, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
});
複製代碼
/**
* 打開指定攝像頭
*/
private void changeCamera(int id) {
closeCamera();
try {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
mCameraManager.openCamera(id+"", mStateCallback, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
/**
* 關閉當前相機
*/
private void closeCamera() {
try {
mCameraOpenCloseLock.acquire();
if (null != mCameraCaptureSession) {
mCameraCaptureSession.close();
mCameraCaptureSession = null;
}
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
if (null != mImageReader) {
mImageReader.close();
mImageReader = null;
}
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
} finally {
mCameraOpenCloseLock.release();
}
}
複製代碼
有個小bug,只能錄一次...僅供參考,若是有好的解決方案,還請指教
/**
* 做者:張風捷特烈<br/>
* 時間:2019/1/8 0008:16:29<br/>
* 郵箱:1981462002@qq.com<br/>
* 說明:視頻錄製輔助類
*/
public class VideoRecorder2Utils {
private MediaRecorder mediaRecorder;
private SurfaceHolder.Callback callback;
private SurfaceView surfaceView;
private CameraDevice mCameraDevice;
private int height;
private int width;
List<Surface> surfaces = new ArrayList<>();
public static Size WH_2160X1080 = new Size(2160, 1080);
public static Size WH_1920X1080 = new Size(1920, 1080);
public static Size WH_1280X960 = new Size(1280, 960);
public static Size WH_1440X720 = new Size(1440, 720);
public static Size WH_1280X720 = new Size(1280, 720);
public static Size WH_864X480 = new Size(864, 480);
public static Size WH_800X480 = new Size(800, 480);
public static Size WH_720X480 = new Size(720, 480);
public static Size WH_640X480 = new Size(640, 480);
public static Size WH_352X288 = new Size(352, 288);
public static Size WH_320X240 = new Size(320, 240);
public static Size WH_176X144 = new Size(176, 144);
private CaptureRequest.Builder mPreviewBuilder;
private CaptureRequest mCaptureRequest;
private CameraCaptureSession mPreviewSession;
public void create(SurfaceView surfaceView, CameraDevice cameraDevice, Size size) {
this.surfaceView = surfaceView;
mCameraDevice = cameraDevice;
//建立錄製的session會話中的請求
try {
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
} catch (CameraAccessException e) {
e.printStackTrace();
}
height = size.getHeight();
width = size.getWidth();
mediaRecorder = new MediaRecorder();
}
public void stopRecord() {
mediaRecorder.release();
mediaRecorder = null;
mediaRecorder = new MediaRecorder();
surfaces.clear();
}
public void stop() {
if (mediaRecorder != null) {
mediaRecorder.release();
}
}
public void destroy() {
if (mediaRecorder != null) {
mediaRecorder.release();
mediaRecorder = null;
}
}
/**
* @param path 保存的路徑
* @param name 錄像視頻名稱(不包含後綴)
*/
public void startRecord(String path, String name, Handler handler) {
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setVideoEncodingBitRate(700 * 1024);
mediaRecorder.setVideoSize(width, height);
mediaRecorder.setVideoFrameRate(24);
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
mediaRecorder.setOutputFile(path + File.separator + name + ".mp4");
File file1 = new File(path + File.separator + name + ".mp4");
if (file1.exists()) {
file1.delete();
}
try {
mediaRecorder.prepare();
mediaRecorder.start();
} catch (IOException e) {
e.printStackTrace();
}
review(handler);
}
public void review(Handler handler) {
Surface previewSurface = surfaceView.getHolder().getSurface();
surfaces.add(previewSurface);
mPreviewBuilder.addTarget(previewSurface);
Surface recorderSurface = mediaRecorder.getSurface();
surfaces.add(recorderSurface);
mPreviewBuilder.addTarget(recorderSurface);
try {
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
//建立捕獲請求
mCaptureRequest = mPreviewBuilder.build();
mPreviewSession = session;
//設置反覆捕獲數據的請求,這樣預覽界面就會一直有數據顯示
mPreviewSession.setRepeatingRequest(mCaptureRequest, null, handler);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
}
}, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//清除預覽Session
private void closePreviewSession() {
if (mPreviewSession != null) {
mPreviewSession.close();
mPreviewSession = null;
}
}
}
複製代碼
/**
* 錄像
*/
private void recodeVideo() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath();
mVideoRecorderUtils.startRecord(path, "Video",childHandler);
mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xffff0000));
}
/**
* 中止錄像
*/
private void stopRecodeVideo() {
mIdIvSnap.setImageTintList(ColorStateList.valueOf(0xff0FC2EF));
mVideoRecorderUtils.stopRecord();
startPreview();
}
//打開照相機時初始化
mVideoRecorderUtils = new VideoRecorder2Utils();
mVideoRecorderUtils.create(mIdSvVideo, mCameraDevice,VideoRecorder2Utils.WH_720X480);
複製代碼
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-9 | Android多媒體之Camera2的相關操做 |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人掘金 | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持