CameraX 入门

1. 开始之前

在本 Codelab 中,您将学习如何创建一个相机应用,该应用使用 CameraX 显示取景器、拍照、录制视频以及分析来自相机的图像流。

为此,我们将介绍 CameraX 中的用例概念,您可以将其用于各种相机操作,从显示取景器到捕获视频。

前提条件

  • 基本的 Android 开发经验。
  • 了解 MediaStore 知识是可取的,但不是必需的。

您将执行的操作

  • 学习如何添加 CameraX 依赖项。
  • 学习如何在 Activity 中显示相机预览。(预览用例)
  • 构建一个可以拍照并将其保存到存储的应用。(ImageCapture 用例)
  • 学习如何实时分析来自相机的帧。(ImageAnalysis 用例)
  • 学习如何将视频捕获到 MediaStore。(VideoCapture 用例)

您将需要

  • Android 设备或 Android Studio 模拟器
  • 建议使用 Android 10 及更高版本:MediaStore 行为取决于分区存储的可用性。
  • 对于 Android 模拟器,我们建议使用基于 Android 11 或更高版本的 Android 虚拟设备 (AVD)。
  • 请注意,CameraX 只要求最低支持的 API 级别为 21。
  • Android Studio Arctic Fox 2020.3.1 或更高版本.
  • 了解 Kotlin 和 Android ViewBinding

2. 创建项目

  1. 在 Android Studio 中,创建一个新项目,并在出现提示时选择 Empty Views Activity

ed0f21e863f9e38f.png

  1. 接下来,将应用命名为“CameraXApp”,并确认或将软件包名称更改为“com.android.example.cameraxapp”。选择 Kotlin 作为语言,并将最低 API 级别设置为 21(这是 CameraX 所需的最低级别)。对于旧版本的 Android Studio,请务必包含 AndroidX 工件支持。

10f0a12f6c8b997c.png

添加 Gradle 依赖项

  1. 打开 CameraXApp.app 模块的 build.gradle 文件,并添加 CameraX 依赖项
dependencies {
  def camerax_version = "1.2.2"
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  implementation "androidx.camera:camera-video:${camerax_version}"

  implementation "androidx.camera:camera-view:${camerax_version}"
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}
  1. CameraX 需要 Java 8 中的一些方法,因此我们需要相应地设置编译选项。确保 android 块具有以下内容
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. 本 Codelab 使用 ViewBinding,因此请使用以下代码启用它(在 android{} 块的末尾)
buildFeatures {
   viewBinding true
}

出现提示时,点击 Sync Now,我们就可以在应用中使用 CameraX 了。

创建 Codelab 布局

在本 Codelab 的 UI 中,我们使用以下内容

  • 一个 CameraX PreviewView(用于预览相机图像/视频)。
  • 一个用于控制图像捕获的标准按钮。
  • 一个用于开始/停止视频捕获的标准按钮。
  • 一个用于定位两个按钮的垂直辅助线。

让我们用此代码替换默认布局,以

  1. 打开 res/layout/activity_main.xml 处的 activity_main 布局文件,并用以下代码替换它。
<?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/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

   <Button
       android:id="@+id/image_capture_button"
       android:layout_width="110dp"
       android:layout_height="110dp"
       android:layout_marginBottom="50dp"
       android:layout_marginEnd="50dp"
       android:elevation="2dp"
       android:text="@string/take_photo"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />

   <Button
       android:id="@+id/video_capture_button"
       android:layout_width="110dp"
       android:layout_height="110dp"
       android:layout_marginBottom="50dp"
       android:layout_marginStart="50dp"
       android:elevation="2dp"
       android:text="@string/start_capture"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toEndOf="@id/vertical_centerline" />

   <androidx.constraintlayout.widget.Guideline
       android:id="@+id/vertical_centerline"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:orientation="vertical"
       app:layout_constraintGuide_percent=".50" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 用以下内容更新 res/values/strings.xml 文件
