CameraX 入门指南

1. 开始之前

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

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

先决条件

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

您将学习的内容

  • 了解如何添加 CameraX 依赖项。
  • 了解如何在活动中显示相机预览。(预览用例)
  • 构建一个可以拍摄照片并将其保存到存储区的应用程序。(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 中,创建一个新项目,并在出现提示时选择空视图活动

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
}

出现提示时,点击立即同步,然后我们就可以在我们的应用程序中使用 CameraX 了。

创建 Codelab 布局

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

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

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

  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 中的代码替换为以下代码,但请保持包名称不变。它包含导入语句、我们将实例化的变量、我们将实现的函数以及常量。

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. 实现预览用例

在相机应用程序中,取景器用于让用户预览他们将要拍摄的照片。我们将使用 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 用例的引用。如果用例为空,则退出函数。如果我们在设置图像捕获之前点击了照片按钮,则它将为空。如果没有 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. 重新运行应用程序并按下拍摄照片。我们应该会在屏幕上看到一个吐司以及日志中的消息。

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 在所需的Quality.HIGHEST不受imageCapture用例支持的情况下选择一个受支持的分辨率。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. 同样在startCamera()中,将imageCapture用例与现有的预览和 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)中替换imageCaptureImageAnalyzer用例:我们将使用Preview + ImageAnalysis + VideoCapture组合(再次注意,即使使用LEVEL_3相机设备,Preview + Analysis + ImageCapture + VideoCapture组合也可能不受支持)!

9. 恭喜!

您已成功地从零开始将以下内容实现到一个新的 Android 应用中

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