###Android Camera2 API预览 拍照和录像

code链接 git clone https://github.com/jzlhll/AndroidCam2Demo.git
#####原因
最近学习Camera2,发现Camera2的架构体系变化非常大。想要直接看懂google的android-Camera2Basic 和 android-Camera2Video,在架构体系上,做的不是很好,session,request写的比较绕。
于是,我使用了设计模式,让代码看上去更加有条理性。我想应该是入门最好的帖子了。

  • 优点1:一个APP,按照不同的场景,设计了3种状态,预览,拍照和录像;
  • 优点2:架构清晰,简单,很容易掌握到camera2的设计理念;
  • API:Camera2,MediaRecorder,TextureView/SurfaceView,ImageReader。
  • 2019.11.17更新:不要急于跳出关闭网页,查找其中红色部分对于surfaces的理解。

#####代码架构

android12 获取摄像头characteristics 安卓摄像头api_mediarecorder


Camera View & Activity

使用了Material design风格(无所谓)Layout中,传入一个自定义的CamSurfaceView或者CamTextureView:

<com.allan.androidcam2api.view.CamTextureView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="512dp"
        tools:ignore="MissingConstraints"
        tools:layout_editor_absoluteX="9dp"
        tools:layout_editor_absoluteY="0dp" />

在CamSurfaceView/TextureView中初始化的时候,设置监听回调,getHolder().addCallback(this),在回调函数:

public interface IViewStatusChangeCallback {
    void onSurfaceCreated();
    void onSurfaceDestroyed();
}
...
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    if (mCallback != null) mCallback.onSurfaceCreated();
}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    if (mCallback != null) mCallback.onSurfaceDestroyed();
    return false;
}

类似的,CamTextureView setSurfaceTextureListener(this)并将CameraView进行包装统一设置到MainActivityCameraViewPresent.java中跟cameraManager接触。

@Override
public void onSurfaceCreated() {
...
	   myCameraManager.init(mainActivity, mCameraView, transmitId);
	   myCameraManager.addModChanged(mainActivity);
	   mActivity.get().openCamera();
}
@Override
public void onSurfaceDestroyed() {
    MyCameraManager.me.get().closeCamera();
}

开始进行单例类MyCameraManager的初始化过程。


MyCameraManager & StateBase & Camera2

之所以Camera2不受大家欢迎的主要原因是,它的所有操作基本都是传入回调方法,等待回调。让代码绕来绕去。

经过我的设计以后,希望能减少大家的困惑。

首先我们来了解大致的流程:

android12 获取摄像头characteristics 安卓摄像头api_mediarecorder_02

  • 使用MyCameraManager保存cameraDevice对象;也就是getSystemService()拿到api
    cameraManager后,open传入CameraDevice.StateCallback回调回来以后,保存下来;
  • camera2比较绕的地方,是需要创建一个requestBuilder,给它添加addTarget surface(送显或者录制使用),可以多个;然后拿到request去创建session;
  • session创建成功的回调onConfigured()就可以保存下来,setRepeatingRequest进去,代表着某一类的显示已经成功。

那么,基于这种架构,我设计了一个抽象类+Handler消息处理机制来流转状态和控制显示模式。
首先,如上述流程图中,StateBase类所支持的能力:

  1. 抽象了需要创建的surface个数:protected abstract void step0_createSurfaces();
    ** 这里不得不提到的是理解camera2很重要的一环,是理解Surface的基本贴入与拍照的surface的区别。 在创建Surfaces的时候,将比如预览或者预览+录像,常驻的surfaces加入addTargetSurfaces用于createCaptureRequest previewBuilder; 而比addTargetSurfaces要多的是,包含拍照的surface也一并保存到allIncludePictureSurfaces中,这个用于createCaptureSession()第一个参数The new set of Surfaces that should be made available as targets for captured image data。**
  2. 抽象了显示模式:step1_getTemplateType()给出该模式的capture templateType;
  3. 在抽象类直接处理target的贴入,因为第一步已经分为2个数列(allIncludePictureSurfaces和targetSurfaces)这里将targetSurfaces贴入previewBuild;
  4. createCaptureSession();将allIncludePictureSurfaces作为第一个参数;
  5. 给createcaptureSession的回调构建抽象方法,子类创建这个回调用于处理具体业务 protected abstract CameraCaptureSession.StateCallback createStateCallback();

State+Handler模式

这种设计模式,其实是android framework的StateMachine机制。大概我的意图是这个方向,可能具体有些差异,不过能达到的目的是一致的控制状态的流转,防止太多同时的操作导致状态的异常

