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

news2024/11/27 4:28:56

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.CameraView
    android: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> {
	//...省略了部分代码...

    @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回调有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>() {
         @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 )

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() {
    @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尺寸。

 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,那么初始化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

接着会调用这两句

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.coloradmin.cn/o/1254821.html

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

相关文章

Mybaits-plus的使用

MybatisPlus特性 润物无声&#xff1a; 只做增强不做改变&#xff0c;引入它不会对现有工程产生改变&#xff0c;如丝般顺滑。 效率至上 只需简单配置&#xff0c;即可快速进行单表CRUD操作&#xff0c;从而节省大量时间。 使用MybatisPlus依赖基本步骤 引入MybatisPlus依…

歌曲《难不难》由歌手荆涛演唱:面对挑战,勇敢前行

在人生的旅途中&#xff0c;我们都会遭遇种种困难和挑战。有时&#xff0c;一个看似简单的创意或想法&#xff0c;想要实现它却需要经历无数次的实践和辛酸。歌曲《难不难》由歌手荆涛演唱&#xff0c;以平实的语言和流畅的旋律&#xff0c;表达了面对困难和挑战时&#xff0c;…

使用360浏览器插件刷新网页

使用360浏览器插件刷新网页 1.打开360浏览器->扩展程序->更多扩展。 2.扩展中心->搜索”网页自动刷新”&#xff0c;然后安装。 3.在要学习的网页上&#xff0c;扩展程序中使用页面自动刷新插件。 4.如果页面打开慢就把10改大&#xff0c;比如改成15&#xff0…

新版idea如何开启多台JVM虚拟机

1.看看自己的项目 2.可能开始的时候啥也没有&#xff0c;就点Run Configuration Type 3.再点击Edit Configurations... 4.点击号添加SpringBoot 5.主类选择一下&#xff0c;一般就一个&#xff0c;点他选了就行。 6.然后点击Modify Options 选择添加add VM Options 7.点击appl…

Huggingface 超详细介绍

Hugging face 起初是一家总部位于纽约的聊天机器人初创服务商&#xff0c;他们本来打算创业做聊天机器人&#xff0c;然后在github上开源了一个Transformers库&#xff0c;虽然聊天机器人业务没搞起来&#xff0c;但是他们的这个库在机器学习社区迅速大火起来。目前已经共享了超…

windows的bat文件(学习笔记)

简介 通过windows的cmd执行的批处理&#xff0c;扩展名可以是.bat或.cmd&#xff08;类似linux的shell脚本&#xff09; 所有语句符号不区分大小写 帮助提示信息&#xff1a;命令 /? 1 基本语法 (1) 注释&#xff1a;rem 注释文本不执行 (2) 关闭盘符输出&#xff1a;e…

【软件测试】“我“做了一年的功能点点点测试,感觉在浪费时间...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 发现人们对测试非…

4/5G语音实现方案

今天又是学习充实的一天&#xff0c;今天我们来学习4G和5G语音实现方案的介绍&#xff0c;VOLITE通信流程是怎么样子的&#xff0c;和之前学的TCP协议有什么联系吗&#xff1f;今天我们换个角度来学习通信的流程~ 目录 2G/3G的电话和上网 4G语音实现方案 4G语音的三种方式 …

“BMP转PNG一键转换,批量处理图片,迈入高效图片管理新时代“

你是否曾经为了转换图片格式而烦恼&#xff1f;是否曾经因为一张一张地手动转换而感到无奈&#xff1f;现在&#xff0c;我们的全新工具将为你解决这些问题&#xff0c;开启高效图片管理新时代&#xff01; 首先&#xff0c;我们进入首助编辑高手主页面&#xff0c;会看到有多种…

1、nmap常用命令

文章目录 1. 主机存活探测2. 常见端口扫描、服务版本探测、服务器版本识别3. 全端口&#xff08;TCP/UDP&#xff09;扫描4. 最详细的端口扫描5. 三种TCP扫描方式&#xff08;1&#xff09;TCP connect 扫描&#xff08;2&#xff09;TCP SYN扫描&#xff08;3&#xff09;TCP …

Python自动化测试学习路线【进阶必看】

软件自动化测试的学习步骤 大概步骤如下&#xff1a; 1. 做好手工测试&#xff08;了解各种测试的知识&#xff09;-> 2. 学习编程语言-> 3. 学习Web基础&#xff08;HTML,HTTP,CSS,DOM,Javascript&#xff09;或者 学习Winform -> 4. 学习自动化测试工具 ->5.…

老师组织课外活动的好处有哪些

亲爱的小伙伴们&#xff0c;不知道你们有没有注意到&#xff0c;老师除了在课堂上教学之外&#xff0c;还会在课外组织各种各样的活动呢&#xff1f;这些活动不仅好玩&#xff0c;而且对我们有很多好处哦&#xff01;今天我就来给大家分享一下老师组织课外活动的好处吧&#xf…

目录树自动生成器 golang+fyne

go tree 代码实现请看 gitee 仓库链接 有很多生成目录树的工具&#xff0c;比如windows自带的tree命令&#xff0c;nodejs的treer&#xff0c;tree-cli等等。这些工具都很成熟、很好用&#xff0c;有较完善的功能。 但是&#xff0c;这些工具全部是命令式的&#xff0c;如果…

Java中wait()方法在synchronized方法中调用的奥秘

作为一名Java程序员&#xff0c;我们深知synchronized关键字和wait()方法在多线程编程中的重要性。 在本文中&#xff0c;我们将探讨为什么wait()方法需要在synchronized方法中调用&#xff0c;以及它们是如何协同工作的。 首先&#xff0c;让我们了解一下synchronized关键字和…

嵌入式硬件电路·电平

目录 1. 电平的概念 1.1 高电平 1.2 低电平 2. 电平的使用场景 2.1 高电平使能 2.2 低电平使能 2.3 失能 1. 电平的概念 电平是指电信号电压的大小或高低状态。在数字电子学中&#xff0c;电平有两种状态&#xff0c;高电平和低电平&#xff0c;用来表示二进制中…

代码随想录算法训练营第四十六天|139.单词拆分、背包问题总结

LeetCode 139. 单词拆分 题目链接&#xff1a;139. 单词拆分 - 力扣&#xff08;LeetCode&#xff09; 这道题使用完全背包来实现&#xff0c;我们首先考虑字符串是否可以由字符串列表组成&#xff0c;因此dp数组大小为n 1 &#xff0c;其意义是&#xff0c;在n个位置时是否能…

前缀和+哈希表——525. 连续数组

文章目录 ⛏1. 题目&#x1f5e1;2. 算法原理⚔解法一&#xff1a;暴力枚举⚔解法二&#xff1a;前缀和哈希表 ⚒3. 代码实现 ⛏1. 题目 题目链接&#xff1a;525. 连续数组 - 力扣&#xff08;LeetCode&#xff09; 给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最…

超全整理,银行测试-银行项目贷款业务详细,一篇概全...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 银行测试&#xf…

cuda magma 构建 使用cmake构建的步骤记录

这不是群论代数软件&#xff0c;而是cuda 矩阵计算软件 1. 生成其他精度的源代码 1.1 复制编辑 make.inc cp make.inc-examples/make.inc.openblas ./make.inc 并修改其中的定义&#xff1a; OPENBLASDIR ? /opt/OpenBLAS 这需要实现安装openblas到此处。文件夹解构&…

Linux 网络通信

(一)套接字Socket概念 Socket 中文意思是“插座”&#xff0c;在 Linux 环境下&#xff0c;用于表示进程 x 间网络通信的特殊文件 类型。本质为内核借助缓冲区形成的伪文件。 既然是文件&#xff0c;那么理所当然的&#xff0c;我们可以使用文件描述符引用套接字。Linux 系统…