Android音視訊-視訊採集(Camera2功能實現)
這一篇文章我們要實現Camera實現的等一些功能。熟悉Camera2API的使用,著重瞭解我們前面沒有深入瞭解的視訊錄製相關的內容。
基本功能實現
切換攝像頭
這個的實現和Camera API的步驟一摸一樣。只是換了一個API而已。Camera是通過Camera.CameraInfo去獲取相機,Camera2通過CameraManger去獲取裝置相機。關鍵程式碼如下:
private void getDefaultCameraId() {
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
try {
String[] cameraList = mCameraManager.getCameraIdList();
for (int i = 0; i < cameraList.length; i++) {
String cameraId = cameraList[i];
if (TextUtils.equals(cameraId, CAMERA_FONT)) {
mCameraId = cameraId;
break ;
} else if (TextUtils.equals(cameraId, CAMERA_BACK)) {
mCameraId = cameraId;
break;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 切換攝像頭
*/
public void switchCamera() {
if (TextUtils.equals(mCameraId, CAMERA_FONT)) {
mCameraId = CAMERA_BACK;
} else {
mCameraId = CAMERA_FONT;
}
closeCamera();
openCamera(getWidth(), getHeight());
}
拍照
使用Camera2API來進行拍照要使用ImageRender來實現。具體步驟如下:
- 設定ImageReader,回掉可用儲存圖片
private void setupImageReader() {
//2代表ImageReader中最多可以獲取兩幀影象流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.JPEG, 2);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage()));
}
}, mBackgroundHandler);
}
private static File mImageFile;
private ImageReader mImageReader;
private static class ImageSaver implements Runnable {
private Image mImage;
private ImageSaver(Image image) {
mImage = image;
}
@Override
public void run() {
ByteBuffer byteBuffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(mImageFile);
fileOutputStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
mImage.close();
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 在預覽建立CameraCaptureSession的時候除了預覽的TextureView,把ImageReader的Surface設定進行配置
//建立CaptureSession物件
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
//The camera is already closed
if (null == mCameraDevice) {
return;
}
Log.e(TAG, "onConfigured: ");
// When the session is ready, we start displaying the preview.
mCameraCaptureSessions = cameraCaptureSession;
//更新預覽
updatePreview();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Toast.makeText(mContext, "Configuration change", Toast.LENGTH_SHORT).show();
}
}, null);
這些程式碼在建立預覽View的方法裡面。
- 拍照
鎖定焦點
```
private void lockFocus() {
try {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);
mCameraCaptureSessions.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
拍照
private void capture() {
try {
final CaptureRequest.Builder mCaptureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
int rotation = ((Activity) mContext).getWindowManager().getDefaultDisplay().getRotation();
mCaptureBuilder.addTarget(mImageReader.getSurface());
mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
Toast.makeText(mContext, "Image Saved!", Toast.LENGTH_SHORT).show();
unLockFocus();
updatePreview();
}
};
mCameraCaptureSessions.stopRepeating();
mCameraCaptureSessions.capture(mCaptureBuilder.build(), CaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
解鎖焦點
private void unLockFocus() {
try {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
//mCameraCaptureSession.capture(mCaptureRequestBuilder.build(), null, mCameraHandler);
mCameraCaptureSessions.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
錄製視訊
開始錄製視訊
錄製視訊同樣使用MediaRecorder來協助完成。具體步驟如下:
關閉預覽會話
private void closePreviewSession() {
if (null != mCameraCaptureSessions) {
mCameraCaptureSessions.close();
mCameraCaptureSessions = null;
}
}
設定MediaRecorder
private void setUpMediaRecorder() throws IOException {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mVideoPath = getOutputMediaFile(MEDIA_TYPE_VIDEO);
mMediaRecorder.setOutputFile(mVideoPath.getAbsolutePath());
mMediaRecorder.setVideoEncodingBitRate(10000000);
mMediaRecorder.setVideoFrameRate(30);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
int rotation = ((Activity) mContext).getWindowManager().getDefaultDisplay().getRotation();
switch (mSensorOrientation) {
case SENSOR_ORIENTATION_DEFAULT_DEGREES:
mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
break;
case SENSOR_ORIENTATION_INVERSE_DEGREES:
mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
break;
}
mMediaRecorder.prepare();
}
構建請求建立會話
private void startRecordingVideo() {
if (null == mCameraDevice || !isAvailable() || null == mPreviewSize) {
return;
}
try {
closePreviewSession();
setUpMediaRecorder();
SurfaceTexture texture = getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
List<Surface> surfaces = new ArrayList<>();
// Set up Surface for the camera preview
Surface previewSurface = new Surface(texture);
surfaces.add(previewSurface);
mPreviewRequestBuilder.addTarget(previewSurface);
// Set up Surface for the MediaRecorder
Surface recorderSurface = mMediaRecorder.getSurface();
surfaces.add(recorderSurface);
mPreviewRequestBuilder.addTarget(recorderSurface);
// Start a capture session
// Once the session starts, we can update the UI and start recording
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
mCameraCaptureSessions = cameraCaptureSession;
updatePreview();
Toast.makeText(mContext, "start record video success", Toast.LENGTH_SHORT).show();
Log.e(TAG, "onConfigured: "+Thread.currentThread().getName());
// Start recording
mMediaRecorder.start();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Toast.makeText(mContext, "Failed", Toast.LENGTH_SHORT).show();
}
}, mBackgroundHandler);
} catch (CameraAccessException | IOException e) {
e.printStackTrace();
}
}
停止錄製視訊
private void stopRecordingVideo() {
// Stop recording
mMediaRecorder.stop();
mMediaRecorder.reset();
Toast.makeText(mContext, "Video saved: " + mVideoPath.getAbsolutePath(),
Toast.LENGTH_SHORT).show();
createCameraPreview();
}
照片新增水印
我們簡單的實現和使用Camera的時候一樣的水印的功能,拿到攝像頭回掉的每一幀資料,然後對它進行加工處理顯示到另外一個SurfaceView上面去。
獲取每一幀的回掉
在拍照的時候我們使用了ImageReader類來進行處理,在處理每一幀的回掉的時候我們還是要藉助這個類來進行處理。
- 在預覽請求類中新增target
mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
這樣新增以後我們可以得到相機的每一幀的資料
- 修改ImageReader的回掉函式
private void setupImageReader() {
//2代表ImageReader中最多可以獲取兩幀影象流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.JPEG, 1);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//這裡一定要呼叫reader.acquireNextImage()和img.close方法否則不會一直回掉了
Image img = reader.acquireNextImage();
img.close();
break;
}
}
}, mBackgroundHandler);
}
這裡把預覽的回掉和拍照的回掉做了一個區分的處理。
有一個要注意的是
Image img = reader.acquireNextImage(); img.close();
一定要在每一幀裡面拿了Image資料,不然會出現只回掉一次的問題。還有一個要注意的是我們每幀資料的格式我們現在設定的JPEG。這個格式有點繞下面瞭解
處理幀資料
private void setupImageReader() {
//2代表ImageReader中最多可以獲取兩幀影象流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.JPEG, 1);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
switch (mState) {
case STATE_PREVIEW:
//這裡一定要呼叫reader.acquireNextImage()和img.close方法否則不會一直回掉了
Image img = reader.acquireNextImage();
if (mIsAddWaterMark) {
try {
//獲取圖片byte陣列
Image.Plane[] planes = img.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
buffer.rewind();
byte[] data = new byte[buffer.capacity()];
buffer.get(data);
//從byte陣列得到Bitmap
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
//得到的圖片是我們的預覽圖片的大小進行一個縮放到水印圖片裡面可以完全顯示
bitmap = ImageUtil.zoomBitmap(bitmap, mWaterMarkPreview.getWidth(),
mWaterMarkPreview.getHeight());
//圖片旋轉 後置旋轉90度,前置旋轉270度
bitmap = BitmapUtils.rotateBitmap(bitmap, mCameraId.equals(CAMERA_BACK) ? 90 : 270);
//文字水印
bitmap = BitmapUtils.drawTextToCenter(mContext, bitmap,
System.currentTimeMillis() + "", 16, Color.RED);
// 獲取到畫布
Canvas canvas = mWaterMarkPreview.getHolder().lockCanvas();
if (canvas == null) return;
canvas.drawBitmap(bitmap, 0, 0, new Paint());
mWaterMarkPreview.getHolder().unlockCanvasAndPost(canvas);
} catch (Exception e) {
e.printStackTrace();
}
}
img.close();
break;
case STATE_CAPTURE:
mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage()));
break;
}
}
}, mBackgroundHandler);
}
還是上面的函式進行了拍照和幀資料返回的區分裡面的程式碼大致和使用Camera的時候一樣但是有一些坑。
設定ImageReader為ImageFormat.YUV_420_888的時候照片返回會流暢一些。但是,它要在的到拍照圖片的時候要轉換YUV->NV->JPEG,在經過這一轉換了的圖片會有一些綠色的遮罩在上面,很不好。
問題現象
找不到合適的解決這個圖片格式轉換的問題,於是只能初始化的時候設定為JPEG的輸出圖片了。還有一個問題就是換了JPEG格式輸出但是在一些配置較低的手機上面明顯就感覺相機的資料看上去有些卡了,看來上面這個問題我們還是得解決,但現在找不到解決的方案了。
總結
我們對於Camera和Camera2做的Demo都是對API有個一定的瞭解。但是要想實際專案裡面來用,那肯定是足夠的。API的相容上面都有很大的問題。仔細看了API的使用,然後有一些參考的程式碼值得學習。