Android Camera各个API录像实践 Androi

Android Camera各个API录像实践

前言

上次刚写了《Android相机各个API拍照实践》,实际上用Camera1、Camera2、CameraX三个API的录像功能,我这也写好了,和拍照类似,但是比拍照坑更多,花了我挺多时间调试的,下面记录下。

目标

还是和上一篇文章一样,先明确下目标:

  1. 能够使用Android三种API预览、录像、播放结果
  2. 能够在录像过程中对视频进行放大,类似微信录小视频
  3. 三种API能够自由切换,互不干扰
  4. 能够拿到拍照结果,并保存到系统录像位置

上一篇文章的代码里面还加上了系统拍照、系统选取、系统分享、保存相册等,这里就不重复了,只要保存到DCIM就行了。

效果图

这里还是可以先搞张效果图看下的,我觉得最后弄的还凑合:

ezgif-4-2e54ba82c4.gif
接口封装


本来想把拍照和录像写到同一个接口的,后面发现预览还是有一些差别的,就另外建了个接口:

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
kotlin复制代码import androidx.activity.ComponentActivity
import androidx.core.util.Consumer

interface ICameraVideoHelper<in T> {

/**
* 使用相机API开始预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
*/
fun startPreview(
activity: ComponentActivity,
view: T
)

/**
* 使用相机API 拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
* @param callback 结果回调
*/
fun startRecord(
activity: ComponentActivity,
view: T,
callback: Consumer<String>
)

/**
* 缩放
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param zoom 缩放倍数
*/
fun zoom(
activity: ComponentActivity,
zoom: Float
)

/**
* 结束视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param callback 结果回调
*/
fun stopRecord(
activity: ComponentActivity,
callback: Consumer<String>
)

/**
* 释放资源
*/
fun release()
}

startPreview和release还是一样,不过takePhoto换成了另外三个方法,startRecord开始录像,stopRecord停止录像并获取结果,zoom能够在录像过程中放大视频。

自定义相关View

录制按钮

要想录像,首先还是要有个像样的按钮,我瞄了一样微信录像的按钮,自己也搞了个,主要有下面点功能:

  1. 能够触发开始录像、放大、停止录像三种事件
  2. 开始录像时有过场动画
  3. 按钮可以移动,移动距离和放大倍数相关联,页面显示放大倍数
  4. 能够记录录像时间,在外圈更新进度
  5. 有最大录制时长,到达该值时停止录制

然后,我就写了下面一个自定义的按钮,用来录像,效果如上面的Gif图:

RecordButton

因为本篇文章的重点不上这个按钮,这里就不详细介绍了,不是很复杂,代码里面注释也挺清楚。

类似ViewPager的带缩放的容器

为了能够在一个页面能够显示三个录像功能,我这又把RecordButton放到了一个能缩放的容器里面,也是根据我之前自定义View事件的控件改造的:

《自定义view实战(7):大小自动变换的类ViewPager》

稍微对这个控件改造了下,去掉了很多东西,只保留这个缩放功能,控件源码如下:

ScrollViewLayout

当然这个控件也是锦上添花罢了,我们这篇文章的重点是用三种API去录像。

使用Camera1 API

搞定录像按钮的问题,我们就能来编写三种API的代码了。

Camera1预览

首先是Camera1的预览:

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
kotlin复制代码/**
* 使用Camera API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 SurfaceView
*/
override fun startPreview(
activity: ComponentActivity,
view: SurfaceView
) {
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、获取后置摄像头ID, 默认 Camera.CameraInfo.CAMERA_FACING_BACK
val cameraId = getCameraId(mFacingType)

// 2、获取相机实例
if (mCamera == null) {
mCamera = Camera.open(cameraId)
}

// 3、设置和屏幕方向一致
setCameraDisplayOrientation(activity, mCamera!!, cameraId)

// 4、设置相机参数
setCameraParameters(mCamera!!)

// 5、在startPreview前设置holder(有前提: surfaceCreated已完成)
// 不要在surfaceCreated设置,不然有问题,使用工具类没法收到surfaceCreated回调
mCamera!!.setPreviewDisplay(view.holder)

// 6、设置SurfaceHolder回调
view.holder.addCallback(mSurfaceCallback)

// 7、开始预览
mCamera!!.startPreview()
}
}

