探索 CameraX(11):Camera1 迁移到 CameraX
系列介绍 CameraX 相关的基础技术。
想要学习和提升音视频技术的朋友,快来加入我们的【音视频技术社群】,加入后你就能:
- 1)下载 30+ 个开箱即用的「音视频及渲染 Demo 源代码」
- 2)下载包含 500+ 知识条目的完整版「音视频知识图谱」
- 3)下载包含 200+ 题目的完整版「音视频面试题集锦」
- 4)技术和职业发展咨询 100% 得到回答
- 5)获得简历优化建议和大厂内推
现在加入,送你一张 20 元优惠券:点击领取优惠券
迁移前的准备
比较 CameraX 与 Camera1 的用法
尽管代码看起来有所不同,但 Camera1 和 CameraX 的底层概念非常相似。CameraX 将常见的相机功能抽象为用例,因此开发者无需过多关注从头构建相机体验,而是更多地关注应用的差异化。
以下是在 CameraX 中的用例与 Camera1 中的概念对比:
CameraX | Camera1 |
---|---|
CameraController / CameraProvider 配置 | 相机配置 |
↓ | ↓ |
Preview(预览) | 管理预览 Surface 并将其设置到相机 |
ImageCapture(图像捕获) | 设置 PictureCallback 并在相机上调用 takePicture() |
VideoCapture(视频捕获) | 管理相机和 MediaRecorder 的配置顺序 |
ImageAnalysis(图像分析) | 在预览 Surface 的基础上构建自定义分析代码 |
↑ | ↑ |
设备特定代码 | 处理设备旋转和缩放 |
↑ | ↑ |
相机会话管理(相机选择、生命周期管理) | 相机选择和生命周期管理 |
兼容性和性能
CameraX 支持运行 Android 5.0(API 级别 21)及更高版本的设备,覆盖了超过 98% 的现有 Android 设备。CameraX 能够自动处理设备间的差异,减少应用中的设备特定代码。
Android 开发概念
在深入代码之前,了解以下概念很有帮助:
- 视图绑定 :为 XML 布局文件生成绑定类,允许在活动中轻松引用视图。
- 异步协程 :一种并发设计模式,可用于处理返回
ListenableFuture
的 CameraX 方法。
迁移常见场景
选择相机
在相机应用中,提供选择不同相机的功能通常是首要任务之一。
Camera1
在 Camera1 中,可以通过调用 Camera.open()
(不带参数以打开第一个后置相机)或传入要打开的相机的整数 ID 来调用它。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Camera1: 通过 ID 选择相机
private fun safeCameraOpen(id: Int): Boolean {
return try {
releaseCameraAndPreview()
camera = Camera.open(id)
true
} catch (e: Exception) {
Log.e(TAG, "打开相机失败", e)
false
}
}
private fun releaseCameraAndPreview() {
preview?.setCamera(null)
camera?.release()
camera = null
}
CameraX: CameraController
在 CameraX 中,相机选择由 CameraSelector
类处理。你可以指定使用默认前置相机或后置相机。
示例代码
1
2
3
4
5
// CameraX: 使用 CameraController 选择相机
var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector
CameraX: CameraProvider
以下是使用 CameraProvider
选择默认前置相机的示例:
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CameraX: 使用 CameraProvider 选择相机
private suspend fun startCamera() {
val cameraProvider = ProcessCameraProvider.getInstance(this).await()
// 设置 UseCases(后续场景中详细介绍)
var useCases:Array = ...
// 设置 cameraSelector 以使用默认前置(自拍)相机
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
try {
// 解绑 UseCases 以重新绑定
cameraProvider.unbindAll()
// 将 UseCases 绑定到相机。此函数返回一个相机对象,可用于执行变焦、闪光灯和对焦等操作
var camera = cameraProvider.bindToLifecycle(
this, cameraSelector, useCases)
} catch(exc: Exception) {
Log.e(TAG, "UseCase 绑定失败", exc)
}
}
显示预览
大多数相机应用需要在屏幕上显示相机图像流。
Camera1
需要自己编写实现 android.view.SurfaceHolder.Callback
接口的 Preview
类,用于将图像数据从相机硬件传递到应用。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// Camera1: 设置相机预览
class Preview(
context: Context,
private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {
private val holder: SurfaceHolder = holder.apply {
addCallback(this@Preview)
setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
}
override fun surfaceCreated(holder: SurfaceHolder) {
// Surface 已创建,告诉相机在哪里绘制预览
camera.apply {
try {
setPreviewDisplay(holder)
startPreview()
} catch (e: IOException) {
Log.d(TAG, "设置相机预览失败", e)
}
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// 在活动中处理相机预览的释放
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {
// 如果预览可以更改或旋转,请在此处理这些事件。在更改之前确保停止预览
if (holder.surface == null) {
return // 预览 Surface 不存在
}
// 在进行更改之前停止预览
try {
camera.stopPreview()
} catch (e: Exception) {
// 尝试停止不存在的预览;无需执行任何操作
}
// 设置预览大小并进行调整
// 以新设置启动预览
camera.apply {
try {
setPreviewDisplay(holder)
startPreview()
} catch (e: Exception) {
Log.d(TAG, "启动相机预览失败", e)
}
}
}
}
class CameraActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
private var camera: Camera? = null
private var preview: Preview? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// 创建 Camera 实例
camera = getCameraInstance()
preview = camera?.let {
// 创建 Preview 视图
Preview(this, it)
}
// 将 Preview 视图设置为活动的内容
val cameraPreview: FrameLayout = viewBinding.cameraPreview
cameraPreview.addView(preview)
}
}
CameraX: CameraController
如果使用 CameraController
,则还必须使用 PreviewView
,这意味着隐式使用了 Preview
UseCase
,从而减少了设置工作。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// CameraX: 使用 CameraController 设置相机预览
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// 创建 CameraController 并将其设置到 PreviewView
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
val previewView: PreviewView = viewBinding.cameraPreview
previewView.controller = cameraController
}
}
CameraX: CameraProvider
使用 CameraX 的 CameraProvider
时,无需使用 PreviewView
,但它仍大大简化了预览设置。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// CameraX: 使用 CameraProvider 设置相机预览
private suspend fun startCamera() {
val cameraProvider = ProcessCameraProvider.getInstance(this).await()
// 创建 Preview UseCase
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(
viewBinding.viewFinder.surfaceProvider
)
}
// 选择默认后置相机
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解绑 UseCases 以重新绑定
cameraProvider.unbindAll()
// 将 UseCases 绑定到相机。此函数返回一个相机对象,可用于执行变焦、闪光灯和对焦等操作
var camera = cameraProvider.bindToLifecycle(
this, cameraSelector, useCases)
} catch(exc: Exception) {
Log.e(TAG, "UseCase 绑定失败", exc)
}
}
轻触对焦
当屏幕上有相机预览时,轻触预览设置对焦点是一种常见的控制方式。
Camera1
在 Camera1 中,必须计算最佳对焦 Area
以指示相机应尝试对焦的位置。此 Area
传递给 setFocusAreas()
。此外,必须在相机上设置兼容的对焦模式。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Camera1: 实现轻触对焦
class TapToFocusHandler : Camera.AutoFocusCallback {
private fun handleFocus(event: MotionEvent) {
val camera = camera ?: return
val parameters = try {
camera.parameters
} catch (e: RuntimeException) {
return
}
// 取消之前的自动对焦功能(如果有)
camera.cancelAutoFocus()
// 创建对焦 Area
val rect = calculateFocusAreaCoordinates(event.x, event.y)
val weight = 1 // 由于只有一个 Area,所以权重值不重要
val focusArea = Camera.Area(rect, weight)
// 设置对焦参数
parameters.focusMode = Parameters.FOCUS_MODE_AUTO
parameters.focusAreas = listOf(focusArea)
// 将参数设置回相机并启动自动对焦
camera.parameters = parameters
camera.autoFocus(this)
}
private fun calculateFocusAreaCoordinates(x: Int, y: Int): Rect {
// 定义要返回的 Area 的大小。此值应针对应用进行优化
val focusAreaSize = 100
// 必须定义函数将 x 和 y 值旋转并缩放到 0 到 1 之间,其中 (0, 0) 是预览的左上角,(1, 1) 是预览的右下角
val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000
// 计算要返回的 Rect 的左、上、右、下值。如果 Rect 超出允许的范围(-1000, -1000, 1000, 1000),则裁剪值以使其在边界内
val left = max(normalizedX - focusAreaSize / 2, -1000)
val top = max(normalizedY - focusAreaSize / 2, -1000)
val right = min(left + focusAreaSize, 1000)
val bottom = min(top + focusAreaSize, 1000)
return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
}
override fun onAutoFocus(focused: Boolean, camera: Camera) {
if (!focused) {
Log.d(TAG, "轻触对焦失败")
}
}
}
CameraX: CameraController
CameraController
监听 PreviewView
的触摸事件以自动处理轻触对焦。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// CameraX: 跟踪 PreviewView 生命周期内的轻触对焦状态
val tapToFocusStateObserver = Observer { state ->
when (state) {
CameraController.TAP_TO_FOCUS_NOT_STARTED ->
Log.d(TAG, "轻触对焦初始化")
CameraController.TAP_TO_FOCUS_STARTED ->
Log.d(TAG, "轻触对焦开始")
CameraController.TAP_TO_FOCUS_FOCUSED ->
Log.d(TAG, "轻触对焦完成(对焦成功)")
CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
Log.d(TAG, "轻触对焦完成(对焦不成功)")
CameraController.TAP_TO_FOCUS_FAILED ->
Log.d(TAG, "轻触对焦失败")
}
}
cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)
CameraX: CameraProvider
使用 CameraProvider
时,需要一些设置才能使轻触对焦工作。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// CameraX: 使用 CameraProvider 实现轻触对焦
val gestureDetector = GestureDetectorCompat(context,
object : SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
val previewView = previewView ?: return false
val camera = camera ?: return false
val meteringPointFactory = previewView.meteringPointFactory
val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
val meteringAction = FocusMeteringAction.Builder(focusPoint).build()
lifecycleScope.launch {
val focusResult = camera.cameraControl.startFocusAndMetering(meteringAction).await()
if (!focusResult.isFocusSuccessful()) {
Log.d(TAG, "轻触对焦失败")
}
}
return true
}
}
)
// 在 PreviewView 的触摸监听器中设置手势检测器
previewView.setOnTouchListener { _, event ->
var didConsume = scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress) {
didConsume = gestureDetector.onTouchEvent(event)
}
didConsume
}
捏合缩放
实现预览的缩放是常见的直接操作之一。
Camera1
使用 Camera1 有两种缩放方式:Camera.startSmoothZoom()
和 Camera.Parameters.setZoom()
。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Camera1: 实现捏合缩放
val scaleGestureDetector = ScaleGestureDetector(context,
object : ScaleGestureDetector.OnScaleGestureListener {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val camera = camera ?: return false
val parameters = try {
camera.parameters
} catch (e: RuntimeException) {
return false
}
// 如果有任何对焦正在进行,则停止对焦
camera.cancelAutoFocus()
// 在 Camera.Parameters 上设置缩放级别,并将参数设置回相机
val currentZoom = parameters.zoom
parameters.zoom = (detector.scaleFactor * currentZoom).toInt()
camera.parameters = parameters
return true
}
}
)
class ZoomTouchListener : View.OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean =
scaleGestureDetector.onTouchEvent(event)
}
// 如果当前相机支持缩放,则为预览视图设置 ZoomTouchListener 以处理触摸事件
if (camera.parameters.isZoomSupported) {
view.setOnTouchListener(ZoomTouchListener())
}
CameraX: CameraController
类似于轻触对焦,CameraController
监听 PreviewView
的触摸事件以自动处理捏合缩放。
示例代码
1
2
3
4
5
6
7
// CameraX: 跟踪 PreviewView 生命周期内的捏合缩放状态
val pinchToZoomStateObserver = Observer { state ->
val zoomRatio = state.zoomRatio
Log.d(TAG, "捏合缩放比例 $zoomRatio")
}
cameraController.zoomState.observe(this, pinchToZoomStateObserver)
CameraX: CameraProvider
要使 CameraProvider
的捏合缩放工作,需要进行一些设置。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// CameraX: 使用 CameraProvider 实现捏合缩放
val scaleGestureDetector = ScaleGestureDetector(context,
object : SimpleOnGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val camera = camera ?: return false
val zoomState = camera.cameraInfo.zoomState
val currentZoomRatio = zoomState.value?.zoomRatio ?: 1f
camera.cameraControl.setZoomRatio(detector.scaleFactor * currentZoomRatio)
return true
}
}
)
// 在 PreviewView 的触摸监听器中设置缩放手势检测器
previewView.setOnTouchListener { _, event ->
var didConsume = scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress) {
didConsume = gestureDetector.onTouchEvent(event)
}
didConsume
}
拍照
此部分介绍如何触发拍照,无论是在按下快门按钮、计时器到期还是其他事件时。
Camera1
在 Camera1 中,首先定义一个 Camera.PictureCallback
来在请求图片数据时进行处理。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Camera1: 定义一个处理 JPEG 数据的 Camera.PictureCallback
private val picture = Camera.PictureCallback { data, _ ->
val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
Log.d(TAG, "创建媒体文件失败,请检查存储权限")
return@PictureCallback
}
try {
val fos = FileOutputStream(pictureFile)
fos.write(data)
fos.close()
} catch (e: FileNotFoundException) {
Log.d(TAG, "文件未找到", e)
} catch (e: IOException) {
Log.d(TAG, "访问文件失败", e)
}
}
然后,调用相机实例上的 takePicture()
方法。
示例代码
1
2
// Camera1: 调用相机实例上的 takePicture() 方法,并传入我们的 PictureCallback
camera?.takePicture(null, null, picture)
CameraX: CameraController
CameraX 的 CameraController
通过实现自己的 takePicture()
方法,简化了图像捕获的流程。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// CameraX: 定义一个使用 CameraController 拍照的函数
private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private fun takePhoto() {
// 创建时间戳名称和 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")
}
}
// 创建输出选项对象,包含文件和元数据
val outputOptions = ImageCapture.OutputFileOptions.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
).build()
// 设置图像捕获监听器,在拍照后触发
cameraController.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(e: ImageCaptureException) {
Log.e(TAG, "拍照失败", e)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val msg = "拍照成功:${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
}
)
}
CameraX: CameraProvider
使用 CameraProvider
拍照的方式与使用 CameraController
几乎完全相同,但需要先创建并绑定一个 ImageCapture
UseCase
,以便有一个可以调用 takePicture()
的对象。
示例代码
1
2
3
4
5
6
7
// CameraX: 创建并绑定一个 ImageCapture UseCase
private var imageCapture: ImageCapture? = null
imageCapture = ImageCapture.Builder().build()
var camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
然后,调用 ImageCapture.takePicture()
。
示例代码
1
2
3
4
5
// CameraX: 定义一个使用 CameraController 拍照的函数
private fun takePhoto() {
val imageCapture = imageCapture ?: return
imageCapture.takePicture(...)
}
录制视频
录制视频比之前介绍的场景复杂得多。CameraX 同样为你处理了大部分复杂性。
Camera1
使用 Camera1 进行视频录制需要仔细管理相机和 MediaRecorder,并且方法调用必须按照特定顺序进行。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Camera1: 设置 MediaRecorder 并定义开始和停止视频录制的函数
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false
private fun prepareMediaRecorder(): Boolean {
mediaRecorder = MediaRecorder()
// 解锁相机并将其设置给 MediaRecorder
camera?.unlock()
mediaRecorder?.run {
setCamera(camera)
// 设置音频和视频源
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
setVideoSource(MediaRecorder.VideoSource.CAMERA)
// 设置 CamcorderProfile(需要 API 级别 8 或更高)
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))
// 设置输出文件
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
// 设置预览输出
setPreviewDisplay(preview?.holder?.surface)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
// 准备配置的 MediaRecorder
return try {
prepare()
true
} catch (e: IllegalStateException) {
Log.d(TAG, "准备 MediaRecorder 失败", e)
releaseMediaRecorder()
false
} catch (e: IOException) {
Log.d(TAG, "设置 MediaRecorder 文件失败", e)
releaseMediaRecorder()
false
}
}
return false
}
private fun releaseMediaRecorder() {
mediaRecorder?.reset()
mediaRecorder?.release()
mediaRecorder = null
camera?.lock()
}
private fun startStopVideo() {
if (isRecording) {
// 停止录制并释放相机
mediaRecorder?.stop()
releaseMediaRecorder()
camera?.lock()
isRecording = false
// 这里是通知用户视频录制已停止的好地方
} else {
// 初始化视频相机
if (prepareVideoRecorder()) {
// 相机可用且已解锁,MediaRecorder 已准备好,现在可以开始录制
mediaRecorder?.start()
isRecording = true
// 这里是通知用户录制已开始的好地方
} else {
// 准备失败,释放相机
releaseMediaRecorder()
// 这里通知用户
}
}
}
CameraX: CameraController
使用 CameraX 的 CameraController
,可以独立切换 ImageCapture
、VideoCapture
和 ImageAnalysis
UseCase
,只要这些用例可以同时使用。默认情况下启用了 ImageCapture
和 ImageAnalysis
UseCase
,这就是为什么拍照时不需要调用 setEnabledUseCases()
。
要使用 CameraController
进行视频录制,需要先调用 setEnabledUseCases()
启用 VideoCapture
UseCase
。
示例代码
1
2
// CameraX: 在 CameraController 上启用 VideoCapture UseCase
cameraController.setEnabledUseCases(VIDEO_CAPTURE)
当需要开始录制视频时,可以调用 CameraController.startRecording()
函数。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// CameraX: 使用 CameraController 实现视频捕获
private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
class VideoSaveCallback : OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: OutputFileResults) {
val msg = "视频捕获成功:${outputFileResults.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
Log.d(TAG, "保存视频失败:$message", cause)
}
}
private fun startStopVideo() {
if (cameraController.isRecording()) {
// 停止当前录制会话
cameraController.stopRecording()
return
}
// 定义保存视频的文件选项
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
val outputFileOptions = OutputFileOptions.Builder(File(this.filesDir, name)).build()
// 调用 CameraController 的 startRecording 方法
cameraController.startRecording(outputFileOptions, ContextCompat.getMainExecutor(this), VideoSaveCallback())
}
CameraX: CameraProvider
如果使用 CameraProvider
,则需要创建一个 VideoCapture
UseCase
并传入一个 Recorder
对象。
示例代码
1
2
3
4
5
6
7
8
9
10
// CameraX: 创建并绑定一个 VideoCapture UseCase
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.FHD))
.build()
videoCapture = VideoCapture.withOutput(recorder)
var camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)
此时,可以通过 videoCapture.output
属性访问 Recorder
。Recorder
可以启动视频录制,这些视频可以保存到 File
、ParcelFileDescriptor
或 MediaStore
。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// CameraX: 使用 CameraProvider 实现视频捕获
private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private fun startStopVideo() {
val videoCapture = this.videoCapture ?: return
if (recording != null) {
// 停止当前录制会话
recording?.stop()
recording = null
return
}
// 创建并启动新的录制会话
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)
.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 = "视频捕获成功:${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
} else {
recording?.close()
recording = null
Log.e(TAG, "视频捕获失败", recordEvent.error)
}
viewBinding.videoCaptureButton.apply {
text = getString(R.string.start_capture)
isEnabled = true
}
}
}
}
}
其他资源
我们有多个完整的 CameraX 应用在 Camera Samples GitHub Repository 中。这些示例展示了本指南中的场景如何融入一个完整的 Android 应用。
如果你在迁移到 CameraX 的过程中需要更多支持,或者有关 Android 相机 API 的问题,请在 CameraX Discussion Group 上与我们联系。
本文转自微信公众号
关键帧Keyframe
,推荐您关注来获取音视频、AI 领域的最新技术和产品信息: