十分钟实现 Android Camera2 视频录制

news2024/11/15 18:58:50

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: CameraCaptureSession

private 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: OrientationLiveData

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

2.2 添加布局

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

<Button
    android: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" />

<Button
    android: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 = camera
            mediaRecorder = 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)

然后设置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 = this

    val nextVideoAbsolutePath = getVideoFilePath(cameraActivity)

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

    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()
    }
}

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 = cameraCaptureSession

val 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 = cameraCaptureSession
	
	            val previewRequestBuilder =
	                cameraDevice!!.createCaptureRequest(TEMPLATE_RECORD).apply {
	                    addTarget(binding.surfaceView.holder.surface)
	                    addTarget(recorderSurface)
	                }
	            session?.setRepeatingRequest(
	                previewRequestBuilder!!.build(),
	                null,
	                cameraHandler
	            )
	
	            isRecordingVideo = true
	            mediaRecorder?.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 = this

    val nextVideoAbsolutePath = getVideoFilePath(cameraActivity)

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

    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 = false
    mediaRecorder?.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来选择出最适合的分辨率,这里的选择规则是最高会返回1080P的分辨率

 videoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder::class.java))
 
/**
 * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes
 * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video.
 *
 * @param choices The list of available sizes
 * @return The video size
 */
private fun chooseVideoSize(choices: Array<Size>) = choices.firstOrNull {
    it.width == it.height * 4 / 3 && it.width <= 1080 } ?: 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(previewSize.width,previewSize.height)
    setVideoEncoder(MediaRecorder.VideoEncoder.H264)
    setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
    prepare()
}

5. 其他

5.1 本文源码下载

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

5.2 Android Camera2 系列

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

5.3 Android 相机相关文章

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python主动抛出Warning的艺术:一种提醒用户的优雅方式

简介&#xff1a;Python提供了一个内置的warnings模块&#xff0c;使得开发者可以在代码中主动地发出警告。这对于告知用户某些问题或者对某些即将废弃的特性进行提示尤其有用。本文将重点介绍如何在Python代码中主动抛出警告&#xff0c;并探讨其使用场景和优势。 历史攻略&a…

设计模式3:单例模式:volatile关键字能不能解决多线程计数问题?

先说结论不能&#xff1a; 代码实测下&#xff1a; public class Counter {public volatile static int count 0;public static void inc() {//这里延迟1毫秒&#xff0c;使得结果明显try {Thread.sleep(1);} catch (InterruptedException e) {}count;}public static void ma…

Win10安装CUDA

一、安装Nvidia显卡驱动 安装Nvidia显卡驱动前可以先检查Nvidia显卡驱动是否已安装。搜索 Nvidia控制面板 或 Nvidia Control Panel可以看到当前已经安装的显卡驱动及版本。 如需安装显卡驱动&#xff0c;在官方驱动下载网站找到自己的显卡型号对应的驱动下载并安装:官方驱动…

行业分析| 无人机电力巡检技术的应用

随着现代生活水平的不断提升&#xff0c;人们对各行各业的发展都提出了更高的品质要求&#xff0c;对于电力的需求不断上涨&#xff0c;因此也加速了电力行业的转型升级。基于这一发展状况&#xff0c;我国电力行业逐渐开始选择应用无人机电力巡检等现代高科技技术。 无人机电…