和拍照一模一样,按顺序执行这七步就行,具体代码后面给出。同样,因为是工具类mSurfaceCallback的surfaceCreated收不到,默认已经created了,可以直接open Camera。

Camera1录像

Camera1的录像需要用到MediaRecorder,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
kotlin复制代码/**
* 使用相机API拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 使用 SurfaceView 拍视频
* @param callback 结果回调
*/
override fun startRecord(
activity: ComponentActivity,
view: SurfaceView,
callback: Consumer<String>
){
// 创建一个 MediaRecorder 对象,或者重置
if (mMediaRecorder == null) {
mMediaRecorder = MediaRecorder()
}

// 释放相机资源,给mMediaRecorder使用
mCamera?.unlock()

// 设定参数
mMediaRecorder!!.apply {

// 绑定相机
setCamera(mCamera)

// 设置预览画面
setPreviewDisplay(view.holder.surface)

// 设置方向
setOrientationHint(
getCameraDisplayOrientation(activity, getCameraId(mFacingType)))

// 设置视频参数
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
setVideoSource(MediaRecorder.VideoSource.CAMERA)

// 设置尺寸,注意两者顺序
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setVideoSize(mPreviewSize.width, mPreviewSize.height)

setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)

// 获取临时视频路径
mTempPath = getTempVideoPath(activity).absolutePath

// 设置输出文件路径
setOutputFile(mTempPath)

// 准备 MediaRecorder
prepare()

// 开始录制
start()

// 传出路径
callback.accept(mTempPath)
}
}

实际就是创建一个mMediaRecorder对象,设置好相关参数,调用start进行录制就可以。

这里代码看起来简单,实际有好多坑,下面一个一个讲。

首先是mCamera的unlock一定要调用,而且在stopRecord的时候还要调用lock方法,目的就是释放相机资源,给mMediaRecorder使用。

其次就是这里的参数设置是有顺序的,不要乱改顺序,当然读者也可以改下顺序,看下哪些会有问题。

在一个就是setVideoSize方法一定要传入合适的尺寸,不然会出问题,可能就是prepare抛出异常,造成闪退,我这没try-catch prepare方法,因为prepare失败了也没法用啊。

Camera1缩放

Camera1的缩放比较简单,拿到mCamera的params进行修改就行了,重新设置即生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码override fun zoom(
activity: ComponentActivity,
zoom: Float
){
val params = mCamera!!.parameters

// 检查设备是否支持变焦
if (!params.isZoomSupported) {
// 处理不支持的情况,例如提示用户或忽略请求
return
}

// 确保缩放级别在允许的范围内
val maxZoom = params.maxZoom
val zoomLevel = kotlin.math.min(maxZoom, 1 + (zoom * (maxZoom - 1)).toInt())

// 设置新的缩放级别
params.zoom = zoomLevel

// 应用新的参数到相机
mCamera!!.parameters = params
}

Camera1停止录像

停止录像调用mMediaRecorder的stop方法即可,不过这里还要reset一下,不然无法继续拍照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
mMediaRecorder?.let {
var isTooShort = false
try {
// 时间过短无法stop
it.stop()
}catch (e: Exception) {
isTooShort = true
e.printStackTrace()
}

it.reset()
mCamera?.lock()
continuePreview()
callback.accept(if (isTooShort) "" else mTempPath)
}
}

mCamera的lock上面有提到,Camera1拍完照之后不会再预览了,需要手动调用下。

这里有个坑就是,录制时间过短的话stop会失败,造成闪退,找了很久也没找到这个最短时间,我就不如catch这个异常,直接传出去算了。

