Android 基于Camera2 API进行摄像机图像预览

前言

近期博主准备编写一个基于Android Camera2的图像采集并编码为h.264的应用,准备分为三个阶段来完成,第一阶段实现Camera2的摄像机预览,第二阶段完成基于MediaCodec H.264编码,第三阶段完成基于MediaCodec H.264解码,针对不同阶段将输出对应的实现博文,因为笔者是第一次接触这块因此如果编写过程中有什么错误的话,欢迎大家指正,对于技术方面感兴趣的也欢迎私信或者留言,一起讨论共同进步。

Camera2 API简介

Android Camera2 API 是从 Android 5.0(Lollipop)开始引入的,用以取代旧的 Camera API。Camera2 提供了更强大和灵活的相机控制能力,允许开发者实现更多的相机功能,如手动对焦、手动曝光、原生 RAW 图像捕获等。

在开始使用Camera2之前,我们先来了解下Camera2使用过程中需要用到的相关类:

  • CameraManager:相机管理器,安卓系统针对不同的功能模块会创建不同的Manager,所以相机也不例外,一般主要用来open相机或者查询相机列表以及对应相机的参数等。

  • CameraDevice:代表一个物理相机设备,可以打开、配置和关闭相机设备等。

  • CameraCaptureSession:相机捕获会话,用于发送捕获请求和接收捕获结果,可以预览、拍照、录像等。

  • CameraCharacteristics:提供了相机设备的静态信息,如支持的参数、分辨率、对焦模式等。

整个执行流程大致上如下:

flowchart TB
    获取CameraManager对象 --> 通过CameraManager打开指定ID的相机 --> 打开后拿到到CameraDevice对象 --> 通过CameraDevice创建CameraCaptureSession对象 --> 通过CameraCaptureSession开启预览

现在已经简单的知晓了相关类的用途和流程,那么我们接下来开始实现Camera2的预览功能。

编码实现

为了方便后续对这块的复用,因此这里我们创建一个名为CameraWrapper的类,用以对Camera相关功能的封装,首先添加如下代码:

class CameraWrapper(private var context:Context) {

    private val TAG = "CameraWrapper"

    private var cameraManager: CameraManager
    private var cameraDevice: CameraDevice? = null
    private var characteristics: CameraCharacteristics? = null
    private var session: CameraCaptureSession? = null

    //当前设备的相机列表
    private var cameraIds:Array<String>

    //默认启用的cameraId
    private var cameraId:String = "0"//默认前置

    private var previewView:SurfaceView? = null

    private  var encoderSurface: Surface? = null

    private var cameraThread:HandlerThread = HandlerThread("CameraThread")
    private var cameraHandler: Handler

    private var previewSize:Size? = Size(1280,720)
    
    init {
        cameraThread.start()
        cameraHandler = Handler(cameraThread.looper)
        cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
        cameraIds = getCameraIds()

        //看下id和前后置的对应关系
        for(id in cameraIds){
            Log.d(TAG,"id : "+id+"---"+getCameraOrientationString(id))
        }

         useCamera(cameraId)
    }

这里我们定义了所有需要用到的成员变量,这里我挑几个说明下,其他的就不过多说明了,看名字应该都能理解。

通过previewView应该不难看出我们使用的预览View为SurfaceView。

encoderSurface这个预留给后续摄像机编码H.264时使用,本篇文章暂未用到。

cameraThread和cameraHandler这里可以不用过多关注,创建的意义主要是传参给Camera,使用是在Camera内。

previewSize可以理解为预览的画面分辨率,默认设置的是720P。

该类类创建时,获取到了设备支持的摄像机列表并打印了前后置的对应关系,这里主要是调试观察的。getCameraIds()和 useCamera(cameraId)我们还没实现,但是先不急,让我们先加两个权限相关的函数。

   //添加权限请求和检查接口,方便使用
    companion object{
         fun requestCameraPermissions(activity: Activity){
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(
                    Manifest.permission.CAMERA,
                ),
                998
            )
        }

