CameraX 入门

1. 开始之前

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

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

先决条件

  • 基本的 Android 开发经验。
  • 熟悉 MediaStore 知识,但不是必需的。

您将学习的内容

  • 了解如何添加 CameraX 依赖项。
  • 了解如何在活动中显示相机预览。(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 Views Activity

ed0f21e863f9e38f.png

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

10f0a12f6c8b997c.png

添加 Gradle 依赖项

  1. 打开 build.gradle 文件,该文件用于 CameraXApp.app 模块,并添加 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
}

出现提示时,单击立即同步,我们就可以在应用中使用 CameraX 了。

创建 Codelab 布局

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

  • 一个 CameraX PreviewView(用于预览相机图像/视频)。
  • 一个标准按钮来控制图像捕获。
  • 一个标准按钮来启动/停止视频捕获。
  • 一个垂直指南来定位 2 个按钮。

让我们使用以下代码替换默认布局来

  1. 打开 activity_main 布局文件,该文件位于 res/layout/activity_main.xml,并将其替换为以下代码。
<?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
  • 如果权限未被授予,则显示一个吐司来通知用户权限未被授予。
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. 实现 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,从取景器获取表面提供程序,然后将其设置在预览上。
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
   }
  • 创建一个 CameraSelector 对象,并选择 DEFAULT_BACK_CAMERA
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)
}
  • 如果捕获未失败,则表示照片已成功拍摄!将照片保存到我们之前创建的文件中,显示一个吐司让用户知道它已成功,并打印一条日志语句。
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. 最后,更新对 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. 重新运行应用并按下拍摄照片。我们应该看到屏幕上显示一个吐司和日志中的消息。

54292eaa4ce3be0a.png

查看照片

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

  1. 启动 Google 相册 相册.
  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 支持许多视频捕获功能,为了使此代码实验室更易于管理,此代码实验室仅演示了将视频和音频捕获到 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 完成请求操作之前,禁用 UI;它将在我们注册的 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
    }
}
  • 当活动录制完成时,通过吐司通知用户,并将“停止捕获”按钮切换回“开始捕获”,并重新启用它
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 相册 相册.
  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. 向现有的 QualitySelector 创建中添加一个 FallbackStrategy。这使 CameraX 能够在 imageCapture 用例不支持所需 Quality.HIGHEST 时,选择支持的分辨率。
.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. 构建并运行。我们应该看到之前步骤中熟悉的 UI,只是这次,“拍摄照片”和“开始捕获”按钮都起作用了。
  2. 执行一些捕获操作
  • 点击“开始捕获”按钮开始捕获。
  • 点击“拍照”按钮拍摄图片。
  • 等待图片拍摄完成(我们应该会看到之前看到的提示信息)。
  • 点击“停止捕捉”按钮停止录制。

我们在预览和视频录制过程中进行图片捕捉!

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 和您可以使用它来做的事情的信息,请查看 文档 或克隆 官方示例