相机,作为手机最重要的一个多媒体东西,被应用于许多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获取到数据显现即可。

Android进阶宝典 -- CameraX与Camera2的使用比对
然后,调用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,重复建议预览的恳求,来获取到每一帧的数据,也正是下图所展示的。

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

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这块仅仅是讲了怎么运用,详细的核心完成等下篇文章细细道来。