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相关功能的封装,首先添加如下代码:

kotlin 复制代码
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)我们还没实现,但是先不急,让我们先加两个权限相关的函数。

kotlin 复制代码
   //添加权限请求和检查接口,方便使用
    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时对相关权限进行检查和请求。

kotlin 复制代码
 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返回。

kotlin 复制代码
    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那么就直接开启预览。

kotlin 复制代码
    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()。

kotlin 复制代码
    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看到我们的预览画面了。

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

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

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

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

kotlin 复制代码
    fun release(){
        stopPreview()
        cameraThread.quit()
    }

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

xml 复制代码
    <uses-permission android:name="android.permission.CAMERA" />

总结

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

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

相关推荐
吾即是光2 小时前
[SWPUCTF 2021 新生赛]error
android
CodeIsCoding2 小时前
鱼眼相机模型-MEI
人工智能·opencv·计算机视觉·相机
MYBOYER2 小时前
Kotlin DSL Gradle 指南
android·开发语言·kotlin
Mr_Xuhhh3 小时前
程序地址空间
android·java·开发语言·数据库
呆呆小雅4 小时前
C# 结构体
android·java·c#
ᥬ 小月亮7 小时前
Layui表格的分页下拉框新增“全部”选项
android·javascript·layui
sunly_16 小时前
Flutter:启动屏逻辑处理02:启动页
android·javascript·flutter
Sgq丶17 小时前
Android Studio 配置 proto
android·ide·android studio
_小马快跑_20 小时前
ConstraintLayout 中的ImageFilterView探索:处理图片圆角、亮度、饱和度、图片重叠等
android