<resources>
   <string name="app_name">CameraXApp</string>
   <string name="take_photo">Take Photo</string>
   <string name="start_capture">Start Capture</string>
   <string name="stop_capture">Stop Capture</string>
</resources>

设置 MainActivity.kt

  1. 用以下代码替换 MainActivity.kt 中的代码,但保留包名不变。它包含导入语句、我们将实例化的变量、我们将实现的函数和常量。

onCreate() 已为我们实现,用于检查相机权限、启动相机、为照片和捕获按钮设置 onClickListener(),并实现 cameraExecutor。尽管 onCreate() 已为您实现,但在我们实现文件中的方法之前,相机仍无法工作。

package com.android.example.cameraxapp

import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.VideoRecordEvent
import androidx.core.content.PermissionChecker
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.Locale

typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    private var imageCapture: ImageCapture? = null

    private var videoCapture: VideoCapture<Recorder>? = null
    private var recording: Recording? = null

    private lateinit var cameraExecutor: ExecutorService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            requestPermissions()
        }

        // Set up the listeners for take photo and video capture buttons
        viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
        viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun takePhoto() {}

    private fun captureVideo() {}

    private fun startCamera() {}

    private fun requestPermissions() {}

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "CameraXApp"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private val REQUIRED_PERMISSIONS =
            mutableListOf (
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
            ).apply {
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }
}

3. 请求必要的权限

在应用打开相机之前,它需要用户的权限;录制音频也需要麦克风权限;在 Android 9 (P) 及更早版本上,MediaStore 需要外部存储写入权限。在此步骤中,我们将实现这些必要的权限。

  1. 打开 AndroidManifest.xml 并在 application 标签之前添加以下行。
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

添加 android.hardware.camera.any 可确保设备具有相机。指定 .any 意味着它可以是前置摄像头或后置摄像头。

  1. 将此代码复制到 MainActivity.kt 中。下面的要点将分解我们刚刚复制的代码。
private val activityResultLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions())
    { permissions ->
        // Handle Permission granted/rejected
        var permissionGranted = true
        permissions.entries.forEach {
            if (it.key in REQUIRED_PERMISSIONS && it.value == false)
                permissionGranted = false
        }
        if (!permissionGranted) {
            Toast.makeText(baseContext,
                "Permission request denied",
                Toast.LENGTH_SHORT).show()
        } else {
            startCamera()
        }
    }
  • 遍历每个 permissions.entries,如果任何 REQUIRED_PERMISSIONS 未被授予,则将 permissionGranted 设置为 false
  • 如果未授予权限,则显示一个 toast 以通知用户权限未授予。
if (!permissionGranted) {
    Toast.makeText(baseContext,
        "Permission request denied",
        Toast.LENGTH_SHORT).show()
}
  • 如果权限已授予,则调用 startCamera()
else {
   startCamera()
}
  1. 将此代码复制到 requestPermissions() 方法中,以启动上一步中添加的 ActivityResultLauncher。有关更多详细信息,请参阅请求运行时权限指南。
private fun requestPermissions() {
    activityResultLauncher.launch(REQUIRED_PERMISSIONS)
}
  1. 运行应用。

它现在应该请求使用相机和麦克风的权限

dcdf8aa3d87e74be.png

4. 实现预览用例

在相机应用中,取景器用于让用户预览他们将要拍摄的照片。我们将使用 CameraX Preview 类实现一个取景器。

要使用 Preview,我们首先需要定义一个配置,然后将其用于创建用例实例。生成的实例是我们绑定到 CameraX 生命周期的对象。

  1. 将此代码复制到 startCamera() 函数中。

