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. 创建项目
- 在 Android Studio 中,创建一个新项目,并在出现提示时选择空视图活动。
- 接下来,将应用程序命名为“CameraXApp”,并确认或更改包名称为“
com.android.example.cameraxapp
”。选择 Kotlin 作为语言,并将最低 API 级别设置为 21(这是 CameraX 的最低要求)。对于旧版本的 Android Studio,请务必包含 AndroidX 工件支持。
添加 Gradle 依赖项
- 打开
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}"
}
- CameraX 需要一些 Java 8 中的方法,因此我们需要相应地设置编译选项。确保
android
块包含以下内容
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- 此 Codelab 使用 ViewBinding,因此使用以下代码启用它(在
android{}
块的末尾)
buildFeatures {
viewBinding true
}
出现提示时,点击立即同步,然后我们就可以在我们的应用程序中使用 CameraX 了。
创建 Codelab 布局
在此 Codelab 的 UI 中,我们使用以下内容
- 一个 CameraX PreviewView(用于预览相机图像/视频)。
- 一个标准按钮来控制图像捕获。
- 一个标准按钮来启动/停止视频捕获。
- 一条垂直指导线来定位这两个按钮。
让我们用以下代码替换默认布局以
- 打开位于
res/layout/activity_main.xml
的activity_main
布局文件,并将其替换为以下代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/image_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginEnd="50dp"
android:elevation="2dp"
android:text="@string/take_photo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />
<Button
android:id="@+id/video_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="50dp"
android:elevation="2dp"
android:text="@string/start_capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/vertical_centerline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".50" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 使用以下内容更新
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
- 将
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 需要外部存储写入权限。在此步骤中,我们将实现这些必要的权限。
- 打开
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
表示它可以是前置摄像头或后置摄像头。
- 将此代码复制到
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()
}
- 将此代码复制到
requestPermissions()
方法中以启动上一步中添加的ActivityResultLauncher
。有关更多详细信息,请参阅 请求运行时权限 指南。
private fun requestPermissions() {
activityResultLauncher.launch(REQUIRED_PERMISSIONS)
}
- 运行应用程序。
它现在应该会询问使用相机和麦克风的权限
4. 实现预览用例
在相机应用程序中,取景器用于让用户预览他们将要拍摄的照片。我们将使用 CameraX Preview
类实现取景器。
要使用 Preview
,我们首先需要定义一个配置,然后将其用于创建用例的实例。生成的实例是我们绑定到 CameraX 生命周期的对象。
- 将此代码复制到
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)
}
- 运行应用程序。我们现在看到了相机预览!
5. 实现 ImageCapture 用例
其他用例的工作方式与 Preview
非常相似。首先,我们定义一个配置对象,用于实例化实际的用例对象。要捕获照片,您将实现 takePhoto()
方法,该方法在按下拍摄照片按钮时被调用。
- 将此代码复制到
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)
}
- 转到
startCamera()
方法并将此代码复制到预览代码下方。
imageCapture = ImageCapture.Builder().build()
- 最后,更新
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))
}
- 重新运行应用程序并按下拍摄照片。我们应该会在屏幕上看到一个吐司以及日志中的消息。
查看照片
现在新捕获的照片已保存到 MediaStore 中,我们可以使用任何 MediaStore 应用程序来查看它们。例如,使用 Google 相册应用,请执行以下操作
- 启动 Google 相册 。
- 点击“库”(如果未使用您的帐户登录相册应用,则无需此操作)查看已排序的媒体文件,
"CameraX-Image"
文件夹是我们的。
- 点击图像图标以查看完整照片;点击右上角的“更多”按钮查看拍摄照片的详细信息。
如果我们只是想找一个简单的相机应用来拍照,我们就完成了。真的就这么简单!如果我们想实现一个图像分析器,请继续阅读!
6. 实现 ImageAnalysis 用例
使我们的相机应用更有趣的一个好方法是使用ImageAnalysis
功能。它允许我们定义一个实现ImageAnalysis.Analyzer
接口的自定义类,该类将使用传入的相机帧进行调用。我们无需管理相机会话状态,甚至无需处理图像;绑定到我们应用所需的生命周期就足够了,就像其他生命周期感知组件一样。
- 在
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()
函数。
- 在
startCamera()
方法中,在imageCapture
代码下添加此代码。
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
- 更新
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))
}
- 现在运行应用程序!它将在 logcat 中大约每秒生成一条类似于此的消息。
D/CameraXApp: Average luminosity: ...
7. 实现 VideoCapture 用例
CameraX 在版本 1.1.0-alpha10 中添加了 VideoCapture 用例,并且此后一直在不断改进。请注意,VideoCapture
API 支持许多视频捕获功能,为了使本代码实验室易于管理,本代码实验室仅演示将视频和音频捕获到MediaStore
中。
- 将此代码复制到
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")
}
}
- 使用外部内容选项创建一个
MediaStoreOutputOptions.Builder
。
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()
}
}
- 启动此新录制,并注册一个 lambda
VideoRecordEvent
监听器。
.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
}
}
- 在
startCamera()
中,在preview
创建行之后放置以下代码。这将创建VideoCapture
用例。
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
- (可选)同样在
startCamera()
中,通过删除或注释掉以下代码来禁用imageCapture
和imageAnalyzer
用例
/* 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")
})
}
*/
- 将
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))
}
- 构建并运行。我们应该看到前面步骤中熟悉的 UI。
- 录制一些片段
- 按下“开始捕获”按钮。请注意,它的标题将更改为“停止捕获”。
- 录制几秒/几分钟的视频。
- 按下“停止捕获”按钮(与开始捕获的按钮相同)。
查看视频(*与查看捕获的图像文件相同*)
我们将使用Google 相册应用来查看捕获的视频
- 启动 Google 相册 。
- 点击“媒体库”以查看排序的媒体文件。点击
"CameraX-Video"
文件夹图标以查看可用视频剪辑的列表。
- 点击图标播放刚刚捕获的视频剪辑。播放完成后,点击右上角的“更多”按钮检查剪辑详细信息。
这就是我们录制视频所需的一切!但是 CameraX VideoCapture
还有更多功能,包括
- 暂停/恢复录制。
- 捕获到
File
或FileDescriptor
。 - 等等。
有关如何使用它们的说明,请参阅官方文档。
8. (可选)将 VideoCapture 与其他用例结合
前面的VideoCapture
步骤演示了Preview
和VideoCapture
的组合,如设备功能表中所述,此组合在所有设备上都受支持。在此步骤中,我们将ImageCapture
用例添加到现有的VideoCapture
+ Preview
组合中,以演示Preview + ImageCapture + VideoCapture
。
- 使用上一步中现有的代码,在
startCamera()
中取消注释并启用imageCapture
创建
imageCapture = ImageCapture.Builder().build()
- 在现有的
QualitySelector
创建中添加一个FallbackStrategy
。这允许 CameraX 在所需的Quality.HIGHEST
不受imageCapture
用例支持的情况下选择一个受支持的分辨率。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- 同样在
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))
}
- 构建并运行。我们应该看到前面步骤中熟悉的 UI,只是这次“拍照”和“开始捕获”按钮都起作用了。
- 进行一些捕获
- 点击“开始捕获”按钮开始捕获。
- 点击“拍照”以捕获图像。
- 等待图像捕获完成(我们应该像以前一样看到一个吐司)。
- 点击“停止捕获”按钮停止录制。
我们正在预览和视频捕获正在进行时执行图像捕获!
- 像我们在前面步骤中使用 Google 相册应用那样查看捕获的图像和视频文件。这次,我们应该看到两张照片和两个视频剪辑。
- (可选)在上述步骤(步骤 1 到步骤 4)中替换
imageCapture
为ImageAnalyzer
用例:我们将使用Preview
+ImageAnalysis
+VideoCapture
组合(再次注意,即使使用LEVEL_3
相机设备,Preview
+Analysis
+ImageCapture
+VideoCapture
组合也可能不受支持)!
9. 恭喜!
您已成功地从零开始将以下内容实现到一个新的 Android 应用中
- 将 CameraX 依赖项 添加到新项目中。
- 使用
Preview
用例显示相机取景器。 - 使用
ImageCapture
用例实现照片捕获并将图像保存到存储中。 - 使用
ImageAnalysis
用例实现对来自相机的帧进行实时分析。 - 使用
VideoCapture
用例实现视频捕获。