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. 创建项目
- 在 Android Studio 中,创建一个新项目,并在出现提示时选择Empty Views Activity。
- 接下来,将应用命名为“CameraXApp”,并确认或更改包名称为“
com.android.example.cameraxapp
”。选择 Kotlin 作为语言,并将最低 API 级别设置为 21(**这是 CameraX 的最低要求**)。对于较旧版本的 Android Studio,请务必包含 AndroidX 工件支持。
添加 Gradle 依赖项
- 打开
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}"
}
- 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(用于预览相机图像/视频)。
- 一个标准按钮来控制图像捕获。
- 一个标准按钮来启动/停止视频捕获。
- 一个垂直指南来定位 2 个按钮。
让我们使用以下代码替换默认布局来
- 打开
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>
- 使用以下内容更新
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. 实现 Preview 用例
在相机应用中,取景器用于让用户预览他们将要拍摄的照片。我们将使用 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
用例的引用。如果用例为 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)
}
- 转到
startCamera()
方法,并将此代码复制到预览代码下方。
imageCapture = ImageCapture.Builder().build()
- 最后,更新对
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 能够在imageCapture
用例不支持所需Quality.HIGHEST
时,选择支持的分辨率。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- 同样在
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))
}
- 构建并运行。我们应该看到之前步骤中熟悉的 UI,只是这次,“拍摄照片”和“开始捕获”按钮都起作用了。
- 执行一些捕获操作
- 点击“开始捕获”按钮开始捕获。
- 点击“拍照”按钮拍摄图片。
- 等待图片拍摄完成(我们应该会看到之前看到的提示信息)。
- 点击“停止捕捉”按钮停止录制。
我们在预览和视频录制过程中进行图片捕捉!
- 像之前步骤中在 Google 相册应用中一样查看捕捉的图片和视频文件。这次我们应该看到两张照片和两个视频片段。
- (可选)在以上步骤(步骤 1 到步骤 4)中将
imageCapture
替换为ImageAnalyzer
用例:我们将使用Preview
+ImageAnalysis
+VideoCapture
组合(请再次注意,即使使用LEVEL_3
摄像头设备,Preview
+Analysis
+ImageCapture
+VideoCapture
组合可能不受支持)!
9. 恭喜!
您已成功将以下内容从头开始实现到一个新的 Android 应用中
- 将 CameraX 依赖项 包含到新项目中。
- 使用
Preview
用例显示相机取景器。 - 使用
ImageCapture
用例实现照片捕捉并将图片保存到存储器中。 - 使用
ImageAnalysis
用例实现对来自摄像头的帧的实时分析。 - 使用
VideoCapture
用例实现视频捕捉。