        fun checkCameraPermission(context: Context):Boolean{
            return ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
        }
    }

这两个函数作用就不解释了,核心作用就是为了方便使用CameraWrapper时对相关权限进行检查和请求。

 fun useCamera(id:String){
        cameraId = id
        try {
            characteristics = cameraManager.getCameraCharacteristics(cameraId)
        } catch (e: CameraAccessException) {
            throw RuntimeException(e)
        }
    }

    fun useBackCamera(){
        cameraId = getSwitchCameraId(CameraCharacteristics.LENS_FACING_BACK)
        useCamera(cameraId)
    }

    fun useFrontCamera(){
        cameraId = getSwitchCameraId(CameraCharacteristics.LENS_FACING_FRONT)
        useCamera(cameraId)
    }

    fun useExternalCamera(){
        cameraId = getSwitchCameraId(CameraCharacteristics.LENS_FACING_EXTERNAL)
        useCamera(cameraId)
    }
    
    private fun getCameraIds():Array<String>{
        try {
            return cameraManager.cameraIdList
        } catch (e:CameraAccessException) {
            throw RuntimeException(e)
        }
    }
    
    private fun getSwitchCameraId(switch:Int):String{
        if(cameraIds==null|| cameraIds.isEmpty()) return "-1"
        cameraIds.forEach {
            var characteristics:CameraCharacteristics?  = null
            try {
                characteristics = cameraManager.getCameraCharacteristics(cameraId);
            } catch (e:CameraAccessException) {
                throw RuntimeException(e)
            }
            var lensFacing  = characteristics.get(CameraCharacteristics.LENS_FACING);
            if(lensFacing == switch){
                return it
            }
        }
        return cameraIds[0]
    }

useCamera就是我们上面初始化中使用相机的函数,这里还有多个use*Camera函数,可以快速切换到对应的相机,这里给出了三种快速选择前置、后置和外置的相机的函数,基本上应该可以满足使用需要了。

getCameraIds()获取摄像机列表也很简单,实际上就cameraManager.cameraIdList这一行代码。

getSwitchCameraId()这是根据摄像机类型获取摄像机机对应的Id,如果摄像机列表为空直接返回摄像机Id为“-1”,如果摄像机类型不存在会直接选择列表中的第一个摄像机Id返回。

    fun startPreview(surfaceView: SurfaceView){
        if(previewView!=null){
            stopPreview()
        }
        previewView = surfaceView
        previewView?.let {
            it.holder.addCallback(previewCallback)
            if(it.holder.surface !=null&&it.holder.surface.isValid){
                openCamera(cameraId)
            }
        }
    }
    
       private var previewCallback:SurfaceHolder.Callback  = object:SurfaceHolder.Callback{
        @Override
        override fun surfaceCreated(surfaceHolder:SurfaceHolder) {
            openCamera(cameraId)
        }

        @Override
        override fun surfaceChanged(surfaceHolder:SurfaceHolder , i:Int , i1:Int , i2:Int) {
        }

        @Override
        override fun surfaceDestroyed(surfaceHolder:SurfaceHolder) {
            stopPreview()
        }
    }