Camera1释放资源

加了一个mMediaRecorder,需要注意它的释放。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码/**
* 释放资源
*/
override fun release() {
mCamera?.stopPreview()
mCamera?.release()
mMediaRecorder?.release()
mCamera = null
mMediaRecorder = null
}

完整代码

Camera1VideoHelper

使用Camera2 API

使用Camera1进行录像相对来说还是比较简单的,只不过就是加了一个MediaRecorder,到了Camera2感觉就头疼了,下面看下吧。

这里先说一下啊,我这用的Camera2 API录像可能不是最佳选择,下面我把预览和拍照分成了两个独立的Session,实际是可以写成一个的,只不过会把预览和录像搞在一起,看需要吧。

Camera2预览

前面说了,我把预览和拍照分成了两个独立的Session,所以这里Camera2预览就仅仅需要预览罢了,下面看代码:

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
kotlin复制代码/**
* 使用Camera2 API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API2使用的 TextureView(当然也能用SurfaceView)
*/
override fun startPreview(
activity: ComponentActivity,
view: TextureView
) {
// 持有TextureView的弱引用,便于释放资源
mTextureViewRef = WeakReference(view)

// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {

// 1、获取CameraManager
val cameraManager = ContextCompat.getSystemService(activity, CameraManager::class.java)
?: throw IllegalStateException("get cameraManager fail")

// 2、获取摄像头mCameraId、摄像头信息mCameraCharacteristics
chooseCameraIdByFacing(mFacingType, cameraManager)

// 3. 获取预览和录像的尺寸(!!!N多错误都是尺寸造成的)
getSizes()

// 4、开启相机
mCameraDevice = openCamera(cameraManager)

// 5.创建预览Session
val surface = getSurface(view)
mPreviewSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface
), mCameraDevice!!)

// 6.设置textureView回调,destroy时释放资源
view.surfaceTextureListener = mTextureViewCallback

// 7.开始预览,预览和拍照都用request实现
preview(surface)
}
}

和拍照的预览相对比,去掉了一个ImageReader的配置,然后就是着重写了下尺寸的获取:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码private fun getSizes() {
// 获取尺寸
mCameraCharacteristics?.let { info ->
val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

// !!!再次注意width比height更大,不然选不到对的size
mVideoSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
mPreviewSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
}
}

这个很重要,这里一定要选对尺寸,不然后面MediaRecorder的prepare就是过不去。其他很好理解,使用可以看下完整代码。

Camera2录像

Camera2录像和Camera1的录像比起来就复杂了,先看下代码,我尽量把要注意的细节说一下:

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
kotlin复制代码/**
* 使用相机API拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 使用 TextureView 拍视频
* @param callback 结果回调
*/
override fun startRecord(
activity: ComponentActivity,
view: TextureView,
callback: Consumer<String>
){
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.Main) {

// 关闭预览对话
closePreviewSession()

// 设置MediaRecorder
setUpMediaRecorder(activity, callback)

// 创建
mRecordRequestBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)

// 设置预览输出
val previewSurface = getSurface(view)
mRecordRequestBuilder!!.addTarget(previewSurface)

// 设置录像输出
val recorderSurface = mMediaRecorder!!.surface
mRecordRequestBuilder!!.addTarget(recorderSurface)

// 创建新的预览对话,能将视频输出到录像surface
mPreviewSession = startCaptureSession(mutableListOf(
previewSurface, recorderSurface
), mCameraDevice!!)

// 录像请求
mRecordRequestBuilder!!.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
mPreviewSession!!.setRepeatingRequest(mRecordRequestBuilder!!.build(), null, mHandler)

// 启动录制
mMediaRecorder!!.start()
}
}

预览Session切换

首先,这里要先把预览的session给关了,创建一个带预览和录像的session进行录像,我这都用mPreviewSession去保存,但要注意下这里有两个session:

1
2
3
4
kotlin复制代码private fun closePreviewSession() {
mPreviewSession?.close()
mPreviewSession = null
}

下面是创建新的Session:

1
2
3
4
kotlin复制代码// 创建新的预览对话,能将视频输出到录像surface
mPreviewSession = startCaptureSession(mutableListOf(
previewSurface, recorderSurface
), mCameraDevice!!)

MediaRecorder配置

其次,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
kotlin复制代码private fun setUpMediaRecorder(activity: ComponentActivity, callback: Consumer<String>) {
// 创建一个 MediaRecorder 对象,或者重置
if (mMediaRecorder == null) {
mMediaRecorder = MediaRecorder()
}

// 设定参数
mMediaRecorder!!.apply {

// 设置视频参数(!!!注意视频不是从Camera来了)
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)

// 设置输出文件路径
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mTempPath = getTempVideoPath(activity).absolutePath
setOutputFile(mTempPath)

// 设置比特率和帧率
setVideoEncodingBitRate(100000000)
setVideoFrameRate(30)

// 设置尺寸,注意两者顺序
setVideoSize(mVideoSize!!.width, mVideoSize!!.height)

setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)

// !!!不能掉了,不然prepare不会成功
setOrientationHint(90)

// 准备 MediaRecorder
prepare()

// 传出路径
callback.accept(mTempPath)
}
}

这里MediaRecorder设置setVideoSource为MediaRecorder.VideoSource.SURFACE后,它自己就带了一个surface,我们要通过MediaRecorder的getSurface,向里面传递数据。

这里的参数顺序也要注意下,最最最坑的就是这个尺寸了,这里不能和拍照一样使用最大尺寸,使用的话就闪退,后面我重写了getSizes方法,才让prepare生效,实际就是(width=1920, height=1080),不过还是要根据机型决定:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码private fun getSizes() {
// 获取尺寸
mCameraCharacteristics?.let { info ->
val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

// !!!再次注意width比height更大,不然选不到对的size
mVideoSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
mPreviewSize = getOptimalPreviewSize(
map.getOutputSizes(MediaRecorder::class.java).toList(), 1920, 1080)
}
}

如果发生各种异常,代码又觉得没错,那估计就是你的尺寸出错了,很坑。

录像请求

上面切换Session后,要注意把预览和录像的surface传进去,这里是两个surface了:

1
2
3
4
5
6
7
kotlin复制代码// 设置预览输出
val previewSurface = getSurface(view)
mRecordRequestBuilder!!.addTarget(previewSurface)

// 设置录像输出
val recorderSurface = mMediaRecorder!!.surface
mRecordRequestBuilder!!.addTarget(recorderSurface)

切换session并发送请求后,就可以路线了:

1
2
3
4
5
6
7
kotlin复制代码// 录像请求
mRecordRequestBuilder!!.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
mPreviewSession!!.setRepeatingRequest(mRecordRequestBuilder!!.build(), null, mHandler)

// 启动录制
mMediaRecorder!!.start()

总而言之,Camera2的录像比较复杂。

Camera2缩放

看完Camera2的录像是不是觉得头痛,只可惜Camera2缩放也是让人头痛,下面看代码:

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
kotlin复制代码override fun zoom(
activity: ComponentActivity,
zoom: Float
){
// 缩放实际是通过修改Rect实现的
val zoomRect = calculateZoomRect(zoom)
// 这两行代码只是为了防止重复请求
mPreviewSession?.stopRepeating()
// 创建请求修改
mRecordRequestBuilder!!.set(CaptureRequest.SCALER_CROP_REGION, zoomRect)
mPreviewSession?.setRepeatingRequest(mRecordRequestBuilder!!.build(), null, mHandler)

}

private fun calculateZoomRect(zoomLevel: Float): Rect {
val sensorRect = mCameraCharacteristics!!.get(
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!

val minZoom = 1.0f
val maxZoom = mCameraCharacteristics!!.get(
CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)!!
val currentZoom = minZoom + (maxZoom - minZoom) * zoomLevel

val centerX = sensorRect.width() / 2
val centerY = sensorRect.height() / 2
val deltaX = (0.5f * sensorRect.width() / currentZoom).toInt()
val deltaY = (0.5f * sensorRect.height() / currentZoom).toInt()

val zoomRect = Rect()
zoomRect.left = centerX - deltaX
zoomRect.right = centerX + deltaX
zoomRect.top = centerY - deltaY
zoomRect.bottom = centerY + deltaY

return zoomRect
}

真不知道谁设计的这功能,缩放居然是通过修改Rect实现的,这里需要我们计算缩放的Rect,好在我用GPT帮我写的。

这里我通过持有mRecordRequestBuilder,并修改了参数,再次对mPreviewSession发起请求,算是有用了。

Camera2停止录像

Camera2停止录像和拍照的差不多,只不过拍照后需要重新预览,这里不做处理,在使用的地方调用吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
mMediaRecorder?.let {
var isTooShort = false
try {
// 时间过短无法stop
it.stop()
}catch (e: Exception) {
isTooShort = true
e.printStackTrace()
}

it.reset()

// 重新预览(外部去操作吧,这里不动了)
// startPreview(activity, mTextureViewRef!!.get()!!)

callback.accept(if (isTooShort) "" else mTempPath)
}
}

Camera2释放资源

记得把mMediaRecorder和持有的mRecordRequestBuilder释放了。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码override fun release() {
// 从 SurfaceTexture 中移除 SurfaceTextureListener
mTextureViewRef?.get()?.surfaceTextureListener = null
// 需要关闭这三个
mCameraDevice?.close()
mPreviewSession?.close()
mMediaRecorder?.release()
mMediaRecorder = null
mRecordRequestBuilder = null
mHandler.removeCallbacksAndMessages(null)
}

完整代码

Camera2VideoHelper

使用CameraX API

引引入CameraX库

这里和拍照类似,暂且列一下吧,这用的version catalog管理依赖,实际都差不多:

1
2
3
4
5
6
7
8
9
10
toml复制代码# cameraX
camerax = "1.1.0-beta01"

# cameraX
camerax = { module = "androidx.camera:camera-core", version.ref = "camerax"}
camerax_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax"}
camerax_video = { module = "androidx.camera:camera-video", version.ref = "camerax"}
camerax_lifecycler = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax"}
camerax_view = { module = "androidx.camera:camera-view", version.ref = "camerax"}
camerax_extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax"}

在dependence里面添加上依赖,就能开始写代码了。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码dependencies {
//。。。

// CameraX 相关依赖
implementation(libs.camerax)
implementation(libs.camerax.camera2)
implementation(libs.camerax.lifecycler)
implementation(libs.camerax.video)
implementation(libs.camerax.view)
implementation(libs.camerax.extensions)
}

CameraX预览

需要注意下,录像和前面不一样,CameraX提供了录像功能,只要使用videoCapture便可以,下面是代码:

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
kotlin复制代码/**
* 使用CameraX API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 PreviewView
*/
@SuppressLint("RestrictedApi")
override fun startPreview(
activity: ComponentActivity,
view: PreviewView
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
// 用于将相机的生命周期绑定到生命周期所有者
// 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
mCameraProvider = cameraProviderFuture.get()

// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(view.surfaceProvider)
}

// 录像的使用场景
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)

// 选择摄像头,省去了去判断摄像头ID
val cameraSelector = mSelector

try {
// Unbind use cases before rebinding
mCameraProvider!!.unbindAll()

// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCamera = mCameraProvider!!.bindToLifecycle(
activity, cameraSelector, preview, videoCapture)

} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}

// 回调代码在主线程处理
}, ContextCompat.getMainExecutor(activity))
}

