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

news2024/11/28 3:52:43

前言

近期博主准备编写一个基于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、建立预览会话、配置并发送预览请求等一系列步骤,同时需要在应用的生命周期内妥善管理相机资源,以确保预览功能的正确实现和相机硬件的安全使用。虽然当前这个代码还比较粗糙,但是相信我们后面会将其优化的更加完美。

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

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

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

相关文章

设计模式:11、迭代器模式(游标)

目录 0、定义 1、迭代器模式的四种角色 2、迭代器模式的UML类图 3、示例代码 4、迭代器的next()方法与集合的get(int index)方法的效率对比&#xff08;LinkedList为例&#xff09; 0、定义 提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;而又不需要暴露该对象…

UE5连接VR(pico,quest)进行PC VR开发(没有废话全是干货)

一、PICO VR连接UE 首先picoVR&#xff0c;不管是pico neo3还是pico4&#xff0c;用到的软件就只有三个 分别是pico互联助手PICO 互联 | PICO (picoxr.com)、steam VR&#xff0c;虚幻引擎5 pico互联助手 在pico互联助手中你需要选择两种连接方式&#xff08;推荐USB连接&a…

《UnityShader 入门精要》更复杂的光照

代码&示例图见&#xff1a;zaizai77/Shader-Learn: 实现一些书里讲到的shader 到了这里就开启了书里的中级篇&#xff0c;之后会讲解 Unity 中的渲染路径&#xff0c;如何计算光照衰减和阴影&#xff0c;如何使用高级纹理和动画等一系列进阶内容 Unity 中的渲染路径 在U…

用nextjs开发时遇到的问题

这几天已经基本把node后端的接口全部写完了&#xff0c;在前端开发时考虑时博客视频类型&#xff0c;考虑了ssr&#xff0c;于是选用了nextJs&#xff0c;用的是nextUi,tailwincss,目前碰到两个比较难受的事情。 1.nextUI个别组件无法在服务器段渲染 目前简单的解决方法&…

Golang项目:实现一个内存缓存系统

要求 支持设定过期时间&#xff0c;精确到秒支持设定最大内存&#xff0c;当内存超过时做出合适的处理支持并发安全按照以下接口安全 type Cache interface{//size : 1KB 100KB 1MB 2MB 1GBSetMaxMemory(size string )bool//将value写入缓存Set(key string, val interface{},e…

Softing线上研讨会 | Ethernet-APL:推动数字时代的过程自动化

| &#xff08;免费&#xff09;线上研讨会时间&#xff1a;2024年11月19日 16:00~16:30 / 23:00~23:30 Ethernet-APL以10Mb/s的传输速率为过程工业中的现场设备带来了无缝以太网连接和本质安全电源&#xff0c;这不仅革新了新建工厂&#xff0c;也适用于改造现有工厂。 与现…

《Deep Multimodal Learning with Missing Modality: A Survey》中文校对版

文章汉化系列目录 文章目录 文章汉化系列目录摘要1 引言2 方法论分类&#xff1a;概述2.1 数据处理方面2.2 策略设计方面 3 数据处理方面的方法3.1 模态填充3.1.1 模态组合方法3.1.2 模态生成方法 3.2 面向表示的模型3.2.1 协调表示方法3.2.2 表示组合方法。3.2.3 表示生成方法…

python爬虫案例——猫眼电影数据抓取之字体解密,多套字体文件解密方法(20)