理解了google的demo以后,我知道了State有N种(实际中,2种即可):

  1. 纯Preview的State;
  2. Preview+Picture的State,注意它并不是拍照瞬间而是一直存在的,takePhoto可以看做一个额外的能力function; 2019.11.17将TakePictureBuilder作为一个能力加进来;因此他与前者纯Preview State可以继承;
  3. Preview+Picture+Record的State, 注意它在进入录制状态就切换到这个State,而停止录制必须回调Preview+Picture State;

这个的区分,**以Surface的个数和是否需要重新CreateSession为标准 **。

回归到代码,第一步,openCamera() -> mCamHandler.sendEmptyMessage(OPEN);
打开以后得到回调后的mCameraDevice;紧接着,transmitModPicturePreview() ->则切换到preview+picture模式;

case MOD_PREVIEW_PIC: {
AbstractStateBase curSt = mManager.getCurrentState();
if (curSt == null) {
         CamLog.d("Goto TRANSMIT_TO_MODE_PICTURE_PREVIEW mode error cause it's deed");
         ifCurrentStNullOpenCameraFirst(msg);
         return;
     }

     if (curSt.getFeatureId() == (FeatureUtil.FEATURE_PREVIEW |FeatureUtil.FEATURE_PICTURE)) {
         if (mWfContext.get() != null) {
             MyToast.toastNew(mWfContext.get(), toastParentView, "Already in this mod");
         }
         return;
     }

     mManager.notifyModChange(MainActivity.MODE_PREVIEW_PICTURE);
     curSt.closeSession(); //关闭session

     curSt = new StatePictureAndPreview(mManager);
     mManager.setCurrentState(curSt);

     try {
         curSt.createSession(new StatePictureAndPreview.IStatePreviewCallback() {
             @Override
             public void onPreviewSucceeded() {
                 CamLog.d("onPreviewSucceeded in myacmera");
             }

             @Override
             public void onPreviewFailed() {
                 CamLog.d("onPreviewFailed in myacmera");
             }
         });
     } catch (Exception e) {
         CamLog.e("start preview err0");
         e.printStackTrace();
     }

这仅仅是创建了一种显示模式,而,拍照只是session下的一个动作。但是拍照的surfaces必须前提贴入(查看前面红色字段,反复提到,备注:20191117这是实践得到的,必须前提贴入)。

只要当前State包含拍照的能力,即可调用它内部的方法。

阅读代码,关注几个transmit的Handler处理流程,并注意Start-Record其实就是一种特殊的切换State模式即可理解我设计的状态机的流转了。

下面一副low low的图,主要展示出几个状态的切换。他们内部具有的能力,在继承关系上可以体现出来。

android12 获取摄像头characteristics 安卓摄像头api_android_03


android12 获取摄像头characteristics 安卓摄像头api_android_04


录像
录像的过程,跟拍照在传统的Camera1的思想是不一致的。Camera1的时候我们使用MediaRecord的时候,没有设置过surface或者设置过surface,其实是framework进行过转换。这里我们可以显式地看出,addTarget的时候,将preview和record的surface都贴入了request。


其他
utils包里面,有几个重要的东西,android6.0以上Perssion申请的代码,这里引用了网上的东西,再此致谢;
Singleton设计,在我的其他文章中有讲;
里面穿插了很多的func接口,这是我写代码喜欢的一种方式,类似IOS上的block设计。在调用方法处比如takePicture(callback), 那么,可以很轻松的在callback中写处理逻辑。代码架构很清晰。


遗憾
有些旋转rotation,闪光灯没有写完,自行查阅相关资料将这些加入;不过都留了TODO;

其实搭这个架构写代码没花太多时间,而在研究动态适配Camera Surface显示分辨率,比如对着一个圆进行浏览的时候,这个圆是否正。这方面百度和google都未能找到满意的答案,因为Camera2相关的介绍太少,基本都是基于google demo在复述。自我夸奖下,能重构到我这个地步,没看到~
很遗憾,在研究了很久很久好几天,setAspectRatio,通过设置可见区域onMesure()和setDefaultBufferSize对texture配置显示比例。都未能达到十分满意的效果。对size我的建议是,一般来说,应用需要找到一个合理的显示比例,固定下来。然后分辨率寻找一个16:9或者4:3定下来。

有兴趣的这部分可以继续,代码中已经留下来了setPreviewSize内很多的策略。

总体来说,如果对Camera2的使用,如果认为查看google demo不清晰的,可以看看我的代码,从架构上很好的剥离了复杂的逻辑。更容易理解其中的关键。