十分钟实现 Android Camera2 视频录制

1. 前言

因为工作中要使用Android Camera2 API,但因为Camera2比较复杂,网上资料也比较乱,有一定入门门槛,所以花了几天时间系统研究了下,并在CSDN上记录了下,希望能帮助到更多的小伙伴。
上两篇文章们使用Camera2实现了相机预览和拍照的功能,这篇文章我们接着上文,来实现Camera2视频录制的功能。

2. 前置操作

2.1 声明相机参数和成员变量

首先还是声明相机参数和成员变量,比起前文增加了这些

private var mediaRecorder: MediaRecorder? = null
private var isRecordingVideo: Boolean = false
private val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90
private val SENSOR_ORIENTATION_INVERSE_DEGREES = 270
private val DEFAULT_ORIENTATIONS = SparseIntArray().apply {append(Surface.ROTATION_0, 90)append(Surface.ROTATION_90, 0)append(Surface.ROTATION_180, 270)append(Surface.ROTATION_270, 180)
}
private val INVERSE_ORIENTATIONS = SparseIntArray().apply {append(Surface.ROTATION_0, 270)append(Surface.ROTATION_90, 180)append(Surface.ROTATION_180, 90)append(Surface.ROTATION_270, 0)
}

完整的需要声明的相机参数和成员变量如下

//后摄 : 0 ,前摄 : 1
private val cameraId = "0"
private val TAG = CameraActivity3::class.java.simpleName
private lateinit var cameraDevice: CameraDevice
private val cameraThread = HandlerThread("CameraThread").apply { start() }
private val cameraHandler = Handler(cameraThread.looper)
private val cameraManager: CameraManager by lazy {getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
private val characteristics: CameraCharacteristics by lazy {cameraManager.getCameraCharacteristics(cameraId)
}
private lateinit var session: CameraCaptureSessionprivate lateinit var imageReader: ImageReader//JPEG格式,所有相机必须支持JPEG输出,因此不需要检查
private val pixelFormat = ImageFormat.JPEG//imageReader最大的图片缓存数
private val IMAGE_BUFFER_SIZE: Int = 3//线程池
private val threadPool = Executors.newCachedThreadPool()
private val imageReaderThread = HandlerThread("imageReaderThread").apply { start() }
private val imageReaderHandler = Handler(imageReaderThread.looper)
/** Live data listener for changes in the device orientation relative to the camera */
private lateinit var relativeOrientation: OrientationLiveDataprivate var mediaRecorder: MediaRecorder? = null
private var isRecordingVideo: Boolean = false
private val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90
private val SENSOR_ORIENTATION_INVERSE_DEGREES = 270
private val DEFAULT_ORIENTATIONS = SparseIntArray().apply {append(Surface.ROTATION_0, 90)append(Surface.ROTATION_90, 0)append(Surface.ROTATION_180, 270)append(Surface.ROTATION_270, 180)
}
private val INVERSE_ORIENTATIONS = SparseIntArray().apply {append(Surface.ROTATION_0, 270)append(Surface.ROTATION_90, 180)append(Surface.ROTATION_180, 90)append(Surface.ROTATION_270, 0)
}

2.2 添加布局

首先我们需要在XML中添加两个按钮,分别是录制按钮和停止录制按钮

<Buttonandroid:id="@+id/btn_capture_video"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="bottom|center"android:layout_marginRight="16dp"android:text="录屏"android:visibility="visible"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent" /><Buttonandroid:id="@+id/btn_stop_capture"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="bottom|left"android:text="停止录屏"android:visibility="visible"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent" />

2.3 初始化MediaPlayer

我们需要在打开相机的时候,去初始化mediaPlayer

mediaRecorder = MediaRecorder()

完整代码如下

@SuppressLint("MissingPermission")
private fun openCamera(cameraId: String) {cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {override fun onOpened(camera: CameraDevice) {cameraDevice = cameramediaRecorder = MediaRecorder()startPreview()}override fun onDisconnected(camera: CameraDevice) {this@CameraActivity3.finish()}override fun onError(camera: CameraDevice, error: Int) {Toast.makeText(application, "openCamera Failed:$error", Toast.LENGTH_SHORT).show()}}, cameraHandler)
}

3. 实现视频录制功能

3.1 关闭原本的Session

因为拍照和录制视频功能不好一起使用,所以需要先调用closePreviewSession,来关闭原来的session

private fun closePreviewSession() {session?.close()
}