接下来这里就开始准备启动预览,首先给将传进来的surfaceView赋值给全局变量previewView,给previewView设置了监听器,主要用来监听Surface的创建,改变和销毁等状态用以控制摄像机的启动和停止预览,如果当前设置的surfaceView已经创建了surfacen那么就可以直接通过openCamera(cameraId)打开摄像机了。这里之所以这么实现,是为了使得开启预览的逻辑闭环,例如设置的SurfaceView还没有创建Surface那么通过监听器启动预览,如果设置的SurfaceView已经创建了Surface那么就直接开启预览。

    private fun openCamera(cameraId:String) {
        try {
            var map:StreamConfigurationMap? = characteristics?.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
            previewSize = getBestSupportedSize(map!!.getOutputSizes(SurfaceTexture::class.java).toMutableList())
            cameraManager.openCamera(cameraId,object:CameraDevice.StateCallback(){
                override fun onOpened(p0: CameraDevice) {
                    cameraDevice = p0
                    try {
                        startPreview()
                    } catch (e:CameraAccessException ) {
                        throw RuntimeException(e)
                    }
                }

                override fun onDisconnected(p0: CameraDevice) {
                    cameraDevice?.close()
                }

                override fun onError(p0: CameraDevice, p1: Int) {
                    Log.e(TAG, "openCamera Failed:$p1");
                    cameraDevice?.close();
                    cameraDevice = null;
                }

            },cameraHandler)
        } catch (e:CameraAccessException) {
            throw RuntimeException(e)
        }
    }
    
    
private fun getBestSupportedSize(sizs:List<Size>):Size {
        var maxPreviewSize: Point = Point(previewSize!!.width, previewSize!!.height)
        var minPreviewSize:Point =  Point(1280, 720)
        var defaultSize:Size  = sizs[0]
        var tempSizes:Array<Size>  = sizs.toTypedArray()
        Arrays.sort(tempSizes, object:Comparator<Size> {
            override fun compare(o1:Size , o2:Size):Int {
                return if (o1.width > o2.width) {
                     -1
                } else if (o1.width == o2.width) {
                    if(o1.height > o2.height){
                         -1
                    }else{
                          1
                    }
                } else {
                     1
                }
            }
        })

    var sizes:MutableList<Size> =  tempSizes.toMutableList()
        for(s in tempSizes){
            if (maxPreviewSize != null) {
                if (s.width > maxPreviewSize.x || s.height > maxPreviewSize.y) {
                    sizes.remove(s)
                    continue
                }
            }
            if (minPreviewSize != null) {
                if (s.width < minPreviewSize.x || s.height < minPreviewSize.y) {
                    sizes.remove(s)
                }
            }
        }

        if (sizes.size == 0) {
            return defaultSize
        }
        var bestSize = sizes[0]
        var previewViewRatio:Float = if (previewSize != null) {
            previewSize!!.width.toFloat() /  previewSize!!.height.toFloat()
        } else {
            bestSize.width.toFloat() /  bestSize.height.toFloat()
        }

        if (previewViewRatio > 1) {
            previewViewRatio = 1 / previewViewRatio
        }

        for ( s in sizes) {
            if (abs((s.height.toFloat() / s.width.toFloat()) - previewViewRatio) < abs(bestSize.height.toFloat() / bestSize.width.toFloat() - previewViewRatio)) {
                bestSize = s
            }
        }
        return bestSize
    }

这里需要注意下,预览分辨率不能随便设置,需要通过摄像机配置选择所支持的预览尺寸,因此这块在实际开启预览之前这里先根据设置的预览尺寸在摄像机中查找与之对应的预览分辨率。

getBestSupportedSize()这个函数是我在网上找到(懒),实际开发时这个匹配策略可能未必能适用所有需求,我简单对这个函数说明下,函数顶部几行定义了最大和最小以及默认的分辨率,最大的分辨率等于设置进来的预览尺寸,最小的这里设置的是720P,默认的为摄像机支持列表中第一个。

接下来那一坨,主要作用就是排序,对摄像机支持分辨率先根据宽度进行降序,如果宽度一致再根据高度降序排序。

接着对排序的列表进行循环,把超出需要的最大和最小的分辨率去除掉,如果全部去掉了就返回默认尺寸。

接下来又定义了一个最优的尺寸,又计算了宽高比,如果计算出的宽高比大于 1,则将其取倒数。这是因为宽高比通常表示为宽度除以高度,而如果宽度大于高度,宽高比会大于 1,但为了与标准宽高比格式保持一致,这里取其倒数。

