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的录像功能实践了下,涉及不深,主要就是使用,记录学习的过程吧。

相关推荐
HerayChen3 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野4 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11236 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件31 分钟前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -2 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨3 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸3 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android