1. 前言
这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github
中的issues
中,有些BUG
作者一直没有修复。
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
而这篇文章是其中关于CameraView
怎么进行预览的源码解析。
以下源码解析基于CameraView 2.7.2
kotlin
implementation("com.otaliastudios:cameraview:2.7.2")
为了在博客上更好的展示,本文贴出的代码进行了部分精简
2. 初始化CameraEngine
在CameraView
构造方法中,会调用doInstantiateEngine
,用来初始化CameraEngine
。
CameraEngine
是一个抽象类,根据我们的配置分别返回Camera1Engine
和Camera2Engine
。
可以看到,这里mExperimental
成立并且engine
等于CAMERA2
的情况下,会返回Camera2Engine
。
java
protected CameraEngine instantiateCameraEngine(Engine engine, CameraEngine.Callback callback) {
if (mExperimental && engine == Engine.CAMERA2) {
return new Camera2Engine(callback);
} else {
mEngine = Engine.CAMERA1;
return new Camera1Engine(callback);
}
}
所以如果我们要使用Camera2 API
,需要配置上app:cameraEngine="camera2"
和app:cameraExperimental="true"
xml
<com.otaliastudios.cameraview.CameraView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cameraEngine="camera2"
app:cameraExperimental="true" />
2. 初始化CameraPreview
在CameraView
调用View
生命周期中的onAttachedToWindow
的时候,调用了doInstantiatePreview()
方法,初始化预览相关代码
kotlin
void doInstantiatePreview() {
mCameraPreview = instantiatePreview(mPreview, getContext(), this);
mCameraEngine.setPreview(mCameraPreview);
if (mPendingFilter != null) {
setFilter(mPendingFilter);
mPendingFilter = null;
}
}
mCameraPreview
是CameraPreview
抽象类,根据xml
中不同的app:cameraPreview
配置会创建不同的CameraPreview
surface
: 创建SurfaceCameraPreview
texture
: 创建TextureCameraPreview
glSurface
: 创建GlCameraPreview
这里以surface
为例,实际创建的CameraPreview
是SurfaceCameraPreview
,其职责就是在初始化方法的时候,通过LayoutInflater.from
创建SurfaceView
,并封装了SurfaceHolder.Callback
回调。
java
public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHolder> {
//...省略了部分代码...
@Override
protected SurfaceView onCreateView(@NonNull Context context, @NonNull ViewGroup parent) {
View root = LayoutInflater.from(context).inflate(R.layout.cameraview_surface_view, parent, false);
parent.addView(root, 0);
SurfaceView surfaceView = root.findViewById(R.id.surface_view);
final SurfaceHolder holder = surfaceView.getHolder();
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(new SurfaceHolder.Callback() {
//...
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (!mDispatched) {
dispatchOnSurfaceAvailable(width, height);
mDispatched = true;
} else {
dispatchOnSurfaceSizeChanged(width, height);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
LOG.i("callback: surfaceDestroyed");
dispatchOnSurfaceDestroyed();
mDispatched = false;
}
});
mRootView = root;
return surfaceView;
}
}
3. Camera2Engine和CameraView建立关联
接着,调用mCameraEngine.setPreview(mCameraPreview);
,mCameraEngine
内部设置了SurfaceCallback
回调,SurfaceCallback
回调有onSurfaceAvailable
、onSurfaceChanged
、onSurfaceDestroyed
三个方法。
kotlin
public final void setPreview(@NonNull CameraPreview cameraPreview) {
if (mPreview != null) mPreview.setSurfaceCallback(null);
mPreview = cameraPreview;
mPreview.setSurfaceCallback(this);
}
4. 回调SurfaceCallBack.onSurfaceAvailable
在CameraBaseEngine
的onSurfaceAvailable
回调中,调用了startBind()->onStartBind()
和startPreview()->onStartPreview()
java
@Override
public final void onSurfaceAvailable() {
startBind();
startPreview();
}
onStartBind
和onStartPreview
是抽象方法,Camera1Engine
和Camera2Engine
分别实现了CameraBaseEngine
,分别用来实现Camera1
和Camera2
。
kotlin
@NonNull
@EngineThread
protected abstract Task<Void> onStartBind();
@NonNull
@EngineThread
protected abstract Task<Void> onStartPreview();
这里以Camera2
为例,对于Camera2
不了解的同学,可以先看我的另一篇博客 : 十分钟实现 Android Camera2 相机预览 。
接下来的部分,就是Camera2 API
绑定Surface
和预览的具体实现了。
5. onStartBind
5.1 估算图像尺寸
分别估算出预览和拍照的图像尺寸,为后续预览和拍照做准备
java
//估算出拍照时图像的尺寸
mCaptureSize = computeCaptureSize();
//估算出预览时的图像尺寸
mPreviewStreamSize = computePreviewStreamSize();
5.2 添加预览SurfaceHolder
接着调用setFixedSize
,给SurfaceView
设置刚才估算出来的预览的尺寸 ;
然后调用outputSurfaces.add
,将CameraView
的SurfaceHolder
添加到outputSurfaces
列表中。
java
List<Surface> outputSurfaces = new ArrayList<>();
final Class outputClass = mPreview.getOutputClass();
final Object output = mPreview.getOutput();
if (outputClass == SurfaceHolder.class) {
Tasks.await(Tasks.call(new Callable<Void>() {
@Override
public Void call() {
//必须在UI线程调用
((SurfaceHolder) output).setFixedSize(
mPreviewStreamSize.getWidth(),
mPreviewStreamSize.getHeight());
return null;
}
}));
mPreviewStreamSurface = ((SurfaceHolder) output).getSurface();
} else if (outputClass == SurfaceTexture.class) {
//...省略了关于SurfaceTexture的实现...
} else {
throw new RuntimeException("Unknown CameraPreview output class.");
}
outputSurfaces.add(mPreviewStreamSurface);
5.3 初始化视频录制相关类
如果是Video
模式,那么会初始化Full2VideoRecorder
,这个专门用来在Camera2
中录制视频的类。
接着在outputSurfaces
列表中添加Full2VideoRecorder
单独创建的Surface
。( 实质就是从mMediaRecorder.getSurface()
中获取的Surface
)
java
if (getMode() == Mode.VIDEO) {
if (mFullVideoPendingStub != null) {
Full2VideoRecorder recorder = new Full2VideoRecorder(this, mCameraId);
try {
outputSurfaces.add(recorder.createInputSurface(mFullVideoPendingStub));
} catch (Full2VideoRecorder.PrepareException e) {
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
}
mVideoRecorder = recorder;
}
}
5.4 初始化拍照相关类
如果是Picture
模式,则会先判断设置的图像格式,如果不是JPEG
或DNG
,则抛出异常,说明不支持该格式。
接着会根据mCaptureSize
拍照尺寸和图片格式,创建mPictureReader
,这个类是Camera2
中拍照要用到的类,用来获取相机捕获的图像数据。
接着会将mPictureReader
中的Surface
也添加到outputSurfaces
列表中。
java
if (getMode() == Mode.PICTURE) {
int format;
switch (mPictureFormat) {
case JPEG: format = ImageFormat.JPEG; break;
case DNG: format = ImageFormat.RAW_SENSOR; break;
default: throw new IllegalArgumentException("Unknown format:" + mPictureFormat);
}
mPictureReader = ImageReader.newInstance(
mCaptureSize.getWidth(),
mCaptureSize.getHeight(),
format, 2);
outputSurfaces.add(mPictureReader.getSurface());
}
5.5 帧处理
创建一个帧处理的ImageReader
,名字叫做mFrameProcessingReader
。
并将其添加到outputSurfaces
列表中。
java
if (hasFrameProcessors()) {
mFrameProcessingSize = computeFrameProcessingSize();
/**
* 很难把原因写出来,但是在Camera2中,我们需要的帧数比图像数少1。
* 如果我们让所有图像都成为帧的一部分,从而让所有图像在任何给定时刻被处理器使用,Camera2输出就会中断。
* 事实上,如果没有可用的图像,传感器会阻塞,直到它找到一个图像,这是一个大问题,因为处理器时间成为预览的瓶颈。
* 这是ImageReader / sensor实现中的一个设计缺陷,因为如果没有可用的图像,它们应该简单地将写入的帧放置到surface上。
* 由于这不是事情的工作方式,我们确保在这里始终有一个图像可用。
*/
mFrameProcessingReader = ImageReader.newInstance(
mFrameProcessingSize.getWidth(),
mFrameProcessingSize.getHeight(),
mFrameProcessingFormat,
getFrameProcessingPoolSize() + 1);
mFrameProcessingReader.setOnImageAvailableListener(this,
null);
mFrameProcessingSurface = mFrameProcessingReader.getSurface();
outputSurfaces.add(mFrameProcessingSurface);
} else {
mFrameProcessingReader = null;
mFrameProcessingSize = null;
mFrameProcessingSurface = null;
}
这里特别需要注意的是这个ImageReader
调用了setOnImageAvailableListener
,当有图像数据时,就会回调onImageAvailable
方法。
这里会从reader.acquireLatestImage()
中获取到android.media.Image
对象,然后将其组装成com.otaliastudios.cameraview.frame.Frame
对象,接着调用getCallback().dispatchFrame(frame);
来进行回调。
java
@EngineThread
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (getState() == CameraState.PREVIEW && !isChangingState()) {
Frame frame = getFrameManager().getFrame(image, System.currentTimeMillis());
if (frame != null) {
getCallback().dispatchFrame(frame);
}
} else {
image.close();
}
}
这个getCallback()
是在哪里设置的呢 ? CameraView
中有addFrameProcessor
方法,专门用来设置这个回调。
java
public void addFrameProcessor(@Nullable FrameProcessor processor) {
if (processor != null) {
mFrameProcessors.add(processor);
if (mFrameProcessors.size() == 1) {
mCameraEngine.setHasFrameProcessors(true);
}
}
}
所以我们想要取到预览时候的实时帧数据,就在自己Activity
的代码中,添加这个回调就行。
kotlin
cameraView.addFrameProcessor {
//预览每一帧的回调
val image = it.getData<Image>()
Log.i(TAG, "image width:${image.width} height:${image.height}")
image.close()
}
5.6 创建CameraCaptureSession
这里就是根据outputSurfaces
列表,创建对应的android.hardware.camera2.CameraCaptureSession
,从而实现关联对应功能的Surface
。
java
mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mSession = session;
task.trySetResult(null);
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
//...省略了配置失败的代码...
}
}, null);
6. onStartPreview
6.1 重新测量CaemraView大小
首先调用回调方法onCameraPreviewStreamSizeChanged()
,内部会去调用下requestLayout()
,从而触发onMeasure
来重新测量CameraView
尺寸。
java
getCallback().onCameraPreviewStreamSizeChanged();
6.2 设置CameraPreview
这个previewSizeForView
就是在onStartBind()
中估算出来的预览大小,并且会对传感器的方向做翻转操作。并将其设置到mPreview
中,CameraPreview
是一个抽象类,具体实现类有SurfaceCameraPreview
、TextureCameraPreview
、GlCameraPreview
,这里以SurfaceCameraPreview
为例。
java
Size previewSizeForView = getPreviewStreamSize(Reference.VIEW);
//设置预览尺寸大小
mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight());
//给mPreview设置绘制的方向
mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE));
if (hasFrameProcessors()) {
//如果有FrameProcessors,那么初始化FrameManager
getFrameManager().setUp(mFrameProcessingFormat, mFrameProcessingSize, getAngles());
}
public final Size getPreviewStreamSize(@NonNull Reference reference) {
Size size = mPreviewStreamSize;
if (size == null) return null;
return getAngles().flip(Reference.SENSOR, reference) ? size.flip() : size;
}
6.3 调用setRepeatingRequest
接着会调用这两句
java
addRepeatingRequestBuilderSurfaces();
applyRepeatingRequestBuilder(false, CameraException.REASON_FAILED_TO_START_PREVIEW);
addRepeatingRequestBuilderSurfaces
会对mRepeatingRequestBuilder
做一些配置,将预览的Surface
添加到mRepeatingRequestBuilder
中,mRepeatingRequestBuilder
是android.hardware.camera2.CaptureRequest.Builder
类,是接下来调用Camera2
中调用setRepeatingRequest
必备的一个参数。
java
private void addRepeatingRequestBuilderSurfaces(@NonNull Surface... extraSurfaces) {
mRepeatingRequestBuilder.addTarget(mPreviewStreamSurface);
if (mFrameProcessingSurface != null) {
mRepeatingRequestBuilder.addTarget(mFrameProcessingSurface);
}
for (Surface extraSurface : extraSurfaces) {
if (extraSurface == null) {
throw new IllegalArgumentException("Should not add a null surface.");
}
mRepeatingRequestBuilder.addTarget(extraSurface);
}
}
然后调用applyRepeatingRequestBuilder
,在内部会调用setRepeatingRequest
,因为mRepeatingRequestBuilder
中添加了预览的Surface
,所以调用后将不断地实时发送视频流给预览的Surface
,从而实现了预览的效果。
java
@EngineThread
private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) {
if ((getState() == CameraState.PREVIEW && !isChangingState()) || !checkStarted) {
//这将不断地实时发送视频流,直到会话断开或调用session.stoprepeat()
mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(),
mRepeatingRequestCallback, null);
}
}
7. 小结
到这里我们就对于CameraView
的预览流程有了大致的了解了,内部就是调用了Camera2
的API
,关联SurfaceView
进行预览。
- 创建
mCameraPreview
,具体实现类是SurfaceCameraPreview
,内部封装了SurfaceView
- 并提供了
SurfaceCallback
接口用来回调onSurfaceAvailable()
、onSurfaceChanged()
、onSurfaceDestroyed
方法
- 并提供了
- 调用
Camera2Engine.setPreview(CameraPreview)
,就是Camera2Engine
实现了CameraPreview
的SurfaceCallback
回调 - 在回调的
onSurfaceAvailable()
方法里- 估算出预览和拍照的尺寸
- 将
CaemraView
中的SurfaceHolder
添加到outputSurfaces
列表 - 如果是
video
模式,初始化视频录制Surface
并添加到outputSurfaces
列表 - 如果是
picture
模式,初始化拍照Surface
并添加到outputSurfaces
列表 - 初始化帧处理
Surface
,并添加到outputSurfaces
列表 - 根据
outputSurfaces
列表创建CameraCaptureSession
:CameraDevice.createCaptureSession()
- 调用
CameraCaptureSession.setRepeatingRequest()
开始不断地传递视频流给预览的Surface
,从而完成预览功能
8. 其他
8.1 CameraView源码解析系列
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客