下面的要点将分解我们刚刚复制的代码。

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
          .build()
          .also {
              it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
          }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  • 创建 ProcessCameraProvider 的实例。这用于将相机的生命周期绑定到生命周期所有者。由于 CameraX 具有生命周期感知能力,因此这消除了打开和关闭相机的工作。
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • cameraProviderFuture 添加侦听器。添加一个 Runnable 作为参数。我们稍后会填充它。添加 ContextCompat.getMainExecutor() 作为第二个参数。这会返回一个在主线程上运行的 Executor
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • Runnable 中,添加 ProcessCameraProvider。这用于将相机的生命周期绑定到应用程序进程中的 LifecycleOwner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • 初始化我们的 Preview 对象,在其上调用 build,从取景器获取表面提供程序,然后将其设置到预览上。
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
   }
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • 创建 try 块。在该块内,确保没有绑定到 cameraProvider 的内容,然后将我们的 cameraSelector 和预览对象绑定到 cameraProvider
try {
   cameraProvider.unbindAll()
   cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
}
  • 此代码有几种可能失败的方式,例如如果应用不再处于焦点。将此代码包装在 catch 块中,以便在失败时进行日志记录。
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. 运行应用。我们现在看到了相机预览!

d61a4250f6a3ed35.png

5. 实现 ImageCapture 用例

其他用例的工作方式与 Preview 非常相似。首先,我们定义一个配置对象,该对象用于实例化实际的用例对象。要捕获照片,您将实现 takePhoto() 方法,该方法在按下拍照按钮时调用。

  1. 将此代码复制到 takePhoto() 方法中。

下面的要点将分解我们刚刚复制的代码。

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions
           .Builder(contentResolver,
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    contentValues)
           .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 msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}
  • 首先,获取 ImageCapture 用例的引用。如果用例为 null,则退出函数。如果在设置图像捕获之前点击照片按钮,它将为 null。如果没有 return 语句,如果它为 null,应用将会崩溃。
val imageCapture = imageCapture ?: return
  • 接下来,创建一个 MediaStore 内容值以保存图像。使用时间戳,以便 MediaStore 中的显示名称是唯一的。
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }
  • 创建一个 OutputFileOptions 对象。此对象是我们可以指定输出方式的地方。我们希望将输出保存到 MediaStore 中,以便其他应用可以显示它,因此添加我们的 MediaStore 条目。
val outputOptions = ImageCapture.OutputFileOptions
       .Builder(contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues)
       .build()
  • imageCapture 对象上调用 takePicture()。传入 outputOptions、执行程序和图像保存时的回调。接下来您将填写回调。
imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this),
   object : ImageCapture.OnImageSavedCallback {}
)
  • 如果图像捕获失败或保存图像捕获失败,请添加一个错误情况以记录它失败。
override fun onError(exc: ImageCaptureException) {
   Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
  • 如果捕获没有失败,则照片拍摄成功!将照片保存到我们之前创建的文件中,显示一个 toast 以告知用户成功,并打印一条日志语句。
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)
}
  1. 转到 startCamera() 方法并将此代码复制到预览代码下方。
imageCapture = ImageCapture.Builder().build()
  1. 最后,更新 try 块中 bindToLifecycle() 的调用以包含新的用例
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)