Baumer工业相机堡盟工业相机如何通过BGAPISDK进行定序器编程:VCXG双快门操作(C#)

Baumer工业相机堡盟工业相机如何通过BGAPISDK进行定序器编程:VCXG双快门操作&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机BGAPISDK和定序器编程的技术背景Baumer工业相机通过BGAPISDK进行定序器编程功能1.引用合适的类文件2.Baumer工业相机通过BGAPISDK进行定序器…

Keep上市,打响健身科技第一炮?

近些年&#xff0c;大众对于身体健康和审美的需求越来越旺盛&#xff0c;因此也引发了一场无形的健身革命。无论是线下动辄大几千的健身房&#xff0c;还是线上的健身直播经济都受到了不小的关注&#xff0c;在疫情刚开始的那段时间&#xff0c;各地的封控让在线健身成为了一种…

修改滚动条样式 和 那些高度

一、滚动条样式 二、那些高度 网页可见区域宽&#xff1a; document .body.clientWidth; 网页可见区域高&#xff1a; document .body.clientHeight; 网页可见区域宽&#xff1a; document .body.offsetWidth (包括边线的宽); 网页可见区域高&#xff1a; document .body.of…

亚马逊实践 | 构建可持续发展的架构模型

可持续发展概念源于对系统性文明危机和世界问题的科学和社会意识形态研究。世界级的进步学术社群和政治精英在二十世纪末就认识到了这些问题的存在。他们将即将到来的二十一世纪视为充满不确定性、全球灾难进程逐步升级的时代。可持续发展对多个领域产生影响&#xff0c;目前已…

Sudo堆溢出漏洞(CVE-2021-3156)复现

背景介绍 2021 年 1 月 26 日&#xff0c;Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时&#xff0c;错误的判断了截断符&#xff0c;从而导致攻击者可以恶意构造载荷&#xff0c;使得sudo发生堆溢出&#xff0c;该漏洞在配合环境变量等分配堆以及…

在Mac上安装Aspectj1.9.8(用于Java17)+IDEA

1. 确定所使用的Java版本和AspectJ的对应关系 2. 下载AspectJ包 3. 安装AspectJ 4. 添加AspectJ对应的环境变量 5. AspectJ测试-简单终端测试 6. AspectJ测试-通过IDEA敲代码测试 ---------------------------------------详细教程-------------------------------------…

【深度学习】7-0 自制框架实现DeZero - 自动微分

介绍下处理深度学习的框架DeZero&#xff0c;通过这个框架来了解自动微分是如何实现的 自动微分指的是自动求出导数的做法(技术)。“自动求出导数”是指由计算机(而非人)求出导数。具体来说&#xff0c;它是指在对某个计算(函数)编码后计算机会自动求出该计算的导数的系统。 自…

flexible.js适配pc端、移动端并自动将px转换rem

首先在assets中创建一个flexible.js文件 ;(function(win, lib) {let doc win.document;let docEl doc.documentElement;let metaEl doc.querySelector(meta[name"viewport"]);let flexibleEl doc.querySelector(meta[name"flexible"]);let dpr 0;let…

POI及EasyExcel操作xls,xlsx文件

Apache POI 是基于 Office Open XML 标准&#xff08;OOXML&#xff09;和 Microsoft 的 OLE 2 复合文档格式&#xff08;OLE2&#xff09;处理各种文件格式的开源项目。 可以使用 Java 读写 MS Excel 文件&#xff0c;可以使用 Java 读写 MS Word 和 MS PowerPoint 文件。 模…

C# 标注图片

画矩形 画四边形 保存标注图片 保存标注信息 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Drawing.Ima…

【UE5 Cesium】06-Cesium for Unreal 从一个地点飞行到另一个地点(上)

UE版本&#xff1a;5.1 介绍 本文以在墨尔本和悉尼这两个城市间为例&#xff0c;介绍如何使用虚幻5引擎和Cesium for Unreal插件在这两个城市间进行飞行移动&#xff0c;其中墨尔本和悉尼城市的倾斜摄影是Cesium官方仓库中自带的资产&#xff0c;我们引入到自己的Cesium账号…

蓝桥杯专题-试题版-【地宫取宝】【斐波那契】【波动数列】【小朋友排队】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

MySQL相关知识点

这里写目录标题 MySQL简介概述配置安装连接&#xff08;企业级&#xff09;数据模型sql语句简介语法分类 数据库设计DDL&#xff08;SQL语句&#xff09;数据库操作idea集成mysql开发图形化工具&#xff08;直接在空java项目里打开mysql数据库&#xff09; 表&#xff08;对表的…

ASEMI代理ST可控硅BTA41封装,BTA41图片

编辑-Z BTA41参数描述&#xff1a; 型号&#xff1a;BTA41 封装&#xff1a;TO-3P RMS导通电流IT(RMS)&#xff1a;40A 非重复浪涌峰值导通电流ITSM&#xff1a;420A 峰值栅极电流IGM&#xff1a;8A 平均栅极功耗PG&#xff1a;1W 存储接点温度范围Tstg&#xff1a;-40…

kubelete源码阅读

kubelet 是运行在每个节点上的主要的“节点代理”&#xff0c;每个节点都会启动 kubelet进程&#xff0c;用来处理 Master 节点下发到本节点的任务&#xff0c;按照 PodSpec 描述来管理Pod 和其中的容器&#xff08;PodSpec 是用来描述一个 pod 的 YAML 或者 JSON 对象&#xf…

ATTCK(四)之ATTCK矩阵战术技术(TTP)逻辑和使用

ATT&CK矩阵战术&技术&#xff08;TTP&#xff09;逻辑和使用 ATT&CK的战术与技术组织结构 ATT&CK矩阵中的所有战术和技术&#xff0c;都可以通过以下链接进行目录结构式的浏览https://attack.mitre.org/techniques/enterprise/&#xff0c;也可以在矩阵里直接…