3.2 给MediaRecorder设置参数

接着,需要调用setUpMediaRecorder()来初始化MediaRecorder
setUpMediaRecorder中,会给mediaRecorder设置很多预置参数

首先获取目标路径

 val nextVideoAbsolutePath = getVideoFilePath(cameraActivity)fun getVideoFilePath(context: Context?): String {val filename = "VIDEO_${System.currentTimeMillis()}.mp4"val dir = context?.getExternalFilesDir("video")return if (dir == null) {filename} else {"${dir.absolutePath}/$filename"}
}

然后设置mediaRecorder方向

val sensorOrientation = characteristics?.get(SENSOR_ORIENTATION)
val rotation = cameraActivity.windowManager.defaultDisplay.rotation
when (sensorOrientation) {SENSOR_ORIENTATION_DEFAULT_DEGREES ->mediaRecorder?.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation))SENSOR_ORIENTATION_INVERSE_DEGREES ->mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation))
}

最后给meidaRecorder设置若干参数项,这里我们默认给视频尺寸设置成了1920*1080,如果你的设备相机不支持这个分辨率,需要修改一下。

mediaRecorder?.apply {setAudioSource(MediaRecorder.AudioSource.MIC)setVideoSource(MediaRecorder.VideoSource.SURFACE)setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)setOutputFile(nextVideoAbsolutePath)setVideoEncodingBitRate(10000000)setVideoFrameRate(30)setVideoSize(1920,1080)setVideoEncoder(MediaRecorder.VideoEncoder.H264)setAudioEncoder(MediaRecorder.AudioEncoder.AAC)prepare()
}

再来看下完整的代码

private fun setUpMediaRecorder() {val cameraActivity = thisval nextVideoAbsolutePath = getVideoFilePath(cameraActivity)val sensorOrientation = characteristics?.get(SENSOR_ORIENTATION)val rotation = cameraActivity.windowManager.defaultDisplay.rotationwhen (sensorOrientation) {SENSOR_ORIENTATION_DEFAULT_DEGREES ->mediaRecorder?.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation))SENSOR_ORIENTATION_INVERSE_DEGREES ->mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation))}mediaRecorder?.apply {setAudioSource(MediaRecorder.AudioSource.MIC)setVideoSource(MediaRecorder.VideoSource.SURFACE)setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)setOutputFile(nextVideoAbsolutePath)setVideoEncodingBitRate(10000000)setVideoFrameRate(30)setVideoSize(1920,1080) //FIXME 如果你的设备相机不支持这个分辨率,需要修改一下setVideoEncoder(MediaRecorder.VideoEncoder.H264)setAudioEncoder(MediaRecorder.AudioEncoder.AAC)prepare()}
}fun getVideoFilePath(context: Context?): String {val filename = "VIDEO_${System.currentTimeMillis()}.mp4"val dir = context?.getExternalFilesDir("video")return if (dir == null) {filename} else {"${dir.absolutePath}/$filename"}
}

3.3 重新创建Session

接着就将binding.surfaceViewrecorderSurface添加到surfaces

val recorderSurface = mediaRecorder!!.surface
val surfaces = ArrayList<Surface>().apply {add(binding.surfaceView.holder.surface)add(recorderSurface)
}

重新调用cameraDevice?.createCaptureSession,将surfaces传入