此时该方法将如下所示

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
                 it.setSurfaceProvider(viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 重新运行应用并按拍照。我们应该会在屏幕上看到一个 toast 提示,并在日志中看到一条消息。

54292eaa4ce3be0a.png

查看照片

现在新捕获的照片已保存到 MediaStore 中,我们可以使用任何 MediaStore 应用程序查看它们。例如,使用 Google 相册应用,执行以下操作

  1. 启动 Google 相册 Photos
  2. 点击“图库”(如果未登录相册应用,则不必要)以查看排序后的媒体文件,"CameraX-Image" 文件夹是我们的。

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. 点击图像图标以查看完整照片;然后点击右上角的“更多”按钮 More以查看捕获照片的详细信息。

55e1a442ab5f25e7.png 70a8b27a76523f56.png

如果只是寻找一个简单的相机应用来拍照,那我们就大功告成了。它真的就是这么简单!如果我们要实现图像分析器,请继续阅读!

6. 实现 ImageAnalysis 用例

使我们的相机应用更有趣的一个好方法是使用 ImageAnalysis 功能。它允许我们定义一个实现 ImageAnalysis.Analyzer 接口的自定义类,该类将使用传入的相机帧进行调用。我们无需管理相机会话状态甚至处理图像;绑定到我们应用程序所需的生命周期就足够了,就像其他生命周期感知组件一样。

  1. MainActivity.kt 中将此分析器添加为内部类。分析器记录图像的平均亮度。要创建分析器,我们会在实现 ImageAnalysis.Analyzer 接口的类中重写 analyze 函数。
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

   private fun ByteBuffer.toByteArray(): ByteArray {
       rewind()    // Rewind the buffer to zero
       val data = ByteArray(remaining())
       get(data)   // Copy the buffer into a byte array
       return data // Return the byte array
   }

   override fun analyze(image: ImageProxy) {

       val buffer = image.planes[0].buffer
       val data = buffer.toByteArray()
       val pixels = data.map { it.toInt() and 0xFF }
       val luma = pixels.average()

       listener(luma)

       image.close()
   }
}

我们的类实现了 ImageAnalysis.Analyzer 接口,我们所需要做的就是在 ImageAnalysis 中实例化一个 LuminosityAnalyzer 实例,类似于其他用例,并在调用 CameraX.bindToLifecycle() 之前再次更新 startCamera() 函数

  1. startCamera() 方法中,将此代码添加到 imageCapture 代码下方。
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. 更新 cameraProvider 上的 bindToLifecycle() 调用以包含 imageAnalyzer
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)

现在完整的方法将如下所示

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture, imageAnalyzer)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 现在运行应用!它将在 logcat 中大约每秒产生一条类似这样的消息。
D/CameraXApp: Average luminosity: ...

7. 实现 VideoCapture 用例

CameraX 在 1.1.0-alpha10 版本中添加了 VideoCapture 用例,并从那时起一直在进行进一步改进。请注意,VideoCapture API 支持许多视频捕获功能,因此为了使本 Codelab 易于管理,本 Codelab 仅演示将视频和音频捕获到 MediaStore

  1. 将此代码复制到 captureVideo() 方法中:它控制 VideoCapture 用例的开始和停止。下面的要点将分解我们刚刚复制的代码。
// Implements VideoCapture use case, including start and stop capturing.
private fun captureVideo() {
   val videoCapture = this.videoCapture ?: return

   viewBinding.videoCaptureButton.isEnabled = false

   val curRecording = recording
   if (curRecording != null) {
       // Stop the current recording session.
       curRecording.stop()
       recording = null
       return
   }

   // create and start a new recording session
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()
   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .apply {
           if (PermissionChecker.checkSelfPermission(this@MainActivity,
                   Manifest.permission.RECORD_AUDIO) ==
               PermissionChecker.PERMISSION_GRANTED)
           {
               withAudioEnabled()
           }
       }
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                            .show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "Video capture ends with error: " +
                           "${recordEvent.error}")
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}
  • 检查是否已创建 VideoCapture 用例:如果未创建,则不执行任何操作。
val videoCapture = videoCapture ?: return
  • 禁用 UI,直到 CameraX 完成请求操作;它将在后续步骤中在我们注册的 VideoRecordListener 中重新启用。
viewBinding.videoCaptureButton.isEnabled = false
  • 如果正在进行主动录制,则停止录制并释放当前 recording。当捕获的视频文件准备好供我们的应用程序使用时,我们将收到通知。
