Android进阶宝典 -- CameraX与Camera2的使用比对

相机,作为手机最重要的一个多媒体工具,被应用于众多app软件中,如果整个项目中涉及到拍照、直播、录视频、扫码,那么相机就必须要用到。传统的相机app,一般使用到Camera或者Camera2比较多,但是Google的JectPack框架中引入了CameraX组件作为官方推荐相机架构,既然推出此框架,那么一定是有它自身的优势之处在的,本文将会从CameraX和Camera2的框架机制出发,分析两者的不同以及性能差异。

1 Google相机元老 Camera2

其实在早期开发相机app的时候,有一部分会使用Camera,有一部分会使用Camera2,但是用起来真的是苦不堪言,往往在相机配置时,为了调出预览页面,至少要写1000行代码,而且仅仅是一个预览页面,后面拍照、录视频等等,还需要做额外的开发,说这么多,我们先看下Camera2是如何使用的吧。

1.1 Camera2的使用

首先Camera2是Google原生的相机框架,所以不需要引任何框架进来。

第一步:创建承载相机的容器

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/camera_view_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

一般来说,承载相机预览页面的就是TextureView,当TextureView创建完成之后,就可以配置相机参数,展示预览页面。

第二步:打开摄像头的时机

Camera2和Camera不同的是,Camera2是将应用层与相机内核层做了完全的解耦,需要通过Camera Service来获取到CameraManager以此调用相机内核层提供的能力。

那么在什么时机才能打开摄像头呢?就是在TextureView的onSurfaceTextureAvailable回调的时候,去初始化相机参数,开启摄像头。

/**
 * 初始化相机
 * 触发时机 TextureView 的 onSurfaceTextureAvailable回调
 */
private fun initCamera(){

}

override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
    initCamera()
}

override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
}

override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
    return false
}

override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}

第三步:搭建应用层与相机内核的桥梁

/**
 * 开启摄像头
 */
@SuppressLint("MissingPermission")
@Synchronized
fun start(textureView: TextureView) {

    val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
    //建立数据传输的桥梁
    imageReader = ImageReader.newInstance(
        previewWidth,
        previewHeight,
        ImageFormat.YUV_420_888,
        2
    )
    val handlerThread = HandlerThread("Camera2Manager")
    handlerThread.start()
    handler = Handler(handlerThread.looper)
    imageReader?.setOnImageAvailableListener(this, handler)
    //真正地开启摄像头
    cameraManager.openCamera("0", this, handler)
}

// setOnImageAvailableListener的回调
override fun onImageAvailable(reader: ImageReader?) {

}
// Device StateCallback
override fun onOpened(camera: CameraDevice) {

}

override fun onDisconnected(camera: CameraDevice) {

}

override fun onError(camera: CameraDevice, error: Int) {

}

因为应用层和相机内核层已经完全解耦,所以两者想要进行数据传递,必须要建立桥梁,那么通过ImageReader就可以完成,可以通过传入一些参数:预览尺寸、图像格式等,设置setOnImageAvailableListener,有数据发送过来之后,通过onImageAvailable获取到数据显示即可。

然后,调用CameraManager的openCamera方法,才是真正打开摄像头,那么成功还是失败呢?就需要通过 CameraDevice.StateCallback的回调来判断。

第四步:建立会话,创建预览请求

override fun onOpened(camera: CameraDevice) {
    this.cameraDevice = camera
    //建立会话
    createPreviewSession()
}

override fun onDisconnected(camera: CameraDevice) {
    cameraDevice?.close()
    cameraDevice = null
}

override fun onError(camera: CameraDevice, error: Int) {
    cameraDevice?.close()
    cameraDevice = null
}

当打开Camera之后,如何判断是否成功开启摄像头呢?就是通过onOpened这个回调来判断,当成功开启摄像头之后,就需要与相机内核建立会话,发起预览请求createCaptureRequest。