最后计算其宽高比与预览视图宽高比之间的绝对差值,如果这个差值小于当前 bestSize 的宽高比与预览视图宽高比之间的绝对差值,则将 s 设置为新的 bestSize。

让我们回到正轨,继续openCamera()函数,cameraManager.openCamera打开指定Id的摄像机,如果摄像机打开成功会在回调中通知,成功后保存CameraDevice,并开始执行startPreview()。

    private fun startPreview() {
        //因为摄像头设备可以同时输出多个流,所以可以传入多个surface
        var targets = ArrayList<Surface>()
        /*,这里可以传入多个surface*/
        targets.add(previewView!!.holder.surface)
        if(encoderSurface!=null){
            targets.add(encoderSurface!!)
        }
        cameraDevice?.createCaptureSession(targets, object:CameraCaptureSession.StateCallback(){
            override fun onConfigured(p0: CameraCaptureSession) {
                session = p0
                try {
                    var captureRequest =
                        cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                    captureRequest?.addTarget(previewView!!.holder.surface)
                    if (encoderSurface != null) {
                        captureRequest?.addTarget(encoderSurface!!)
                    }
                    captureRequest?.set(CaptureRequest.SCALER_CROP_REGION, Rect(0,0,previewSize!!.width,previewSize!!.height))
                    //这将不断地实时发送视频流,直到会话断开或调用session.stoprepeat()
                    session?.setRepeatingRequest(captureRequest?.build()!!, null, cameraHandler);
                } catch (e:CameraAccessException) {
                    throw RuntimeException(e)
                }
            }

            override fun onConfigureFailed(p0: CameraCaptureSession) {
                Log.e(TAG,"session configuration failed")
            }

        },cameraHandler)
    }

这块首先将预览surface和编码输入的surface(如果有)统一保存到一个List中。接着通过我们打开的CameraDevice 传入刚才创建的List和回调作为参数用以创建一个CameraCaptureSession,创建成功后会通过回调通知我们。在成功的回调中保存了CameraCaptureSession到全局变量session中,后面需要通过这个session来启动或者停止预览。

接下来就是最后的预览启动逻辑了,首先创建了一个基于CameraDevice.TEMPLATE_PREVIEW的名为captureRequest的CaptureRequest.Builder,这里又再一次将Surface传入到了captureRequest中,具体原因没深究,不过查下来的所有资料貌似都是这么搞的,最后通过session发送一个重复的捕获请求,这步执行完之后就可以在SurfaceView看到我们的预览画面了。

现在启动预览已经完成,接着让我们再添加一个停止预览函数,有启动就必定有停止。

    fun stopPreview(){
        try {
            session?.stopRepeating()
            cameraDevice?.close()
            session = null
            cameraDevice = null
        } catch (e:CameraAccessException) {
            throw RuntimeException(e)
        }
    }

停止就很简单了,先停止了循环采集请求,接着关闭了摄像机。

因为我们代码中使用了HandlerThread如果不释放会有内存泄露风险,因此让我们在不需要预览的时候将其释放掉。

    fun release(){
        stopPreview()
        cameraThread.quit()
    }

至此我们Camera2预览功能就已经编写完成,那么貌似还好了些什么,当然少了权限添加,快点在AndroidManifest.xml中加上权限。

    <uses-permission android:name="android.permission.CAMERA" />

总结

使用 Android Camera2 API 实现相机预览涉及权限申请、获取 CameraManager 实例、打开相机设备、获取相机特性、选择合适的预览尺寸、创建预览 Surface、建立预览会话、配置并发送预览请求等一系列步骤,同时需要在应用的生命周期内妥善管理相机资源,以确保预览功能的正确实现和相机硬件的安全使用。虽然当前这个代码还比较粗糙,但是相信我们后面会将其优化的更加完美。

最后,有需要完整源码的同学请在有用的代码片段专栏中观看。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT 线程 QThread QT5.12.3环境 C++实现

