CameraX 使用入门

1. 前言

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

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

前提条件

  • 拥有基本的 Android 开发经验。
  • 需要具备 MediaStore 知识,但这并非强制性要求。

实践内容

  • 了解如何添加 CameraX 依赖项。
  • 了解如何在 activity 中显示相机预览。(Preview 用例)
  • 构建能够拍摄照片并将其保存到存储空间的应用。(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 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.1.0-beta01"
  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 代码块的末尾,紧跟在 buildTypes 之后,添加以下代码:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. 此 Codelab 使用 ViewBinding,因此请使用以下代码(在 android{} 代码块末尾)启用它:
buildFeatures {
   viewBinding true
}

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

创建 Codelab 布局

在此 Codelab 的界面中,我们使用了以下内容:

  • CameraX PreviewView(用于预览相机图片/视频)。
  • 用于控制图片拍摄的标准按钮。
  • 用于开始/停止视频拍摄的标准按钮。
  • 用于放置 2 个按钮的垂直指南。

我们将默认布局替换为以下代码,从而:

  1. 打开位于 res/layout/activity_main.xmlactivity_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 中的代码替换为以下代码,但保留软件包名称不变。它包含 import 语句、将要实例化的变量、要实现的函数以及常量。

系统已实现 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.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 {
           ActivityCompat.requestPermissions(
               this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
       }

       // 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 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 const val REQUEST_CODE_PERMISSIONS = 10
       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. 中。下面几项内容将会详细介绍我们刚刚复制的代码。
override fun onRequestPermissionsResult(
   requestCode: Int, permissions: Array<String>, grantResults:
   IntArray) {
   if (requestCode == REQUEST_CODE_PERMISSIONS) {
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           Toast.makeText(this,
               "Permissions not granted by the user.",
               Toast.LENGTH_SHORT).show()
           finish()
       }
   }
}
  • 检查请求代码是否正确;否则,请忽略它。
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
  • 如果已授予权限,请调用 startCamera()
if (allPermissionsGranted()) {
   startCamera()
}
  • 如果未授予权限,系统会显示一个消息框,通知用户未授予权限。
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. 运行应用。

它现在应请求使用摄像头和麦克风:

dcdf8aa3d87e74be.png

4. 实现 Preview 用例

在相机应用中,取景器用于让用户预览他们拍摄的照片。我们将使用 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,从取景器中获取 Surface 提供程序,然后在预览上进行设置。
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() 方法,该方法会在用户按下 photo 按钮时调用。

  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,请退出函数。如果在设置图片拍摄之前点按“photo”按钮,它将为 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)
}
  • 如果拍摄未失败,即表示照片拍摄成功!将照片保存到我们之前创建的文件中,显示消息框,让用户知道照片已拍摄成功,并输出日志语句。
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. 重新运行应用,然后按 Take Photo。屏幕上应该会显示一个消息框,我们会在日志中看到一条消息。

54292eaa4ce3be0a.png

查看照片

现在,新拍摄的照片会保存到 MediaStore 中,我们可以使用任何 MediaStore 应用查看这些照片。例如,如果使用 Google 相册应用,请执行以下操作:

  1. 启动 Google 相册 Google 相册
  2. 点按“媒体库”(如果未使用您的帐号登录 Google 相册应用,则不需要)查看已排序的媒体文件,"CameraX-Image" 文件夹是我们的文件夹。

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. 点按图片图标即可查看完整照片;点按右上角的“更多”按钮 更多 即可查看所拍摄照片的细节。

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 实例(与其他用例类似),并再次更新 startCamera() 函数,然后调用 CameraX.bindToLifecycle() 即可:

  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
  • 在 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<Recorder>Recorder 并启用录音
    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
}
  • 当相机设备开始请求录制时,将“Start Capture”按钮文本切换为“Stop Capture”。
is VideoRecordEvent.Start -> {
    viewBinding.videoCaptureButton.apply {
        text = getString(R.string.stop_capture)
        isEnabled = true
    }
}
  • 完成录制后,用消息框通知用户,并将“Stop Capture”按钮切换回“Start Capture”,然后重新启用它:
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. 构建并运行。您应该会看到之前步骤中的熟悉界面。
  2. 录制一些剪辑:
  • 按“START CAPTURE”按钮。请注意,图片说明会变为“STOP CAPTURE”。
  • 录制几秒钟或几分钟的视频。
  • 按“STOP CAPTURE”按钮(和 start capture 按钮是同一个按钮)。

ef2a6005defc4977.png 8acee41fd0f4af0f.png

观看视频(与查看拍摄图片文件相同)

我们会使用 Google 相册应用查看拍摄的视频:

  1. 启动 Google 相册 Google 相册
  2. 点按“媒体库”以查看已排序的媒体文件。点按 "CameraX-Video" 文件夹图标,查看可用视频剪辑的列表。

71f07e32d5f4f268.png 596819ad391fac37.png

  1. 点按相应图标可播放刚刚拍摄的视频剪辑。播放完毕后,点按右上角的“更多”按钮 更多 可查看剪辑详情。

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 创建中。这样一来,如果 imageCapture 用例不支持所需的 Quality.HIGHEST,CameraX 将可以选择支持的分辨率。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. 同样在 startCamera() 中,将 imageCapture 用例与现有 preview 和 videoCapture 用例绑定(注意:不要绑定 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. 构建并运行。我们应该会看到之前步骤中的熟悉界面,但这一次,“Take Photo”和“Start Capture”按钮一切正常。
  2. 请进行一些拍摄:
  • 点按“START CAPTURE”按钮即可开始拍摄。
  • 点按“TAKE PHOTO”即可拍摄图片。
  • 等待图片拍摄完成(我们应该会看到之前看到的消息框)。
  • 点按“STOP CAPTURE”按钮即可停止录制。

我们会在预览和视频拍摄过程中执行图片拍摄!

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 及其作用,请查看相关文档或克隆官方示例