文章目录 1、任务目标2、网站分析3、代码编写1、任务目标 目标网站:猫眼电影(https://www.maoyan.com/films?showType=2) 要求:抓取该网站下,所有即将上映电影的预约人数,保证能够获取到实时更新的内容;如下: 2、网站分析 进入目标网站,打开开发者模式,经过分析,我…

鸿蒙安全控件之位置控件简介

位置控件使用直观且易懂的通用标识&#xff0c;让用户明确地知道这是一个获取位置信息的按钮。这满足了授权场景需要匹配用户真实意图的需求。只有当用户主观愿意&#xff0c;并且明确了解使用场景后点击位置控件&#xff0c;应用才会获得临时的授权&#xff0c;获取位置信息并…

MATLAB矩阵元素的修改及删除

利用等号赋值来进行修改 A ( m , n ) c A(m,n)c A(m,n)c将将矩阵第 m m m行第 n n n列的元素改为 c c c&#xff0c;如果 m m m或 n n n超出原来的行或列&#xff0c;则会自动补充行或列&#xff0c;目标元素改为要求的&#xff0c;其余为 0 0 0 A ( m ) c A(m)c A(m)c将索引…

网络安全之内网安全

下面给出了应对企业内网安全挑战的10种策略。这10种策略即是内网的防御策略&#xff0c;同时也是一个提高大型企业网络安全的策略。 1、注意内网安全与网络边界安全的不同 内网安全的威胁不同于网络边界的威胁。网络边界安全技术防范来自Internet上的攻击&#xff0c;主要是防…

Python 爬虫入门教程:从零构建你的第一个网络爬虫

网络爬虫是一种自动化程序&#xff0c;用于从网站抓取数据。Python 凭借其丰富的库和简单的语法&#xff0c;是构建网络爬虫的理想语言。本文将带你从零开始学习 Python 爬虫的基本知识&#xff0c;并实现一个简单的爬虫项目。 1. 什么是网络爬虫&#xff1f; 网络爬虫&#x…

solr 远程命令执行 (CVE-2019-17558)

目录 漏洞描述 执行漏洞py脚本&#xff0c;取得shell连接 EXP 漏洞描述 Apache Velocity是一个基于Java的模板引擎&#xff0c;它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目&#xff0c;旨在确保Web应用程序在表示层和业…

数据库中的视图

数据库中的视图 什么是视图创建视图使⽤视图修改数据注意事项 删除视图视图的优点 什么是视图 视图是⼀个虚拟的表&#xff0c;它是基于⼀个或多个基本表或其他视图的查询结果集。视图本⾝不存储数 据&#xff0c;⽽是通过执⾏查询来动态⽣成数据。⽤户可以像操作普通表⼀样使…

爬虫实战:采集知乎XXX话题数据

目录 反爬虫的本意和其带来的挑战目标实战开发准备代码开发发现问题1. 发现问题[01]2. 发现问题[02] 解决问题1. 解决问题[01]2. 解决问题[02] 最终结果 结语 反爬虫的本意和其带来的挑战 在这个数字化时代社交媒体已经成为人们表达观点的重要渠道&#xff0c;对企业来说&…

springboot-vue excel上传导出

数据库 device_manage表 字段&#xff0c;id&#xff0c;workshop,device_number,device_name,device_model,warn_time,expired_time device_warn表 字段&#xff0c;id,warn_time,expired_time 后端 实体类格式 device_manage Data TableName("device_manage"…

【简单好抄保姆级教学】javascript调用本地exe程序(谷歌,edge,百度,主流浏览器都可以使用....)

javascript调用本地exe程序 详细操作步骤结果 详细操作步骤 在本地创建一个txt文件依次输入 1.指明所使用注册表编程器版本 Windows Registry Editor Version 5.00这是脚本的第一行&#xff0c;指明了所使用的注册表编辑器版本。这是必需的&#xff0c;以确保脚本能够被正确解…

Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)

目录 一、Zset有序集合类型介绍 二、常见命令 1、ZADD 2、ZCARD 3、ZCOUNT 4、ZRANGE 5、ZREVRANGE 6、ZRANGEBYSCORE 7、ZREVRANGEBYSCORE 8、ZPOPMAX 9、ZPOPMIN 10、ZRANK 11、ZREVRANK 12、ZSCORE 13、ZREM 14、ZREMRANGEBYRANK 15、ZREMRANGEBYSCORE 16…

设计模式之 责任链模式

责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式&#xff0c;旨在将多个处理对象通过链式结构连接起来&#xff0c;形成一条处理请求的链条。每个处理对象都有机会处理请求&#xff0c;或者将请求传递给链中的下一个对象。这样&#x…

新版布谷直播软件源码开发搭建功能更新明细

即将步入2025年也就是山东布谷科技专注直播系统开发,直播软件源码出售开发搭建等业务第9年,山东布谷科技不断更新直播软件功能&#xff0c;以适应当前新市场环境下的新要求。山东布谷科技始终秉承初心&#xff0c;做一款符合广大客户需求的直播系统软件。支持广大客户提交更多个…