###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的理解。
#####代码架构
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不受大家欢迎的主要原因是,它的所有操作基本都是传入回调方法,等待回调。让代码绕来绕去。
经过我的设计以后,希望能减少大家的困惑。
首先我们来了解大致的流程:
- 使用MyCameraManager保存cameraDevice对象;也就是getSystemService()拿到api
cameraManager后,open传入CameraDevice.StateCallback回调回来以后,保存下来; - camera2比较绕的地方,是需要创建一个requestBuilder,给它添加addTarget surface(送显或者录制使用),可以多个;然后拿到request去创建session;
- session创建成功的回调onConfigured()就可以保存下来,setRepeatingRequest进去,代表着某一类的显示已经成功。
那么,基于这种架构,我设计了一个抽象类+Handler消息处理机制来流转状态和控制显示模式。
首先,如上述流程图中,StateBase类所支持的能力:
- 抽象了需要创建的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。
** - 抽象了显示模式:step1_getTemplateType()给出该模式的capture templateType;
- 在抽象类直接处理target的贴入,因为第一步已经分为2个数列(allIncludePictureSurfaces和targetSurfaces)这里将targetSurfaces贴入previewBuild;
- createCaptureSession();将allIncludePictureSurfaces作为第一个参数;
- 给createcaptureSession的回调构建抽象方法,子类创建这个回调用于处理具体业务 protected abstract CameraCaptureSession.StateCallback createStateCallback();
State+Handler模式
这种设计模式,其实是android framework的StateMachine机制。大概我的意图是这个方向,可能具体有些差异,不过能达到的目的是一致的控制状态的流转,防止太多同时的操作导致状态的异常。
理解了google的demo以后,我知道了State有N种(实际中,2种即可):
- 纯Preview的State;
- Preview+Picture的State,注意它并不是拍照瞬间而是一直存在的,takePhoto可以看做一个额外的能力function; 2019.11.17将TakePictureBuilder作为一个能力加进来;因此他与前者纯Preview State可以继承;
- 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的图,主要展示出几个状态的切换。他们内部具有的能力,在继承关系上可以体现出来。
录像
录像的过程,跟拍照在传统的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不清晰的,可以看看我的代码,从架构上很好的剥离了复杂的逻辑。更容易理解其中的关键。