一、线程 QT主线程称为GUI线程&#xff0c;负责初始化界面并监听事件循环&#xff0c;并根据事件处理做出界面上的反馈。如果把一些比较复杂或者费时的操作放在主线程中&#xff0c;界面就会出现卡顿或者无响应的现象。一般主线程负责影响界面上的操作&#xff0c; 子线程负责负…

【LLM】一文学会SPPO

博客昵称&#xff1a;沈小农学编程 作者简介&#xff1a;一名在读硕士&#xff0c;定期更新相关算法面试题&#xff0c;欢迎关注小弟&#xff01; PS&#xff1a;哈喽&#xff01;各位CSDN的uu们&#xff0c;我是你的小弟沈小农&#xff0c;希望我的文章能帮助到你。欢迎大家在…

Vue3-后台管理系统

目录 一、完成项目历程 1、构建项目 2、项目的自定义选项 3、 封装组件 4、配置对应页面的路由 5、从后端调接口的方式 二、引入Element Plus、Echarts、国际化组件 1、Element Plus安装 2、Echarts安装 3、国际化 三、介绍项目以及展示 1、项目是基于Vue3、Element …

C0030.Clion中运行提示Process finished with exit code -1073741515 (0xC0000135)解决办法

1.错误提示 2.解决办法 添加环境变量完成之后&#xff0c;重启Clion软件&#xff0c;然后就可以正常调用由mingw编译的opencv库了。

【es6进阶】vue3中的数据劫持的最新实现方案的proxy的详解

vuejs中实现数据的劫持,v2中使用的是Object.defineProperty()来实现的&#xff0c;在大版本v3中彻底重写了这部分&#xff0c;使用了proxy这个数据代理的方式&#xff0c;来修复了v2中对数组和对象的劫持的遗留问题。 proxy是什么 Proxy 用于修改某些操作的默认行为&#xff0…

Python浪漫之画明亮的月亮

目录 1、效果展示 2、完整版代码 1、效果展示 2、完整版代码 import turtledef draw_moon():# 设置画布turtle.bgcolor("black") # 背景颜色为黑色turtle.speed(10) # 设置绘制速度# 绘制月亮的外圈turtle.penup()turtle.goto(0, -100) # 移动到起始…

《线性代数的本质》

之前收藏的一门课&#xff0c;刚好期末复习&#xff0c;顺便看一看哈哈 课程链接&#xff1a;【线性代数的本质】合集-转载于3Blue1Brown官方双语】 向量究竟是什么 线性代数中最基础、最根源的组成部分就是向量&#xff0c;需要先明白什么是向量 不同专业对向量的看法 物理专…

鸿蒙系统ubuntu开发环境搭建

在RISC-V等平台移植鸿蒙系统OpenHarmony&#xff0c;需要使用linux环境进行代码的编译&#xff0c;为兼顾日常办公需要&#xff0c;可采用WindowsUbuntu虚拟机的混合开发的环境&#xff0c;通过网络及文件夹共享&#xff0c;在主机和虚拟机之间共享文件数据。 工具准备&#x…

智能停车解决方案之停车场室内导航系统(二):核心技术与系统架构构建

hello~这里是维小帮&#xff0c;如有项目需求和技术交流欢迎大家私聊我们&#xff01;点击文章最下方获取智慧停车场方案~撒花&#xff01; 随着城市化进程的加速&#xff0c;停车难问题日益凸显。智能停车系统作为缓解停车压力的有效手段&#xff0c;其核心技术与架构的构建至…

(免费送源码)计算机毕业设计原创定制:Java+JSP+HTML+JQUERY+AJAX+MySQL springboot计算机类专业考研学习网站管理系统

摘 要 大数据时代下&#xff0c;数据呈爆炸式地增长。为了迎合信息化时代的潮流和信息化安全的要求&#xff0c;利用互联网服务于其他行业&#xff0c;促进生产&#xff0c;已经是成为一种势不可挡的趋势。在大学生在线计算机类专业考研学习网站管理的要求下&#xff0c;开发一…

