上一篇文章主要介绍Camera的基本功能,我们在做相机应用的时候,除了相机的基本功能,还有一个非常重要的点,就是性能不能查,有几个方面:预览不能卡顿、拍照速度要快、录制视频不能卡。
我们做相机应用开发,不是做相机HAL层开发,优化的粒度没法像厂商name细致,上层可供优化的空间并不是很多,即使如此,大家在做相机调试的时候,还是有一些建议提供给大家。
相机处理放在子线程
如果你使用Camera1,开启预览要进行如下步骤:
这些操作可以放在单一线程中,只要你控制好先后顺序就行,Camera1的相机操作是同步的,执行完一个步骤需要等它结束返回值才行进行下一步,手机中相机必须是一个独占资源,需要通过CameraServer和HAL层交互,然后调用底层的sensor,放在子线程操作,可以防止出现ANR。所有有关Camera实例的操作都要放在子线程中进行。
Camera1中你要设置帧回调要调用Camera.setPreviewCallback(...),如果将onPreviewFrame作为帧回调的监测接口,会发现部分手机上出帧比较慢,例如设置了30fps,但是出帧速度最多20fps,HAL层会将数据同步处理之后才返回。不过PreviewCallback已经被废弃了,用的人应该不多。
这种情况建议使用SurfaceTexture.setOnFrameAvailableListener(...)来监控帧回调。
Camera2支持你设置相机处理的Handler,你可以自己定义HandlerThread来设置Camera2的相机操作Handler。
Zero-Shot拍照
我们想要调用相机拍照,用户点击拍照,Camera1执行takePicture函数开始拍照,此函数是异步返回照片数据,Camera2通过CameraCaptureSession的capture方法开始拍照。换言之,它们都是在你点击拍照的瞬间去底层取下一帧,然后开始返回数据的,出帧的时间至少需要33ms(假设帧率是30fps),还不算其他的耗时。
拍照之前我一直在预览中,如果在用户点击拍照的瞬间,我将指令传递下去,之前预览的那一帧作为拍照的帧来处理,这样的耗时几乎为0,大大降低了拍照的耗时。实际过程中,可能会存在拍照时没有聚焦的问题,还需要手动聚焦一下,或者设置相机长期聚焦。Camera2原生也是支持Zero-Shot模式的,这样省去了你定制的精力了。
全局Surface设置
这是针对Camera2的优化,正常情况下,我们使用Camera2开启预览、拍照、录像,需要设置几个Surface?
操作Camera2调用预览的完整流程:
第一步:获取CameraManager实例
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
第二步:获取特定的摄像头ID
mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_BACK);
第三步:打开摄像头
mCameraManager.openCamera(mCameraId, mStateCallback, mCameraHandler);
第四步:开始预览
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
applyFocusInternal(mPreviewBuilder);
applyFlashInternal(mPreviewBuilder, mFlash);
mSurfaceTexture.setDefaultBufferSize(mPreviewWidth, mPreviewHeight);
Surface surface = new Surface(mSurfaceTexture);
mPreviewBuilder.addTarget(surface);
mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
if (mCameraDevice == null) {
return;
}
Log.i(Constants.TAG, "onConfigured");
mCameraCaptureSession = session;
CaptureRequest previewRequest = mPreviewBuilder.build();
mCameraCaptureSession.setRepeatingRequest(previewRequest, null, mCameraHandler);
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, mCameraHandler);
如果你想加入拍照和录制视频,你需要创建额外两个CaptureRequest,而且需要分别设置拍照的surface——ImageReader.getSurface和录制视频的surface——MediaRecorder.getSurface,这样在预览、拍照、录制的过程中,你不仅需要创建多个CaptureRequest,还要设置多个Surface,这确实有点麻烦。
如果只设置一个Surface,后续所有的预览、拍照、录制视频都从这个Surface上取数据,也是可行的。上面我们拍照要借助ImageReader,利用其帧回调来取得具体的图片数据:
private ImageReader.OnImageAvailableListener mOnJpegImageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();
if (image != null) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer jpegByteBuffer = planes[0].getBuffer();
byte[] jpegByteArray = new byte[jpegByteBuffer.remaining()];
jpegByteBuffer.get(jpegByteArray);
generatePictureFile(jpegByteArray);
image.close();
}
}
};
如果是录制视频,需要借助系统API——MediaRecorder,创建特定的CaptureRequest来实现抓取视频帧的目的:
private boolean prepareVideoRecorder() {
mMediaRecorder = new MediaRecorder();
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
//3.设置视频的质量,但是这样设置不够精细化,如果想设置的更加细致一些,可以拆开来设置
mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
mMediaRecorder.setVideoSize(mVideoWidth, mVideoHeight);
mMediaRecorder.setOutputFile(MediaUtils.getOutputMediaFile(this.getApplicationContext(), MediaUtils.MEDIA_TYPE_VIDEO));
//6.设置旋转的角度
if (mCurrentCameraId.equals(mBackCameraId)) {
mMediaRecorder.setOrientationHint(mOrientation);
} else if (mCurrentCameraId.equals(mFrontCameraId)) {
mMediaRecorder.setOrientationHint(360 - mOrientation);
} else {
throw new RuntimeException("No available camera");
}
try {
mMediaRecorder.prepare();
} catch (Exception e) {
releaseMediaRecorder();
LogUtils.w(TAG, "MediaRecorder prepare failed, exception = " + e.getMessage());
return false;
}
return true;
}
private void recordVideoInternal() {
CameraDevice cameraDevice = mCameraDevice;
if (prepareVideoRecorder() && cameraDevice != null) {
//说明可以创建MediaRecorder
stopPreview();
try {
mCaptureVideoRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
List<Surface> outputSurfaces = new ArrayList<>();
Surface previewSurface = mPreviewSurface;
mCaptureVideoRequestBuilder.addTarget(previewSurface);
outputSurfaces.add(previewSurface);
Surface recordSurface = mMediaRecorder.getSurface();
if (recordSurface != null) {
mCaptureVideoRequestBuilder.addTarget(recordSurface);
outputSurfaces.add(recordSurface);
}
cameraDevice.createCaptureSession(outputSurfaces, mCaptureVideoStateCallback, mMainHandler);
} catch (Exception e) {
LogUtils.w(TAG, "Record video failed, exception = " + e.getMessage());
}
}
}
private CameraCaptureSession.StateCallback mCaptureVideoStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
//重新开始预览
try {
mCameraCaptureSession.setRepeatingRequest(mCaptureVideoRequestBuilder.build(), null, mMainHandler);
} catch (Exception e) {
LogUtils.w(TAG, "CaptureVideo setRepeatingRequest failed, exception = " + e.getMessage());
}
mMainHandler.post(new Runnable() {
@Override
public void run() {
//开始录制视频
mMediaRecorder.start();
mIsRecording = true;
}
});
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
}
};
设置多个Surface实现预览、拍照、录制的功能,效率太低了,可以只设置一个Surface,这个Surface上渲染的画面同时用来预览、拍照、录制。
你还知道哪些Camera性能优化的方法,一起私信讨论下吧。