val curRecording = recording
if (curRecording != null) {
    curRecording.stop()
    recording = null
    return
}
  • 要开始录制,我们创建一个新的录制会话。首先,我们创建我们预期的 MediaStore 视频内容对象,并以系统时间戳作为显示名称(这样我们可以捕获多个视频)。
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
           .format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH,
               "Movies/CameraX-Video")
       }
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
      .Builder(contentResolver,
               MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
  • 将创建的视频 contentValues 设置到 MediaStoreOutputOptions.Builder 中,并构建我们的 MediaStoreOutputOptions 实例。
    .setContentValues(contentValues)
    .build()
    videoCapture
    .output
    .prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
  • 在此录制中启用音频
.apply {
   if (PermissionChecker.checkSelfPermission(this@MainActivity,
           Manifest.permission.RECORD_AUDIO) ==
       PermissionChecker.PERMISSION_GRANTED)
   {
       withAudioEnabled()
   }
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
   //lambda event listener
}
  • 当相机设备开始请求录制时,将“开始捕获”按钮文本切换为“停止捕获”。
is VideoRecordEvent.Start -> {
    viewBinding.videoCaptureButton.apply {
        text = getString(R.string.stop_capture)
        isEnabled = true
    }
}
  • 当主动录制完成时,通过 toast 通知用户,并将“停止捕获”按钮切换回“开始捕获”,并重新启用它
is VideoRecordEvent.Finalize -> {
   if (!recordEvent.hasError()) {
       val msg = "Video capture succeeded: " +
                 "${recordEvent.outputResults.outputUri}"
       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
            .show()
       Log.d(TAG, msg)
   } else {
       recording?.close()
       recording = null
       Log.e(TAG, "Video capture succeeded: " +
                  "${recordEvent.outputResults.outputUri}")
   }
   viewBinding.videoCaptureButton.apply {
       text = getString(R.string.start_capture)
       isEnabled = true
   }
}
  1. startCamera() 中,将以下代码放在 preview 创建行之后。这将创建 VideoCapture 用例。
val recorder = Recorder.Builder()
   .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
   .build()
videoCapture = VideoCapture.withOutput(recorder)
  1. (可选)在 startCamera() 内部,通过删除或注释掉以下代码来禁用 imageCaptureimageAnalyzer 用例
/* comment out ImageCapture and ImageAnalyzer use cases
imageCapture = ImageCapture.Builder().build()

val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
*/
  1. Preview + VideoCapture 用例绑定到生命周期相机。仍在 startCamera() 内部,将 cameraProvider.bindToLifecycle() 调用替换为以下内容
   // Bind use cases to camera
   cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)

此时,startCamera() 应该如下所示

   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
           }

       val recorder = Recorder.Builder()
           .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
           .build()
       videoCapture = VideoCapture.withOutput(recorder)

       /*
       imageCapture = ImageCapture.Builder().build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }
       */

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider
               .bindToLifecycle(this, cameraSelector, preview, videoCapture)
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 构建并运行。我们应该会看到与之前步骤相似的 UI。
  2. 录制几个片段
  • 按“开始捕获”按钮。请注意,其标题将变为“停止捕获”。
  • 录制视频几秒钟/几分钟。
  • 按“停止捕获”按钮(与开始捕获按钮相同)。

ef2a6005defc4977.png 8acee41fd0f4af0f.png

