Android Camera各个API录像实践

Android Camera各个API录像实践

前言

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

目标

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

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

上一篇文章的代码里面还加上了系统拍照、系统选取、系统分享、保存相册等,这里就不重复了,只要保存到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

录制按钮

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

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

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

RecordButton

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

类似ViewPager的带缩放的容器

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

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

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

ScrollViewLayout

当然这个控件也是锦上添花罢了,我们这篇文章的重点是用三种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
}

完整代码

Camera1VideoHelper

使用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)
}

完整代码

Camera2VideoHelper

使用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
}

完整代码

CameraXVideoHelper

使用Demo

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

TakeVideoFragment

小结

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

相关推荐
找藉口是失败者的习惯14 分钟前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey1 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!3 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟4 小时前
Android音频采集
android·音视频
小白也想学C5 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程5 小时前
初级数据结构——树
android·java·数据结构
闲暇部落7 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX9 小时前
Android 分区相关介绍
android
大白要努力!10 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee11 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip