Android 相机库CameraView源码解析 (一) : 预览

1. 前言

这段时间,在使用 natario1/CameraView 来实现带滤镜的预览拍照录像功能。
由于CameraView封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github中的issues中,有些BUG作者一直没有修复。

那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
而这篇文章是其中关于CameraView怎么进行预览的源码解析。

以下源码解析基于CameraView 2.7.2

implementation("com.otaliastudios:cameraview:2.7.2")

为了在博客上更好的展示,本文贴出的代码进行了部分精简

在这里插入图片描述

2. 初始化CameraEngine

CameraView构造方法中,会调用doInstantiateEngine,用来初始化CameraEngine
CameraEngine是一个抽象类,根据我们的配置分别返回Camera1EngineCamera2Engine
可以看到,这里mExperimental成立并且engine等于CAMERA2的情况下,会返回Camera2Engine

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"

<com.otaliastudios.cameraview.CameraViewandroid:layout_width="match_parent"android:layout_height="match_parent"app:cameraEngine="camera2"app:cameraExperimental="true" />

2. 初始化CameraPreview

CameraView调用View生命周期中的onAttachedToWindow的时候,调用了doInstantiatePreview()方法,初始化预览相关代码

void doInstantiatePreview() {mCameraPreview = instantiatePreview(mPreview, getContext(), this);mCameraEngine.setPreview(mCameraPreview);if (mPendingFilter != null) {setFilter(mPendingFilter);mPendingFilter = null;}
}

mCameraPreviewCameraPreview抽象类,根据xml中不同的app:cameraPreview配置会创建不同的CameraPreview

  • surface : 创建SurfaceCameraPreview
  • texture : 创建TextureCameraPreview
  • glSurface : 创建GlCameraPreview

这里以surface为例,实际创建的CameraPreviewSurfaceCameraPreview,其职责就是在初始化方法的时候,通过LayoutInflater.from创建SurfaceView,并封装了SurfaceHolder.Callback回调。

public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHolder> {//...省略了部分代码...@Overrideprotected 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() {//...@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {if (!mDispatched) {dispatchOnSurfaceAvailable(width, height);mDispatched = true;} else {dispatchOnSurfaceSizeChanged(width, height);}}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {LOG.i("callback: surfaceDestroyed");dispatchOnSurfaceDestroyed();mDispatched = false;}});mRootView = root;return surfaceView;}
}

3. Camera2Engine和CameraView建立关联

接着,调用mCameraEngine.setPreview(mCameraPreview);mCameraEngine内部设置了SurfaceCallback回调,SurfaceCallback回调有onSurfaceAvailableonSurfaceChangedonSurfaceDestroyed三个方法。

public final void setPreview(@NonNull CameraPreview cameraPreview) {if (mPreview != null) mPreview.setSurfaceCallback(null);mPreview = cameraPreview;mPreview.setSurfaceCallback(this);
}

4. 回调SurfaceCallBack.onSurfaceAvailable

CameraBaseEngineonSurfaceAvailable回调中,调用了startBind()->onStartBind()startPreview()->onStartPreview()

@Override
public final void onSurfaceAvailable() {startBind();startPreview();
}

onStartBindonStartPreview是抽象方法,Camera1EngineCamera2Engine分别实现了CameraBaseEngine,分别用来实现Camera1Camera2

@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 估算图像尺寸

分别估算出预览和拍照的图像尺寸,为后续预览和拍照做准备

//估算出拍照时图像的尺寸
mCaptureSize = computeCaptureSize();
//估算出预览时的图像尺寸
mPreviewStreamSize = computePreviewStreamSize();

5.2 添加预览SurfaceHolder

接着调用setFixedSize,给SurfaceView设置刚才估算出来的预览的尺寸 ;
然后调用outputSurfaces.add,将CameraViewSurfaceHolder添加到outputSurfaces列表中。

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>() {@Overridepublic 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 )

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模式,则会先判断设置的图像格式,如果不是JPEGDNG,则抛出异常,说明不支持该格式。
接着会根据mCaptureSize拍照尺寸和图片格式,创建mPictureReader,这个类是Camera2中拍照要用到的类,用来获取相机捕获的图像数据。
接着会将mPictureReader中的Surface也添加到outputSurfaces列表中。

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列表中。

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);来进行回调。

@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方法,专门用来设置这个回调。

public void addFrameProcessor(@Nullable FrameProcessor processor) {if (processor != null) {mFrameProcessors.add(processor);if (mFrameProcessors.size() == 1) {mCameraEngine.setHasFrameProcessors(true);}}
}

所以我们想要取到预览时候的实时帧数据,就在自己Activity的代码中,添加这个回调就行。

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

mCamera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mSession = session;task.trySetResult(null);}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession session) {//...省略了配置失败的代码...}
}, null);

6. onStartPreview

6.1 重新测量CaemraView大小

