Android Camera各个API录像实践
前言
上次刚写了《Android相机各个API拍照实践》,实际上用Camera1、Camera2、CameraX三个API的录像功能,我这也写好了,和拍照类似,但是比拍照坑更多,花了我挺多时间调试的,下面记录下。
目标
还是和上一篇文章一样,先明确下目标:
- 能够使用Android三种API预览、录像、播放结果
- 能够在录像过程中对视频进行放大,类似微信录小视频
- 三种API能够自由切换,互不干扰
- 能够拿到拍照结果,并保存到系统录像位置
上一篇文章的代码里面还加上了系统拍照、系统选取、系统分享、保存相册等,这里就不重复了,只要保存到DCIM就行了。
效果图
这里还是可以先搞张效果图看下的,我觉得最后弄的还凑合:
接口封装
本来想把拍照和录像写到同一个接口的,后面发现预览还是有一些差别的,就另外建了个接口:
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
录制按钮
要想录像,首先还是要有个像样的按钮,我瞄了一样微信录像的按钮,自己也搞了个,主要有下面点功能:
- 能够触发开始录像、放大、停止录像三种事件
- 开始录像时有过场动画
- 按钮可以移动,移动距离和放大倍数相关联,页面显示放大倍数
- 能够记录录像时间,在外圈更新进度
- 有最大录制时长,到达该值时停止录制
然后,我就写了下面一个自定义的按钮,用来录像,效果如上面的Gif图:
因为本篇文章的重点不上这个按钮,这里就不详细介绍了,不是很复杂,代码里面注释也挺清楚。
类似ViewPager的带缩放的容器
为了能够在一个页面能够显示三个录像功能,我这又把RecordButton放到了一个能缩放的容器里面,也是根据我之前自定义View事件的控件改造的:
《自定义view实战(7):大小自动变换的类ViewPager》
稍微对这个控件改造了下,去掉了很多东西,只保留这个缩放功能,控件源码如下:
当然这个控件也是锦上添花罢了,我们这篇文章的重点是用三种API去录像。
使用Camera1 API
搞定录像按钮的问题,我们就能来编写三种API的代码了。
Camera1预览
首先是Camera1的预览:
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使用前注意要先预览,下面看下预览代码:
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进行修改就行了,重新设置即生效。
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一下,不然无法继续拍照。
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,需要注意它的释放。
kotlin
/**
* 释放资源
*/
override fun release() {
mCamera?.stopPreview()
mCamera?.release()
mMediaRecorder?.release()
mCamera = null
mMediaRecorder = null
}
完整代码
使用Camera2 API
使用Camera1进行录像相对来说还是比较简单的,只不过就是加了一个MediaRecorder,到了Camera2感觉就头疼了,下面看下吧。
这里先说一下啊,我这用的Camera2 API录像可能不是最佳选择,下面我把预览和拍照分成了两个独立的Session,实际是可以写成一个的,只不过会把预览和录像搞在一起,看需要吧。
Camera2预览
前面说了,我把预览和拍照分成了两个独立的Session,所以这里Camera2预览就仅仅需要预览罢了,下面看代码:
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的配置,然后就是着重写了下尺寸的获取:
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的录像比起来就复杂了,先看下代码,我尽量把要注意的细节说一下:
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:
kotlin
private fun closePreviewSession() {
mPreviewSession?.close()
mPreviewSession = null
}
下面是创建新的Session:
kotlin
// 创建新的预览对话,能将视频输出到录像surface
mPreviewSession = startCaptureSession(mutableListOf(
previewSurface, recorderSurface
), mCameraDevice!!)
MediaRecorder配置
其次,MediaRecorder的配置也比较容易出错:
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),不过还是要根据机型决定:
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了:
kotlin
// 设置预览输出
val previewSurface = getSurface(view)
mRecordRequestBuilder!!.addTarget(previewSurface)
// 设置录像输出
val recorderSurface = mMediaRecorder!!.surface
mRecordRequestBuilder!!.addTarget(recorderSurface)
切换session并发送请求后,就可以路线了:
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缩放也是让人头痛,下面看代码:
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停止录像和拍照的差不多,只不过拍照后需要重新预览,这里不做处理,在使用的地方调用吧。
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释放了。
kotlin
override fun release() {
// 从 SurfaceTexture 中移除 SurfaceTextureListener
mTextureViewRef?.get()?.surfaceTextureListener = null
// 需要关闭这三个
mCameraDevice?.close()
mPreviewSession?.close()
mMediaRecorder?.release()
mMediaRecorder = null
mRecordRequestBuilder = null
mHandler.removeCallbacksAndMessages(null)
}
完整代码
使用CameraX API
引引入CameraX库
这里和拍照类似,暂且列一下吧,这用的version catalog管理依赖,实际都差不多:
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里面添加上依赖,就能开始写代码了。
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便可以,下面是代码:
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,其实不对,正确用法应该如下:
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去设置:
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时的返回值,我们在拍照的时候并未用到:
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:
kotlin
override fun stopRecord(activity: ComponentActivity, callback: Consumer<String>) {
// 注意stopRecording是个异步方法,先保存下,在startRecording的callback里面触发
mRecordEndCallback = callback
mRecording!!.stop()
}
最后在startRecord代码中的回调中执行,通过"?."操作符就能在录像完成时触发回调了:
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也清除下:
kotlin
/**
* 释放资源
*/
override fun release() {
// 取消绑定生命周期观察者
mCameraProvider?.unbindAll()
mCameraProvider = null
mRecordEndCallback = null
mCamera = null
}
完整代码
使用Demo
使用的demo就是上面gif显示的内容,代码如下:
小结
这篇文章把Android相机中Camera1、Camera2、CameraX三个API的录像功能实践了下,涉及不深,主要就是使用,记录学习的过程吧。