cameraDevice?.createCaptureSession(surfaces,object : CameraCaptureSession.StateCallback() {override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {//待实现}override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) {Toast.makeText(application, "onConfigureFailed", Toast.LENGTH_SHORT).show()}}, cameraHandler)

3.4 开始录制

onConfigured调用后,我们执行下面这些代码,主要执行了这些操作

  • cameraCaptureSession赋值给session
  • session?.setRepeatingRequest,这将不断地实时发送视频流,直到会话断开或调用session.stoprepeat()
  • 调用mediaRecorder?.start录制视频
session = cameraCaptureSessionval previewRequestBuilder = cameraDevice!!.createCaptureRequest(TEMPLATE_RECORD).apply {addTarget(binding.surfaceView.holder.surface)addTarget(recorderSurface)
}
session?.setRepeatingRequest(previewRequestBuilder!!.build(), null, cameraHandler)isRecordingVideo = true
mediaRecorder?.start()

3.5 录制视频完整代码

binding.btnCaptureVideo.setOnClickListener {startRecordingVideo()
}private fun startRecordingVideo() {
closePreviewSession()
setUpMediaRecorder()val recorderSurface = mediaRecorder!!.surface
val surfaces = ArrayList<Surface>().apply {add(binding.surfaceView.holder.surface)add(recorderSurface)
}cameraDevice?.createCaptureSession(surfaces,object : CameraCaptureSession.StateCallback() {override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {session = cameraCaptureSessionval previewRequestBuilder =cameraDevice!!.createCaptureRequest(TEMPLATE_RECORD).apply {addTarget(binding.surfaceView.holder.surface)addTarget(recorderSurface)}session?.setRepeatingRequest(previewRequestBuilder!!.build(),null,cameraHandler)isRecordingVideo = truemediaRecorder?.start()}override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) {Toast.makeText(application, "onConfigureFailed", Toast.LENGTH_SHORT).show()}}, cameraHandler)
}private fun closePreviewSession() {session?.close()
}private fun setUpMediaRecorder() {val cameraActivity = thisval nextVideoAbsolutePath = getVideoFilePath(cameraActivity)val sensorOrientation = characteristics?.get(SENSOR_ORIENTATION)val rotation = cameraActivity.windowManager.defaultDisplay.rotationwhen (sensorOrientation) {SENSOR_ORIENTATION_DEFAULT_DEGREES ->mediaRecorder?.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation))SENSOR_ORIENTATION_INVERSE_DEGREES ->mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation))}mediaRecorder?.apply {setAudioSource(MediaRecorder.AudioSource.MIC)setVideoSource(MediaRecorder.VideoSource.SURFACE)setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)setOutputFile(nextVideoAbsolutePath)setVideoEncodingBitRate(10000000)setVideoFrameRate(30)setVideoSize(1920, 1080) //FIXME 如果你的设备相机不支持这个分辨率,需要修改一下setVideoEncoder(MediaRecorder.VideoEncoder.H264)setAudioEncoder(MediaRecorder.AudioEncoder.AAC)prepare()}
}

在这里插入图片描述

我们运行程序,点击录制视频,过几秒点击停止录制,然后打开文件管理器,在/sdcard/Android/data/包名/files/video文件夹下,可以看到这个视频了

在这里插入图片描述

4. 停止录制视频

停止录制视频比较简单,只需要释放mediaRecorder
然后再调用startPreview重新开始预览就可以了

private fun stopRecordingVideo() {isRecordingVideo = falsemediaRecorder?.apply {stop()reset()}//重新开始预览startPreview()
}

5. 实现动态设置分辨率

之前我们这是录制分辨率是写死的1920*1080,这样是不够动态灵活的,接下来我们来实现下动态设置分辨率

首先通过characteristics获取到可用的分辨率列表

val characteristics = manager.getCameraCharacteristics(cameraId)
val map = characteristics.get(SCALER_STREAM_CONFIGURATION_MAP) ?:throw RuntimeException("Cannot get available preview/video sizes")

然后通过这个map来选择出最适合的分辨率,这里的选择规则是返回宽高比3:4的分辨率中最高的分辨率

 videoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder::class.java))fun chooseVideoSize(choices: Array<Size>) = choices.firstOrNull {it.width == it.height * 4 / 3 } ?: choices[choices.size - 1]

最后,将该分辨率设置到mediaRecorder中就行了

mediaRecorder?.apply {setAudioSource(MediaRecorder.AudioSource.MIC)setVideoSource(MediaRecorder.VideoSource.SURFACE)setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)setOutputFile(nextVideoAbsolutePath)setVideoEncodingBitRate(10000000)setVideoFrameRate(30)//setVideoSize(1920, 1080)setVideoSize(videoSize.width,videoSize.height)setVideoEncoder(MediaRecorder.VideoEncoder.H264)setAudioEncoder(MediaRecorder.AudioEncoder.AAC)prepare()
}

6. 其他

6.1 本文源码下载

下载地址 : Android Camera2 Demo - 实现相机预览、拍照、录制视频功能

6.2 Android Camera2 系列

更多Camera2相关文章,请看
十分钟实现 Android Camera2 相机预览_氦客的博客-CSDN博客
十分钟实现 Android Camera2 相机拍照_氦客的博客-CSDN博客
十分钟实现 Android Camera2 视频录制_氦客的博客-CSDN博客

6.3 Android 相机相关文章

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作_氦客的博客-CSDN博客
Android 从零开发一个简易的相机App_android开发简易app_氦客的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/91.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

快速入门微信小程序

文章目录 快速入门微信小程序1、微信小程序的注册1.1 注册账户1.2 激活1.3 登记信息1.4 下载微信开发工具1.5 使用微信开发工具创建一个小程序 2、导入官方的Demo2.1 下载官方Demo2.2 导入官方Demo2.3 运行官方Demo 3、编写自己的Demo3.1 创建微信小程序3.2 微信小程序常见参数…

机房动环是什么?内附最新机房动环监控系统报价

伴随着计算机信息化的发展和物联网的广泛运营&#xff0c;为了减少人员维护成本&#xff0c;实现智能化监控管理&#xff0c;机房动环监控系统逐渐被应用开来。通过一套完整的机房动环监控系统&#xff0c;一个偌大的机房就可以实现24小时无人值守。机房动环是什么&#xff1f;…

合宙Air724UG Cat.1模块硬件设计指南--原理图设计注意事项

在设计原理时注意以下几点&#xff1a; 严格按照模块硬件手册设计原理图 1.调试接口&#xff1a; 调试务必留出usb&#xff08;烧录脚本&#xff0c;升级用&#xff09; ,1.8v&#xff08;开机标志&#xff09;&#xff0c;uboot&#xff08;强制烧录用&#xff09;测试点&…

【爬虫】3.4 爬取网站复杂数据

1. Web服务器网站 进一步把前面的Web网站的mysql.html, python.html, java.html丰富其中 的内容&#xff0c;并加上图形&#xff1a; mysql.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>my…

rabbitmq第二课-RabbitMQ核心编程模型以及消息应用场景详解

一、回顾RabbitMQ基础概念 二、RabbitMQ基础编程模型 使用RabbitMQ提供的原生客户端API进行交互。这是使用RabbitMQ的基础。 1.1、maven依赖 <dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version&g…

2023考研一战上岸 电子科技大学 860软件工程 经验分享

目录 1. 前言&#xff1a;考研&#xff0c;心态最重要&#xff01; 2. 初试各科复习经验 (1) 数学一 (2) 英语一 (3) 专业课 (4) 政治 (5) 四门课时间划分 3. 复试流程和备考建议 (1) 复试流程 (2) 备考建议 4. 结语 首先&#xff0c;先简要做一个自我介绍&#xff…

墨迹api实现天气预测

文章目录 需求背景解决效果接口地址index.vueweather.vue图标文件 视频效果 需求背景 使用墨迹天气api实现天气预报&#xff0c;空气质量预报功能 解决效果 接口地址 墨迹天气 index.vue <template><div class"dqhjjc-wrap"><div class"fir…

知易行难!项目推进的6大常见问题

项目推进是一项企业发展业务中的关键任务。然而&#xff0c;许多项目在实施过程中遇到各种困难和挑战&#xff0c;导致项目无法按计划进行或无法实现预期的成果。以下是项目推进过程中常见的六个问题以及解决方案。1、项目目标不明确 项目推进时&#xff0c;如果项目团队不清楚…

Linux学习之以openresty为例学习源码安装软件

https://github.com/openresty/openresty/tags里边有openresty各个版本的源码。 https://openresty.org/en/是官网。 wget https://github.com/openresty/openresty/archive/refs/tags/v1.15.8.1.tar.gz(github网址)或者wget https://openresty.org/download/openresty-1.15.…

Redis五种数据结构底层编码结构

RedisObject Redis中的任意数据类型的键和值都会被封装为一个RedisObject&#xff0c;也叫做Redis对象&#xff0c;源码如下&#xff1a; 对象头不包含数据就已经占16字节&#xff0c;如果数据存string型&#xff0c;一个string一个对象头比较浪费空间&#xff0c;存大量数据…

Kafka系列之:对源连接器的的Exactly-Once支持

Kafka系列之&#xff1a;对源连接器的的Exactly-Once支持 一、背景二、目标三、公共接口四、连接器 API 扩展五、REST API验证六、新指标七、计划变更八、任务计数记录九、重新平衡的准备十、源任务启动十一、领导者访问配置主题十二、用于隔离事务生产者的管理 API十三、解决了…

python 加速(1)

文章目录 简单步骤像Python一样做torch 的一切安装Cmake安装 Torch &#xff08;GPU&#xff09;CMakeLists.txt试用小样设置 CLion 环境 Cuda配置VS C 环境建上手的文件step1: interpolation.cppstep2: interpolation_kernel.custep3: include/ utils.hstep4: setup.pystep5: …