jetpack库之CameraX的使用

jetpack库之CameraX的使用

jetpack库之一的 CameraX。值得了解与学习

最初编写摄像头相关的应用时,使用的是 Camera.open() 接口。简单快捷。后台 Camera 接口标记为过时。而 Camera2 接口又相对复杂,于是很长一段时间,在编写 Demo 演示类 App 时,我依旧偏向使用 Camera.open() 接口。现在 google 有 Camerax 库,简化 Camera2 相机操作,那么自然要体验一下了

要求

  • Android 最低 sdk版本为 21
  • androidx 组件库最低 1.1.1

准备工作

创建一个空的工程,并导入依赖项

在项目 app 的 build.gradle 文件的 android{} 中,添加 java 1.8 要求

compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

dependencies 中添加 Camerax 依赖库

    implementation "androidx.camera:camera-core:1.0.0-beta10"
    implementation "androidx.camera:camera-camera2:1.0.0-beta10"
    // 上面两个是核心库,这个是用于 surface 预览需要的库
    implementation "androidx.camera:camera-lifecycle:1.0.0-beta10"
    implementation "androidx.camera:camera-view:1.0.0-alpha17"

添加权限配置

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

编写请求权限的代码

请求权限是使用 Camera 的必备操作

代码如下:

class MainActivity : AppCompatActivity() {

    val CAMERA_PERMISSION = 1001
    val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.CAMERA
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            reqCameraPermission();
        } else {
            openCameraPreview()
        }
    }

    private fun reqCameraPermission() {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.CAMERA),
            CAMERA_PERMISSION
        )
    }

    /**
     * 打开相机预览
     */
    private fun openCameraPreview() {

    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == CAMERA_PERMISSION) {
            if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                reqCameraPermission()
            } else {
                openCameraPreview()
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }
}

打开相机预览画面

相机预览不可避免的要用到 SurfaceView ,那么在 layout 中要定义布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewSurface"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

上面的 MainActivity 类,定义了一个 openCameraPreview() 方法,然后权限请求也都有了。现在只需要在这个方法中实现方法就好了。

这个方法的具体实现为下列这样:

    /**
     * 打开相机预览
     */
    private fun openCameraPreview() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            val cameraProvider = cameraProviderFuture.get()
            val surfaceView = findViewById<PreviewView>(R.id.previewSurface)

            val preview = Preview.Builder()
                .build().also {
                    it.setSurfaceProvider(surfaceView.surfaceProvider)
                }
            val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build()
            try {
                cameraProvider.unbind()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview)
            } catch (e: Exception) {
                Log.w(TAG, e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

点击 AndroidStudiorun 按钮,稍等片刻,app 上便会看到相机的预览画面了

HAMpfZaBl4cKODd

仅以预览页面来说,需要编写的代码量确实降低许多。而且可以感知生命周期,不需要在 onStart、onStop、onDestory 中做各种操作了
使用原来的 Camera api ,一旦生命周期管理不当,就有可能导致相机崩溃。而现在这些工作都交给库去处理

进阶使用

相机的使用一般是预览后拍摄图片。或者是实时的视频流获取,所以要尝试使用拍照和实时视屏流功能

拍照

在 xml 布局文件中增加一个按钮:

    <Button
        android:id="@+id/captureBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="30dp"
        android:text="拍照"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

然后在 MainActivity 中定义 ImageCapture

var imageCapture: ImageCapture? = null

private fun openCameraPreview() {
	.........
	// 修改 预览方法中 ,bindToLifecycle 的调用
        imageCapture = ImageCapture.Builder()
            .build()
        cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
}

定义一个新的方法,并且为之前创建的 button 添加点击事件:

    /**
     * 截图图片
     */
    private fun takePhoto() {
        val imageCapture = imageCapture ?: return

        // Create time-stamped output file to hold the image
        val photoFile = File(
            this.getExternalFilesDir("images"),
            "test.jpg"
        )

        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        // Set up image capture listener, which is triggered after photo has
        // been taken
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val savedUri = Uri.fromFile(photoFile)
                    val msg = "Photo capture succeeded: $savedUri"
                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                    Log.d(TAG, msg)
                }
            })
    }

现在只要点击按钮就可以拍照了

拍摄

录制视频

在前面的拍照章节中,获取图片是定义了一个 ImageCapture ,然后 bindToLifecycle 方法中再增加绑定 ImageCapture ,录制视频也是同样的方式

需要使用的是 VideoCapture

            videoCapture = VideoCapture.Builder()
                .build()

            try {
                cameraProvider.unbind()
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    videoCapture
                )
            } catch (e: Exception) {
                Log.w(TAG, e)
            }

注意:录制视频和拍照两者不能共存

所以在使用 VideoCapture 时,要将 ImageCapture 移除

否则会出现如下错误:

May be attempting to bind too many use cases. Existing surfaces: [] New configs:

然后定义两个按钮,一个开始,一个结束录制:

    private fun recoverStart() {
        val videoCapture = videoCapture ?: return

        val mp4File = File(
            this.getExternalFilesDir("video"),
            "test.mp4"
        )
        val options = VideoCapture.OutputFileOptions.Builder(mp4File).build()

        videoCapture.startRecording(options, Executors.newSingleThreadExecutor(),
            object : VideoCapture.OnVideoSavedCallback {
                override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
                    Log.i(TAG, "onVideoSaved: saved")
                }

                override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                    Log.i(TAG, "onError: $videoCaptureError")
                }

            })
        // 停止按钮调用
//        videoCapture.stopRecording()
    }

增加音频权限,并且代码中要动态申请权限

由于录制视频时,默认是有声音的,所以录制时必须要在清单文件中增加权限

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

如果没有音频权限在点击录制按钮时是会报错的!!!

成功录制视频:

录制

目前录制视频功能还需要 @SuppressLint("RestrictedApi") 注释,否则会有一个红色警告,所以暂时还不推荐使用 CameraX 的库直接录制 mp4

获取实时视频流

拍照和录制是最基本的功能,而我需要获取实时的视频流,然后推送给某个程序进行处理。目前未找到 camerax 能够直接提供 Callback 的方法

也就是说目前 CameraX 暂时只能满足拍照、录制、预览等处理,对于实时视频流获取,依旧要使用 Camera2

后续

CameraX 出来也有一段时间了。目前很多网站上均有相关的教程和文章。

通过实际编写,发现部分接口与以前的教程相比,有略微的改动。

随着时间推移,可能本篇文章的部分方法和接口也会有所修改

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://zwc365.com/2020/10/28/jetpack库之camerax的使用

Buy me a cup of coffee ☕.