需要注意的是videoCapture别和ImageCapture一样用Builder创建,会提示报错,如果强行使用的话会很卡很卡!按网上说的,这里会把预览和录像的surface叠加,导致卡顿,虽然很多博客都是通过Builder创建的,实际上用法是错的。。。

我们需要通过recorder去创建videoCapture,来实现录像功能。

CameraX录像

CameraX录像稍微复杂一些,网上大部分博文都是通过videoCapture去startRecord,其实不对,正确用法应该如下:

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
kotlin复制代码/**
* 使用相机API拍视频
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 使用 PreviewView 拍视频
* @param callback 结果回调
*/
@SuppressLint("MissingPermission")
override fun startRecord(
activity: ComponentActivity,
view: PreviewView,
callback: Consumer<String>
){
// 视频文件
val videoFile = getTempVideoPath(activity)
mTempPath = videoFile.absolutePath
val outputFileOptions = FileOutputOptions.Builder(videoFile).build()

// 录像(直接用videoCapture的写法预览会卡顿)
mRecording = videoCapture!!.output
.prepareRecording(activity, outputFileOptions)
.apply {
withAudioEnabled()
}
.start(ContextCompat.getMainExecutor(activity)) { recordEvent ->
when(recordEvent) {
is VideoRecordEvent.Start -> {}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
mRecordEndCallback?.accept(mTempPath)
} else {
// 录制失败
mRecording?.close()
mRecording = null
mRecordEndCallback?.accept("")
}
}
}
}

// 传出地址
callback.accept(mTempPath)
}

这里有个坑,因为我们需要在stopRecord中拿到回调的视频路径,但是这个视频完成录制是异步的,只能在startRecord里面的代码监听,所以我这加了个mRecordEndCallback来传递结果,具体代码要结合后面stopRecord来看。

CameraX缩放

CameraX的缩放需要通过mCamera的cameraControl去设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码override fun zoom(
activity: ComponentActivity,
zoom: Float
){
mCamera?.let {
// 先获取最大缩放级别
val zoomState = it.cameraInfo.zoomState
val maxZoomRatio = zoomState.value?.maxZoomRatio ?: 1.0f
// 设置缩放级别
val zoomLevel = 1 + (zoom * (maxZoomRatio - 1))
it.cameraControl.setZoomRatio(zoomLevel)
}
}

注意下,这个mCamera是在bindToLifecycle时的返回值,我们在拍照的时候并未用到:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码try {
// Unbind use cases before rebinding
mCameraProvider!!.unbindAll()

// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCamera = mCameraProvider!!.bindToLifecycle(
activity, cameraSelector, preview, videoCapture)

} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}

CameraX停止录像

CameraX停止录像只需要通过mRecording执行stop就行,只是stop是异步的,所以这里需要先保存下callback,再stop:

1
2
3
4
5
kotlin复制代码override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
// 注意stopRecording是个异步方法,先保存下,在startRecording的callback里面触发
mRecordEndCallback = callback
mRecording!!.stop()
}

最后在startRecord代码中的回调中执行,通过“?.”操作符就能在录像完成时触发回调了:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码when(recordEvent) {
is VideoRecordEvent.Start -> {}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
mRecordEndCallback?.accept(mTempPath)
} else {
// 录制失败
mRecording?.close()
mRecording = null
mRecordEndCallback?.accept("")
}
}
}

CameraX释放资源

这里多了一个mCamera需要释放,内部设置的回调mRecordEndCallback也清除下:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码/**
* 释放资源
*/
override fun release() {
// 取消绑定生命周期观察者
mCameraProvider?.unbindAll()
mCameraProvider = null
mRecordEndCallback = null
mCamera = null
}

完整代码

CameraXVideoHelper

使用Demo

使用的demo就是上面gif显示的内容,代码如下:

TakeVideoFragment

小结

这篇文章把Android相机中Camera1、Camera2、CameraX三个API的录像功能实践了下,涉及不深,主要就是使用,记录学习的过程吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%