private fun createPreviewSession() {
    //创建Surface,所有的画面渲染都是由Surface处理
    val texture = textureView?.surfaceTexture
    texture?.setDefaultBufferSize(previewWidth, previewHeight)
    val surface = Surface(texture)
    //开启预览会话,这样就可以预览数据
    val builder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
    builder?.addTarget(surface)
    builder?.set(
        CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
    )
    //想要获取每一帧的数据,需要给ImageReader设置Target
    imageReader?.surface?.let {
        builder?.addTarget(it)
    }
    //建立连接,开启会话
    cameraDevice?.createCaptureSession(
        mutableListOf(surface, imageReader?.surface),
        CaptureSessionCallback(),
        handler
)

我们知道,如果使用TextureView,那么所有画面的渲染都是通过Surface来渲染,因此在创建Surface之后,就可以添加到cameraDevice中,此时页面中就会有图像了,如果想要分析每一帧的数据,那么也需要给ImageReader设置Surface.

完成一系列配置之后,调用CameraDevice的createCaptureSession方法,开启会话。

inner class CaptureSessionCallback : CameraCaptureSession.StateCallback() {
    override fun onConfigured(session: CameraCaptureSession) {
        mSession = session
        if (cameraDevice == null) return
        session.setRepeatingRequest(requestBuilder?.build()!!, null, handler)
    }

    override fun onConfigureFailed(session: CameraCaptureSession) {

    }
}

因为需要实时的数据流一帧一帧地传递过来,因此需要发起重复请求setRepeatingRequest,以此将实时的图像帧发送到手机页面上进行渲染。

1.2 小结

至此我们先做一个小小的总结,我们知道如果做一个相机app,最主要的两个功能:预览和分析图片帧。

我们在打开摄像头之前,首先会通过ImageReader来完成应用层和相机内核层的建立,通过监听回调获取每一帧的图像数据;然后开启摄像头之后,又完成的一个工作就是配置Surface,将其挂载到CameraDevice上,然后如果想要请求获取图像帧,那么就需要开启session,重复发起预览的请求,来获取到每一帧的数据,也正是下图所展示的。

1.3 问题分析

如果我们做到了上图的配置,我们运行之后发现,只拿到了一帧数据,紧接着抛出了一个异常,这个是在onError中回调的。

2023-01-14 14:56:14.220 29697-29803/com.lay.highavailablecamera E/HighAvailableCamera: onOpened
2023-01-14 14:56:14.574 29697-29803/com.lay.highavailablecamera E/HighAvailableCamera: onImageAvailable android.media.ImageReader@f04668
2023-01-14 14:59:14.122 29697-29803/com.lay.highavailablecamera E/HighAvailableCamera: onError 3

如果做过相机应用的伙伴应该也见到过这个情况,主要原因就是,一帧一帧地数据传递过来之后,我们没有处理,导致数据一直阻塞没有关闭,从而无法处理下一帧数据,因此在拿到最新的数据之后,需要主动调用close方法,以便下一帧数据进入到屏幕中。

override fun onImageAvailable(reader: ImageReader?) {
    Log.e(Constants.TAG, "onImageAvailable $reader")
    val latestImage = reader?.acquireNextImage()
    //NOTE 做数据处理
    latestImage?.close()
}

2 CameraX的便捷之处

前面我们用Camera2完成了页面的预览,伙伴们如果有需要源码的,可以直接找我。通过前面对于Camera2的使用,我们发现太麻烦了,就为了完成一个页面预览,写了很多配置,好处就是这些配置是通用的,不需要频繁地改动,但是我们想的就是能够一键式搭建一个相机应用,那么CameraX就是我们需要的”傻瓜相机“。

2.1 UseCase分类

其实,CameraX对于每个功能都做了详细的划分,例如预览的PreView、图像分析的ImageAnalysis等,因此在做Camera配置的时候,可以选择自己想要的UseCase进行初始化。

private fun initCameraConfig(context: Context) {

    val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

    cameraProviderFuture.addListener({

        // 预览配置
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(binding.basePreview.surfaceProvider)
            }
        //设置摄像头是前置还是后置
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        if (cameraProvider == null) {

            cameraProvider = cameraProviderFuture.get()
            //拍照useCase配置
            if (imageCapture == null) {
                imageCapture = ImageCapture
                    .Builder()
                    .setTargetRotation(Surface.ROTATION_90)
                    .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
                    .setFlashMode(ImageCapture.FLASH_MODE_AUTO) 
                    .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                    .build()
            }
            //图像分析useCase配置
            if (imageAnalysis == null) {
                imageAnalysis = ImageAnalysis.Builder()
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                    .build()
            }
            //图像数据处
            try {
                cameraProvider?.unbindAll()
                cameraProvider?.bindToLifecycle(
                    lifecycleOwner!!, cameraSelector, preview, imageCapture, imageAnalysis
                )

            } catch (exc: Exception) {
            }

        }

    }, ContextCompat.getMainExecutor(context))
}

其实我们可以看到,这里我们是将拍照的userCase以及图像分析的useCase都做了配置,如果只需要做一个预览配置,也就几十行代码就能够完成,单就使用上,比Camera2要简单的多的多。

<androidx.camera.view.PreviewView
    android:id="@+id/base_preview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

对于预览页面,CameraX中也提供了PreviewView容器,它其实是继承自FrameLayout。

其实对于CameraX这块仅仅是讲了如何使用,具体的核心实现等下篇文章细细道来。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

更多Android可以查看我的个人介绍!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容