IDEA2023版本中如何启动项目的多个实例

假设现在要启动多个服务&#xff0c;例如简单的客户端和服务端&#xff0c;默认的idea是只能启动一个的&#xff0c;那么我们需要进行配置允许多个项目的同时启动&#xff0c;现在进行多实例的配置。 第一步 点击Edit Configurations 第二步 点击Modify options 第三步 勾选…

图的邻接矩阵和邻接表存储

目录 邻接矩阵存储法 简介 ​编辑 邻接矩阵举例 无向图邻接矩阵 有向图邻接矩阵 当各条边带有权值时 邻接矩阵算法实现 结构体定义和函数声明 函数的实现 邻接表存储法 简介 邻接表的算法实现 结构体定义和函数声明 函数的实现 邻接矩阵和邻接表的差别 邻接矩阵存…

【Linux命令】grep

Linux命令-grep GREP命令&#xff1a;进行字符串数据的比对&#xff0c;并将符合指定模式的字符串行打印出来。1.命令介绍基础正则表达式原始文档如下&#xff1a; 2.练习题&#xff1a;2.1 练习&#xff08;一&#xff09;&#xff1a;2.1.1 读取加行号的文件内容&#xff1a;…

WMS 如何实现智能仓储与自动化物流的无缝对接

【大家好&#xff0c;我是唐Sun&#xff0c;唐Sun的唐&#xff0c;唐Sun的Sun。】 在当今高度竞争的商业环境中&#xff0c;企业对于物流效率和仓储管理的要求日益严苛。智能仓储和自动化物流作为现代物流领域的重要发展方向&#xff0c;能够显著提高物流运作的速度、准确性和成…

DevOps-Jenkins-新手入门级

1. Jenkins概述 1. Jenkins是一个开源持续集成的工具&#xff0c;是由JAVA开发而成 2. Jenkins是一个调度平台&#xff0c;本身不处理任何事情&#xff0c;调用插件来完成所有的工作 1.1 什么是代码部署 代码发布/部署>开发书写的程序代码---->部署测试/生产环境 web服务…

WEB APIS(DOM对象,操作元素内容,属性,表单属性,自定义属性,定时器)

js基础基本语法&#xff1a; 变量&#xff0c;数据类型&#xff0c;循环&#xff0c;函数&#xff0c;对象等(主要是控制台打印&#xff09; WEB APIS 操作DOM BOM &#xff1a; 控制网页元素&#xff0c;交互等各种网页交互效果 js高级 语法&#xff1a; js新增语法&#xff0…

cs144(一)

cs144(一) 1、osi 当应用程序有数据要发送时&#xff0c;应用层将数据交给传输层&#xff0c; 传输层负责将数据可靠或不可靠地传送到另外一端&#xff0c;传输层通过将数据交给网络层来发送数据 网络层负责将数据分成数据包&#xff0c;每个数据包都有正确的目的地址 最后…

IEC61850读服务器目录命令——GetServerDirectory介绍

IEC61850标准中的GetServerDirectory命令是变电站自动化系统中非常重要的一个功能&#xff0c;它主要用于读取服务器的目录信息&#xff0c;特别是服务器的逻辑设备节点&#xff08;LDevice&#xff09;信息。以下是对GetServerDirectory命令的详细介绍。 目录 一、命令功能 …

如何使用AWS Lambda构建一个云端工具(超详细)

首发地址&#xff08;欢迎大家访问&#xff09;&#xff1a;如何使用AWS Lambda构建一个云端工具&#xff08;超详细&#xff09; 1 前言 1.1 无服务器架构 无服务器架构&#xff08;Serverless Computing&#xff09;是一种云计算服务模型&#xff0c;它允许开发者构建和运行…

力扣-位运算-1【算法学习day.41】

前言 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&am…