Android Camera2: 输出预览画面到SurfaceT
项目地址:https://github.com/wangcan26/CameraCVGLView
简介
Android 官方在Android Api21也就是Android 5.0系统之后推出了Camera2摄像机类包, 该类包正式取代了原先的Camera类。相比于Camera,我认为Camera2有两方面明显的优势:
- Camera2提供了更加强大的对画面处理的功能,包括预处理, 预览,保存,后处理,录制等。
Camera Api对摄像头画面的处理仅限预览,保存和录制,并且,其这些处理没有一种统一的模式, 如对预览的处理,是通过setPreviewDisplay或者setPreviewTexture,对预览画面的保存通过tackPicture[参考1]和PreviewCallback[参考2], 而对录制功能的处理,需要借助MediaRecoder的setCamera来完成[参考3]。
Camera2对画面处理,都是通过addTarget的方式来完成的, 然后根据不同处理方式选择创建不同的任务请求(CameraRequest)。这样Camera2不仅可以通过添加 SurfaceHolder和SurfaceTexture的Surface来完成摄像头的预览,也可以通过添加ImageReader的Surface来完成对预览画面的保存,以及通过添加MediaCodec的Surface来完成录制,甚至通过添加Allocation的Surface来完成对预览画面的处理[参考4]。
从上述分析也可以看出, Camera2的Api相比于Camera的 Api,更加松散耦合。 - Camera2 提供了对摄像头设备属性更多的控制,如曝光的时长,白平衡的范围等,当然有些属性需要手机设备的支持[参考5]。
因此,Android对于Camera Api的废弃是必然,接下来本文以及本文相关的一系列文章都是对Camera2 Api的使用进行经验介绍。
目的
目前,Camera2 Api可以通过多种方式对摄像头的预览画面进行显示:
- 通过SurfaceView的SurfaceHolder直接显示[参考6], 该方法对于只是显示预览画面的功能足够了,但却不能满足对预览画面进一步的直接处理;
- 通过TextureView的SurfaceTexture来显示[参考7], 这种方法是google官方给出的sample, 该方法实现了显示的目的,同时可以将画面交给TextureView进一步做处理,但是实际应用中,使用TextureView 的例子并不多。
- 通过SurfaceTexture和OpenGL 渲染来完成对画面的显示。
上述第三种方法,能满足通过渲染进一步处理预览画面的需求,但是网上能看到实用的实现并不多, 这种不实用体现在不能很好的匹配Android生命周期流程(不同手机在Home键退出再进,锁屏等机制的差异化)。
本文针对第三种方法,提出了一种通过SurfaceView和SurfaceTexture组合的方法,并将实现提交到了GitHub[项目地址]以供大家参考。
流程
本文关于摄像头画面显示的java层全部封装在CameraRenderView.java类中, 关于实现的流程主要遵照Activity的生命周期和SurfaceHolder.Callback的生命周期, 如下所示:
Activity生命周期
public class MainActivity extends AppCompatActivity {
private CameraRenderView mCameraView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PermissionHelper.requestCameraPermission(this, true);
setContentView(R.layout.activity_main);
mCameraView = (CameraRenderView) findViewById(R.id.camera_view);
mCameraView.init(this);
}
@Override
protected void onResume() {
super.onResume();
if(mCameraView != null)
{
mCameraView.onResume();
}
}
@Override
protected void onPause() {
if(mCameraView != null)
{
mCameraView.onPause();
}
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mCameraView != null){
mCameraView.deinit();
}
}
}
以上,是响应Android Activity的生命周期方法, 注意在onCreate方法里,我们需要手动对Camera权限和读写权限进行申请, 另外关于权限的申请还需要在AndroidManifest.xml里进行声明,详情请参考[项目地址]。
SurfaceView.Callback
CameraRenderView类是基于SurfaceView的定制类, 一般我们使用SurfaceView最多的情况是利用其SurfaceHolder所提供的Surface创建一个OpenGL渲染环境(一个创建了EGLContext和EGLSurface的线程),关于这种使用用例,可参考GLSurfaceView.java的实现。
创建OpenGL渲染环境
public void init(Activity activity)
{
mWeakActivity = new WeakReference<>(activity);
mSurfaceHolder = this.getHolder();
mSurfaceHolder.addCallback(this);
mSurfaceHolder.setKeepScreenOn(true);
mIsSurfaceAvailable = false;
//Create a App
nativeCreateApp();
}
在该方法里,
首先,需要添加一个SurfaceHolder的回调,这样才能保证Callback方法被执行到
然后,创建一个OpenGL运行的线程。由于OpenGL涉及到渲染,所以其整个实现,本例全部在native层实现。 nativeCreateApp创建一个OpenGL线程。
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
mWidth = width;
mHeight = height;
//This method may block the ui thread until the gl context and surface texture id created
nativeSetSurface(surfaceHolder.getSurface());
...
mIsSurfaceAvailable = true;
}
surfaceChanged和surfaceCreate其实并没有本质的区别,唯一的区别就是当窗口大小改变时,就会调用surfaceChanged一次,所以,一般我们只在surfaceChanged里做创建的工作,另外用一个变量来纪录是否是第一次。
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
nativeSetSurface(null);
...
mIsSurfaceAvailable = false;
}
surfaceDestroyed在每次Activity 退到后台的时候会执行到, surfaceCreate和surfaceChanged会在Activity进入时被执行到。
nativeSetSurface根据传入surface的有无来判断是否在OpenGL线程里创建OpenGL环境。
关于OpenGL线程的销毁,我们放在deinit方法里去做。
Camera2预览实现
Camera2预览的大体流程如下:
首先, 我们需要通过CameraManager类来打开摄像头设备, 其结果通过CameraDevice.StateCallback告知用户
然后,通过摄像头设备创建一个CaptureRequest和CaptureSession, CaptureSession创建成功与否是通过CaptureSession.StateCallback告知
最后,通过创建的CaptureSession的setRepeatingRequest来完成预览
关于Camera2预览的流程可以参考Android-Camera-Archnitecture的第22页。
由于创建Session是一个特别耗时的工作[参考8],因此我们需要将创建Session的任务放在另外一个线程去做,并从CameraDevice.StateCallback开始就需要由该线程来处理。 这里,我们需要将线程的创建和Camera的打开以及它们的销毁都在SurfaceHolder.Callback方法里完成(不然会导致Activity切换前后台卡顿的现象)。
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
....
configureCamera(width, height);
//Only First time we open the camera
if(!mIsSurfaceAvailable)
{
if(mCameraId != null)
{
startCameraSessionThread();
openCamera();
}
}
...
}
我们需要选择打开摄像头的id, 是前置摄像头还是后置摄像头,以及为预览选择一个合适的大小
private void configureCamera(int width, int height)
{
//Assume it is a face back camera
mCameraId = CAMERA_FACE_BACK;
///Configure camera output surfaces
setupCameraOutputs(mWidth, mHeight);
}
通过CameraManager 打开摄像头之前需要确认摄像头权限, 在CameraSession线程中通过mCamSessionHandler处理CameraDevice.StateCallback
private void openCamera()
{
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
PermissionHelper.requestCameraPermission(getActivity(), true);
return;
}
...
try{
mCamManager.openCamera(mCameraId, mCameraDeviceCallback, mCamSessionHandler);
}catch (CameraAccessException e)
{
e.printStackTrace();
}
}
如果摄像头打开,那么开始创建CaptureSession
private CameraDevice.StateCallback mCameraDeviceCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
...
try{
createCameraPreviewSession();
}catch (CameraAccessException e)
{
e.printStackTrace();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
...
}
...
};
private void createCameraPreviewSession() throws CameraAccessException
{
try{
mSurface = getPreviewSurface(mPreviewSize);
mPreviewBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); //This must called before createCaptureSession
mCamera.createCaptureSession(Arrays.asList(mSurface, mImageReader.getSurface()), mSessionStateCallback,null);
}catch (CameraAccessException e)
{
e.printStackTrace();
}
}
在上述方法里,首先获取根据OpenGL的textureId创建的SurfaceTexture, 然后根据该SurfaceTexture创建一个用于预览显示的Surface(后续,预览画面通过渲染到该Surface上,将画面更新到textureId上,再由OpenGL渲染显示)
注意,这里创建CaptureRequest的类型为TEMPLATE_PREVIEW
创建的CaptureSession需要将预览数据流返回到配置的Surface上。
最后,当CaptureSession.StateCallback返回正确结果后, 开启预览:
// Session State Callback
private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
if(null == mCamera) return;
mCaptureSession = cameraCaptureSession;
try{
...
startPreview(mCaptureSession);
}catch (CameraAccessException e){
e.printStackTrace();
}
}
...
};
private void startPreview(CameraCaptureSession session) throws CameraAccessException{
//Set Surface of SurfaceView as the target of the builder
mPreviewBuilder.addTarget(mSurface);
mPreviewBuilder.addTarget(mImageReader.getSurface());
session.setRepeatingRequest(mPreviewBuilder.build(), mSessionCaptureCallback, mCamSessionHandler);
}
另外,关于摄像头的销毁工作:
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
...
closeCamera();
destroyImageReader();
stopCameraSessionThread();
destroyPreviewSurface();
...
}
注意,SurfaceTexture以及Suface的销毁需要放在CaptureSession销毁之后去做(不然会引起卡顿)。
结果
最终,本实现在坚果,华为Mate8, Google Pixel上都测试通过。
总结
本文实现并介绍了如何利用SurfaceTexture来渲染Camera2的预览画面并最终显示在SurfaceView。本文通过创建OpenGL渲染环境来实现对Camera2预览画面的渲染,渲染环境的线程以及SurfaceTexture是通过在native层实现的。SufaceView 以及渲染环境可以用GLSurfaceView来代替。该方法具有很强的实用性,用户即可以利用该方法实现OpenGL来处理摄像头画面,又可以利用该方法实现计算机视觉方面的工作。
欢迎 参考本文的项目地址: https://github.com/wangcan26/CameraCVGLView
参考
[参考1]: Camera Api. https://developer.android.com/reference/android/hardware/Camera .Android Developer
[参考2]: Camera.PreviewCallback. https://developer.android.com/reference/android/hardware/Camera.PreviewCallback .Android Developer
[参考3]: MediaRecoder. https://developer.android.com/reference/android/media/MediaRecorder#setCamera(android.hardware.Camera .Android Developer
[参考4]: Camera2 Api. https://developer.android.com/reference/android/hardware/camera2/package-summary .Android Developer
[参考5]: CameraMetaData. https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR .Android Developer
[参考6]: Android Camera2 拍照(一). https://blog.csdn.net/foolish0421/article/details/77732140 .hunter800421
[参考7]: android-Camera2Basic. https://github.com/googlesamples/android-Camera2Basic .google samples
[参考8]: CameraCaptureSession. https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession .Android Developer