首先调用回调方法onCameraPreviewStreamSizeChanged(),内部会去调用下requestLayout(),从而触发onMeasure来重新测量CameraView尺寸。

 getCallback().onCameraPreviewStreamSizeChanged();

6.2 设置CameraPreview

这个previewSizeForView就是在onStartBind()中估算出来的预览大小,并且会对传感器的方向做翻转操作。并将其设置到mPreview中,CameraPreview是一个抽象类,具体实现类有SurfaceCameraPreviewTextureCameraPreviewGlCameraPreview,这里以SurfaceCameraPreview为例。

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,那么初始化FrameManagergetFrameManager().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

接着会调用这两句

addRepeatingRequestBuilderSurfaces();
applyRepeatingRequestBuilder(false, CameraException.REASON_FAILED_TO_START_PREVIEW);

addRepeatingRequestBuilderSurfaces会对mRepeatingRequestBuilder做一些配置,将预览的Surface添加到mRepeatingRequestBuilder中,mRepeatingRequestBuilderandroid.hardware.camera2.CaptureRequest.Builder类,是接下来调用Camera2中调用setRepeatingRequest必备的一个参数。

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,从而实现了预览的效果。

@EngineThread
private void applyRepeatingRequestBuilder(boolean checkStarted, int errorReason) {if ((getState() == CameraState.PREVIEW && !isChangingState()) || !checkStarted) {//这将不断地实时发送视频流,直到会话断开或调用session.stoprepeat()mSession.setRepeatingRequest(mRepeatingRequestBuilder.build(),mRepeatingRequestCallback, null);}
}

7. 小结

到这里我们就对于CameraView的预览流程有了大致的了解了,内部就是调用了Camera2API,关联SurfaceView进行预览。

  • 创建mCameraPreview,具体实现类是SurfaceCameraPreview,内部封装了SurfaceView
    • 并提供了SurfaceCallback接口用来回调onSurfaceAvailable()onSurfaceChanged()onSurfaceDestroyed方法
  • 调用Camera2Engine.setPreview(CameraPreview),就是Camera2Engine实现了CameraPreviewSurfaceCallback回调
  • 在回调的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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/171582.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode】挑战100天 Day17(热题+面试经典150题)

【LeetCode】挑战100天 Day17&#xff08;热题面试经典150题&#xff09; 一、LeetCode介绍二、LeetCode 热题 HOT 100-192.1 题目2.2 题解 三、面试经典 150 题-193.1 题目3.2 题解 一、LeetCode介绍 LeetCode是一个在线编程网站&#xff0c;提供各种算法和数据结构的题目&…

java stream流常用方法

filter(Predicate predicate)&#xff1a;根据指定条件过滤元素。 List<Integer> numbers Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> evenNumbers numbers.stream().filter(n -> n % 2 0).collect(Collectors.toList()); System.out.pr…

VS2010 VS2015环境编译boost库

VS2010下安装boost库 去www.boost.org下载最新的boost&#xff0c;我下载了boost_1_46_1.7z&#xff08;我放在D:/cpp目录下&#xff09;解压到当前文件夹打开VS2010->VS TOOLS->VS命令提示CD D:/cpp/boost_1_46_1输入bootstrap&#xff0c;便生成bjam.exe文件输入bjam …

[建议收藏] 一个网站集合所有最新最全的AI工具

今天给大家推荐一个宝藏的AI工具合集网站&#xff0c;有了这个网站&#xff0c;你们再也不用去其他地方找AI工具了。 名称&#xff1a;AI-BOT工具集 这个网站精选1000AI工具&#xff0c;并持续每天更新添加&#xff0c;包括AI写作、AI绘画、AI音视频处理、AI平面设计、AI自动编…

Atcoder Beginner Contest 330——A~F题

A - Counting Passes Description Problem Statement N N N people labeled 1 , 2 , … , N 1,2,\dots,N 1,2,…,N took an exam, and person i i i scored A i A_i Ai​ points. Only those who scored at least L L L points pass this exam. Determine how many peopl…

SpringBoot:邮件发送

官网文档&#xff1a;39. Sending Email (spring.io)。 Sending Email Spring框架提供了JavaMailSender实例&#xff0c;用于发送邮件。 如果SpringBoot项目中包含了相关的启动器&#xff0c;那么就会自动装配一个Bean实例到项目中。 在SpringBoot项目中引入如下Email启动器&a…

Add, Divide and Floor(cf round 158 div2)

题目&#xff1a;给你一个整数数组 a1,a2,…,an 。在一次操作中&#xff0c;你可以选择一个整数 x &#xff0c;并用 (a[i]x)/2 替换 ai ( (a[i]x)/2表示将 y(a[i]x)/2舍入为最接近的整数(下取整)。 ⌊y⌋ 表示将 y 舍入为最接近的整数&#xff09;来替换从 1 到 n 的所有 i。…

【数据分享】2019-2023年我国区县逐月新房房价数据(Excel/Shp格式)

房价是一个城市发展程度的重要体现&#xff0c;一个城市的房价越高通常代表这个城市越发达&#xff0c;对于人口的吸引力越大&#xff01;因此&#xff0c;房价数据是我们在各项城市研究中都非常常用的数据&#xff01;之前我们分享过2019-2023年我国地级市逐月房价数据&#x…

Spring Boot 项目中读取 YAML 文件中的数组、集合和 HashMap

在 Spring Boot 项目中&#xff0c;我们经常使用 YAML 文件来配置应用程序的属性。在这篇博客中&#xff0c;我将模拟如何在 Java 的 Spring Boot 项目中读取 YAML 文件中的数组、集合和 HashMap。 1. 介绍 YAML&#xff08;YAML Aint Markup Language&#xff09;是一种人类…

OpenMp并行编程

目录 介绍编译用法>OpenMp parallel>OpenMp for>OpenMp private、firstprivate、lastprivate>OpenMp section>OpenMp reduction>OpenMp single>OpenMp master>OpenMp barrier OpenMp的API函数 介绍 OpenMp是一种并行编程模型&#xff0c;旨在简化多线…

【Spring集成MyBatis】MyBatis注解开发

文章目录 1. MyBatis的常用注解2. 基于注解的MyBatis增删改查增删改查完整代码加载映射关系测试代码 3. MyBatis的注解实现复杂映射开发一对一操作的实现一对一操作实现的第二种方式一对多操作的实现多对多操作实现 1. MyBatis的常用注解 2. 基于注解的MyBatis增删改查 使用注…

Linux加强篇004-Vim编辑器与Shell命令脚本

目录 前言 1. Vim文本编辑器 1.1 编写简单文档 1.2 配置主机名称 1.3 配置网卡信息 1.4 配置软件仓库 2. 编写Shell脚本 2.1 编写简单的脚本 2.2 接收用户的参数 2.3 判断用户的参数 3. 流程控制语句 3.1 if条件测试语句 3.2 for条件循环语句 3.3 while条件循环语…

【开源】基于JAVA的高校学院网站

项目编号&#xff1a; S 020 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S020&#xff0c;文末获取源码。} 项目编号&#xff1a;S020&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 学院院系模块2.2 竞赛报名模块2.3 教…

Postman如何使用(三):使用数据文件

数据文件是非常强大的方式使用不同的测试数据来测试我们的API&#xff0c;以检查它们是否在各种情况下都能正常运行。我们可以认为数据文件是“Collection Runner”中每个请求的参数。下面&#xff0c;我们通过一个例子来说明如何使用数据文件。 这篇文章需要结合下面两个文件进…

史上最全前端知识点+高频面试题合集,十二大专题,命中率高达95%

前言&#xff1a; 下面分享一些关于阿里&#xff0c;美团&#xff0c;深信服等公司的面经&#xff0c;供大家参考一下。大家也可以去收集一些其他的面试题&#xff0c;可以通过面试题来看看自己有哪里不足。也可以了解自己想去的公司会问什么问题&#xff0c;进行有针对的复习。…

PowerShell基础

1. Tab键补全 有时候不记得指令全称&#xff0c;只记得开头几个字母&#xff0c;使用Tab键可显式建议选项&#xff0c;再次按Tab可以往后翻&#xff0c;ShiftTab可以往前翻。 2. 查看指令类型 Get-Command -Name Get-Alias 指令是遵循一定的格式规范的&#xff0c;如动词加名…

css之选择第一个或最后一个元素、第n个标签、选择偶数或奇数标签、选择最后n个标签、等差数列标签的选择、first、last、nth、child

MENU first-child选择列表中的第一个标签last-child选择列表中的最后一个标签nth-child(n)选择列表中的第n个标签nth-child(2n)选择列表中的偶数位标签nth-child(2n-1)选择列表中的奇数位标签nth-child(nm)选择从第m个到最后一个标签nth-child(-nm)选择从第1个到第m个nth-last-…

Python与设计模式--桥梁模式

11-Python与设计模式–桥梁模式 一、画笔与形状 在介绍原型模式的一节中&#xff0c;我们举了个图层的例子&#xff0c;这一小节内容&#xff0c;我们同样以类似画图的例子&#xff0c; 说明一种结构类设计模式&#xff1a;桥梁模式。 在一个画图程序中&#xff0c;常会见到这…

《数据结构与算法之美》读书笔记2

链表操作的技巧 1.理解指针 将摸个变量赋值给指针&#xff0c;实际上就是将这个变量的地址赋给指针&#xff0c;或者&#xff0c;指针中存储了这个变量的地址&#xff0c;指向了这个变量&#xff0c;所以可以通过指针找到这个变量。 2.内存泄漏或指针丢失 删除链表节点时&a…

人工智能|机器学习——循环神经网络的简洁实现

循环神经网络的简洁实现 如何使用深度学习框架的高级API提供的函数更有效地实现相同的语言模型。 我们仍然从读取时光机器数据集开始。 import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 t…