观看视频(与观看捕获的图像文件相同

我们将使用 Google 相册应用来查看捕获的视频

  1. 启动 Google 相册 Photos
  2. 点击“图库”查看排序后的媒体文件。点击 "CameraX-Video" 文件夹图标可查看可用视频片段列表。

71f07e32d5f4f268.png 596819ad391fac37.png

  1. 点击图标播放刚刚捕获的视频片段。播放完成后,点击右上角的“更多”按钮 More以检查剪辑详细信息。

7c7125726af9e429.png 44da18b15ad2f607.png

这就是录制视频所需的全部!但 CameraX VideoCapture 还提供更多功能,包括

  • 暂停/恢复录制。
  • 捕获到 FileFileDescriptor
  • 等等。

有关如何使用它们的说明,请参阅官方文档

8. (可选) 将 VideoCapture 与其他用例结合

上一步的 VideoCapture 演示了 PreviewVideoCapture 的组合,如设备功能表中所述,所有设备都支持该组合。在此步骤中,我们将 ImageCapture 用例添加到现有的 VideoCapture + Preview 组合中,以演示 Preview + ImageCapture + VideoCapture

  1. 使用上一步的现有代码,取消注释并启用 startCamera() 中的 imageCapture 创建
imageCapture = ImageCapture.Builder().build()
  1. FallbackStrategy 添加到现有的 QualitySelector 创建中。这允许 CameraX 在所需的 Quality.HIGHEST 不支持 imageCapture 用例时选择支持的分辨率。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. 同样在 startCamera() 中,将 imageCapture 用例与现有的预览和视频捕服用例绑定(注意:不要绑定 imageAnalyzer,因为不支持 preview + imageCapture + videoCapture + imageAnalysis 组合)
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, videoCapture)

最终的 startCamera() 函数现在将如下所示

private fun startCamera() {
       val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

       cameraProviderFuture.addListener({
           // Used to bind the lifecycle of cameras to the lifecycle owner
           val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

           // Preview
           val preview = Preview.Builder()
               .build()
               .also {
                   it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
               }
           val recorder = Recorder.Builder()
               .setQualitySelector(QualitySelector.from(Quality.HIGHEST,
                    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
               .build()
           videoCapture = VideoCapture.withOutput(recorder)

           imageCapture = ImageCapture.Builder().build()

           /*
           val imageAnalyzer = ImageAnalysis.Builder().build()
               .also {
                   setAnalyzer(
                       cameraExecutor,
                       LuminosityAnalyzer { luma ->
                           Log.d(TAG, "Average luminosity: $luma")
                       }
                   )
               }
           */

           // Select back camera as a default
           val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

           try {
               // Unbind use cases before rebinding
               cameraProvider.unbindAll()

               // Bind use cases to camera
               cameraProvider.bindToLifecycle(
                   this, cameraSelector, preview, imageCapture, videoCapture)

           } catch(exc: Exception) {
               Log.e(TAG, "Use case binding failed", exc)
           }

       }, ContextCompat.getMainExecutor(this))
   }
  1. 构建并运行。我们应该会看到与之前步骤相似的 UI,只是这次“拍照”和“开始捕获”按钮都可用。
  2. 进行一些捕获
  • 点击“开始捕获”按钮开始捕获。
  • 点击“拍照”以捕获图像。
  • 等待图像捕获完成(我们应该会看到与之前一样的 toast 提示)。
  • 点击“停止捕获”按钮停止录制。

我们在预览和视频捕获进行中时执行图像捕获!

ef2a6005defc4977.png 16bc70ec3346fa66.png

  1. 像我们在前面步骤中使用 Google 相册应用那样查看捕获的图像和视频文件。这次,我们应该会看到两张照片和两个视频片段。

3f3feb19c8c73532.png

  1. (可选)在上述步骤(步骤 1 到步骤 4)中 imageCapture 替换为 ImageAnalyzer 用例:我们将使用 Preview + ImageAnalysis + VideoCapture 组合(请再次注意,即使使用 LEVEL_3 相机设备,Preview + Analysis + ImageCapture + VideoCapture 组合也可能不受支持。)!

9. 恭喜!

您已成功从头开始在新 Android 应用中实现以下功能

  • CameraX 依赖项包含到一个新项目中。
  • 通过使用 Preview 用例显示相机取景器。
  • 通过使用 ImageCapture 用例实现照片捕获并将图像保存到存储。
  • 通过使用 ImageAnalysis 用例实现实时分析来自相机的帧。
  • 通过使用 VideoCapture 用例实现视频捕获。

如果您有兴趣阅读更多关于 CameraX 及其功能的信息,请查看文档或克隆官方示例