Android Camera2采集并编码为H.264

前言

本篇博文主要讲述的是基于Android原生MediaCodec通过Camera2 API进行图像数据采集并编码为H.264的实现过程,如果对此感兴趣的不妨驻足观看,也欢迎大家大家对本文中描述不当或者不正确的地方进行指正。如果对于Camera2预览还不熟悉的可以观看博主上一篇博文:Android 基于Camera2 API进行摄像机图像预览。

MediaCodec简介

MediaCodec 主要是用于对音视频进行编解码。它通常与 MediaExtractor、MediaMuxer、Surface 和 AudioTrack 等组件一起使用。MediaCodec 支持硬件加速,可以利用设备的硬件资源来提高编解码的性能。

1、MediaCodec 编解码流程

MediaCodec 采用异步方式处理数据,使用一组输入输出缓冲区(ByteBuffer)。编解码流程大致如下:

  • 请求一个空的输入缓冲区,填充满数据后传递给 MediaCodec 处理。

  • MediaCodec 处理完数据后,将结果输出至一个空的输出缓冲区中。

  • 从 MediaCodec 获取输出缓冲区的数据,消耗掉里面的数据后,释放回编解码器。

具体流程可以参考下图:

在这里插入图片描述

2、MediaCodec 生命周期

在这里插入图片描述

从上图可以看出MediaCodec 的生命周期包括三种状态:Stopped、Executing、Released。

  • Stopped,分三种子状态:

    • Configured,MediaCodec实例创建后,调用configure方法后就进入了Configured状态

    • Uninitialized,MediaCodec实例被创建后,在调用configure方法前都处于该状态;

    • Error,MediaCodec遇到错误时进入该状态,通常可能是队列操作返回错误或异常导致的;

  • Executing,分三种状态

    • Flushed,在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。可以在Executing状态的任何时候通过调用flush()方法返回到Flushed子状态;

    • Running,一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。通过调用stop()方法转移到Uninitialized状态;

    • End of Stream,将一个带有end-of-stream标记的输入buffer入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入buffer,但它仍然产生输出buffer直到end-of-stream标记输出

  • Released,当使用完MediaCodec后,必须调用release()方法释放其资源。调用 release()方法进入最终的Released状态;

编码实现

老规矩,依然需要对MediaCodec进行封装,在封装之前我们需要先设计下对应的接口,而根据上面了解到的MediaCodec生命周期中的几个状态我们需要对其进行记录,方便对外获取,因此接口设计如下:

interface ICodec {enum class State{IDLE,START,STOP,CLOSE}fun start()fun stop()fun close()fun getState():State
}interface IVideoEncoder: ICodec 

这里的close对应的是MediaCodec的release接口。ICodec设计思想是考虑到后续会扩展出不止VideocEncoder还会有Decoder因此只包含了最原始的几个接口设计,针对Encoder和Decoder区别性的接口可以基于ICodec进行扩展继续添加,因为IVideoEncoder暂时没有需要额外添加的接口所以只是单纯的继承自ICodec,后面有需要再添加即可。

接着让我们再编写一个VideoCodec类并实现IVideoEncoder接口,开始我们的编码实现。

class VideoEncoder(private var params:VideoEncParams = VideoEncParams()):IVideoEncoder {private var state = ICodec.State.IDLEprivate var enccoder:MediaCodec? = nullprivate var inputSurface:Surface? = nullprivate var encodeThread:Thread? = nullprivate var callback:EncoderCallback? = nullfun setEncoderCallback(callback:EncoderCallback){this.callback = callback}interface EncoderCallback{fun onCallback(data:ByteArray,frameFlags:Int)}

VideoEncoder实现了IVideoEncoder接口并且构造时需要传入VideoEncParams,VideoEncParams我们等下再看,inputSurface是编码输入源,encodeThread用于异步进行编码,callback则是对外提供编码后数据的回调,回调并不仅仅只包含编码后的帧数据,还包含有一个Int类型的frameFlags,这个frameFlags可以理解为编码这一帧的类型,例如SPS帧或者关键帧等,现在我们回过头来看下传入的VideoEncParams:

class VideoEncParams(var mime:String = "video/avc",var codecWidth:Int = 1920,var codecHeight:Int = 1080,var bitRate:Int = 2048,var fps:Int = 30,var keyInterval:Float = 1f,var rotation:Int = 90
)

参数貌似有点多,但实际编码过程中的传参根据需要可能会更多,只是我这里罗列的这些参数已经满足我们当前示例的需要了,这些参数的意义等下在配置到编码器的时候我们再详细解释,继续往下。

    override fun start() {if(state == ICodec.State.START) returnstate =  ICodec.State.STARTif(enccoder == null){enccoder = MediaCodec.createEncoderByType(params.mime).apply {configure(createMediaFormat(),null,null,MediaCodec.CONFIGURE_FLAG_ENCODE)}inputSurface = enccoder?.createInputSurface()}enccoder?.start()encodeThread = Thread(encodeTask).apply { start() }}

这里我们实现了IVideoEncoder的第一个接口函数,也就是我们启动编码器的接口,函数的第一行代码做了一个保护,防止多次调用同一个编码器的start,如果全局变量enccoder为null就通过MediaCodec.createEncoderByType函数进行创建,创建时需要传入一个String类型的参数,这里我们传入的是params中保存的mime,上面我们也看到了这里的mime是“video/avc”。之后调用编码器configure函数进行了初始化配置,通过上面MediaCodec的生命周期可知,MediaCodec在编解码之前必须要先通过configure函数进行配置,才可以正常使用。

而这里的configure需要传入的参数有点多,我们一个一个看:

第一个参数类型是MediaFormat,createMediaFormat()函数的实际作用就是这里对VideoEncParams进行了转换将其转换成了MediaFormat对象。

第二个参数类型为Surface,是设置解码器的显示Surface我们这里用的是编码器,因此直接设置为null。

第三个参数类型为MediaCrypto,主要是与媒体数据加解密有关,我们这边不需要因此也设置为null。

第四个参数类型为int,设置为MediaCodec.CONFIGURE_FLAG_ENCODE配置MediaCodec为编码器。

现在我们来看下createMediaFormat()函数实现。

    private fun  createMediaFormat(): MediaFormat {var mediaFormat =  MediaFormat.createVideoFormat(params.mime,params.codecWidth,params.codecHeight)//设置比特率mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,params.bitRate)//设置帧率mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,params.fps)//设置颜色模式mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)//设置关键帧频率 帧/秒mediaFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL,params.keyInterval)//设置摄像机旋转角度mediaFormat.setInteger(MediaFormat.KEY_ROTATION,params.rotation)LogPrint.debug("createMediaFormat mediaFormat:$mediaFormat")return mediaFormat}

第一行代码是创建MediaFormat对象,MediaFormat可以理解为媒体数据的描述信息,通过createVideoFormat(),意思是创建一个与视频相关的描述信息对象,这里依然传入了mime,之后两个参数依次传入了需要编码视频的宽高。

MediaFormat设置媒体描述信息的方式是以键值对的方式传入的,键是字符串,值可以是int、long、float、String或ByteBuffer。

这里代码都有注释,就不再额外赘述。

让我们继续回到start()函数,当configure配置结束之后,通过enccoder获取到了一个Surface(inputSurface),该Surface可以理解为编码器的输入队列。

当编码器创建就绪之后,就创建了一个encodeThread线程,那么encodeTask的实现逻辑就是编码的关键逻辑了。

    private var encodeTask:Runnable = Runnable {//编码输出信息的对象,赋值由MediaCodec实现var bufferInfo = MediaCodec.BufferInfo()//当前可用的ByteBuffer索引var outputBufferIndex:Intvar encBuf:ByteArraytry {//死循环获取,也可以对MediaCodec设置callback获取while (state == ICodec.State.START) {//获取下一个可用的输出缓冲区,如果存在可用,返回值大于等于0,bufferInfo也会被赋值,最大等待时间是100000微秒,其实就是100毫秒outputBufferIndex = enccoder?.dequeueOutputBuffer(bufferInfo, 100000)!!//如果当前输出缓冲区没有可用的,返回负值,不同值含义不一样,有需要做判定即可if (outputBufferIndex < 0) {continue;}val outputBuffer: ByteBuffer = enccoder?.getOutputBuffer(outputBufferIndex)!!//调整数据位置,从offset开始。这样我们一会儿读取就不用传offset偏差值了。outputBuffer.position(bufferInfo.offset)//改完位置,那肯定要改极限位置吧,不然你数据不就少了数据末尾长度为offset的这一小部分?这两步不做也可以,get的时候传offset也一样outputBuffer.limit(bufferInfo.offset + bufferInfo.size)encBuf = ByteArray(bufferInfo.size)//获取编码数据outputBuffer.get(encBuf, 0, bufferInfo.size)callback?.onCallback(encBuf, bufferInfo.flags)//释放数据,不释放就一直在,MediaCodec数据满了可不行enccoder!!.releaseOutputBuffer(outputBufferIndex, false)}}catch (e:Exception){enccoder?.release()enccoder = nullinputSurface?.release()inputSurface = nullLog.e("VideoEncoder","VideoEncoder error:",e)}}

这块代码稍微有点多,我们来逐行看下,第一行代码是创建了一个BufferInfo对象,该对象主要是用来描述编码数据信息。第二行代码是当前MediaCodec输出buffer的索引位置,不理解的可以再回头看下上面给出的MediaCodec编解码流程图,第三行代码创建了一个ByteArray对象encBuf用于保存MediaCodec编码后的数据。

再往下就是核心的编码代码了,while循环中,通过MediaCodec的dequeueOutputBuffer获取当前可用的编码完成后的Buffer索引,第一个参数是上面我们创建的BufferInfo对象,第二个参数是等待时间,如果获取成功,MediaCodec会将缓存信息保存到BufferInfo对象中,并且返回Buffer的索引。

enccoder?.getOutputBuffer(outputBufferIndex)!!通过index获取输出Buffer。再之后是根据bufferInfo中的描述信息获取正确的缓存数据并保存到我们上面定义的encBuf中。

callback?.onCallback(encBuf, bufferInfo.flags)拿到编码数据之后通过该接口同步出去,这里这个代码可能有点不太合理,因为callback?.onCallback外部使用者如果使用不当可能会影响编码,理论上应该用队列同步出去会更好,这里我们先这样,后续我再进行优化。

while循环最后一行代码就是释放掉当前获取的输出Buffer数据,不释放的话MediaCodec缓存满了可能会出现异常。

再最后如果编码过程中出现异常,就会释放掉当前的编码器,按照生命周期其实通过MediaCodec.reset()也可以,当前这里为了方便,就直接释放了,等下次再start()重新创建即可。

至此编码就已经全部完成了,接下来让我们再加如一些其他的函数,使得VideoCodec更加完善。

    override fun stop() {if(state != ICodec.State.START) returnstate = ICodec.State.STOPencodeThread?.interrupt()enccoder?.stop()encodeThread = null}override fun close() {stop()state = ICodec.State.CLOSEenccoder?.release()inputSurface?.release()}override fun getState(): ICodec.State {return state}fun getInputSurface():Surface?{return inputSurface}

stop()和close()分别是停止解码器和释放解码器,stop()之后通过start()还能继续进行编码但是调用close()之后就不能再调用start(),否则就会报错,需要重新创建VideoEncodec进行编码。getState()没什么好说的,就只是返回当前编码器的状态,getInputSurface()将编码器的输入Surface(理解为队列)返回。

现在我们编码器就已经编写完成了,让我们看看如何跟我们的CameraWrapper进行组合编码Camera画面。让我们继续编写一个新的类CameraEncoder,通过这个类让我们把VideoEncodec和CameraWrapper组合起来,用以实现Camera画面编码为H.264。

class CameraEncoder :VideoEncoder.EncoderCallback{private var cameraWrapper: CameraWrapperprivate var videoEncoder:VideoEncoderprivate var callback:VideoEncoder.EncoderCallback? = nullconstructor(context:Context){cameraWrapper = CameraWrapper(context)videoEncoder = VideoEncoder()videoEncoder.setEncoderCallback(this)}fun start(surfaceView: SurfaceView){videoEncoder.start()cameraWrapper.setEncoderSurface(videoEncoder.getInputSurface()!!)cameraWrapper.startPreview(surfaceView)}fun stop(){videoEncoder.stop()cameraWrapper.stopPreview()cameraWrapper.setEncoderSurface(null)}fun close(){cameraWrapper.release()videoEncoder.close()}fun setEncoderCallback(callback:VideoEncoder.EncoderCallback){this.callback = callback}override fun onCallback(data: ByteArray,flags:Int) {callback?.onCallback(data,flags)}
}

因为代码量不多,而且没有什么难度,所以就直接全部贴出来了,在构造函数中同时创建了VideoCodec和CameraWrapper对象,对外的编码数据并不是通过VideoEncoder的回调直返回的,而是通过CameraEncoder的回调间接返回。

启动时先后启动了videoEncoder编码和cameraWrapper预览,特殊一点就是将videoEncoder中的输入Surface也就是我们上面说的可以理解为输入队列的哪个Surface传递到cameraWrapper这样在cameraWrapper预览时会自动关联起来。

停止时也是一样两个对象依次停止,不过在停止之后设置了cameraWrapper在启动时传入的Surface为空,其实也可以不用置空,但个人感觉这样可能会更好一点。

close()就不多说,与上面描述的start和stop逻辑一致。

至此我们Camera画面采集并编码为H.264就已经完成了。将编码后的数据保存到文件,然后通过VLC即可观看。

总结

Camera2与MediaCodec的结合在Android平台上提供了一种强大的视频处理解决方案。Camera2 API负责高效地从摄像头采集原始视频帧,而MediaCodec API则负责将这些帧实时编码为H.264格式,这是目前最广泛支持的视频编码标准之一。这种组合不仅利用了硬件加速来提高编码性能,减少CPU负担,还确保了视频的高质量输出和良好的兼容性。通过精确控制编码参数,可以根据应用需求调整视频的比特率、帧率和分辨率,实现定制化的视频录制和处理。总的来说,Camera2与MediaCodec的协同工作为开发者提供了一个灵活、高效的工具,用于创建和处理高质量的视频内容。

代码依然比较粗糙,但作为启蒙(用词貌似不当),应该是够了,后续随着博主的继续学习将会继续完善,感谢大家观看。

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

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

相关文章

【ROS2】Ubuntu22.04安装ROS humble

一. ROS简介 1.1 什么是ROS ROS 是一个适用于机器人的开源的元操作系统。它提供了操作系统应有的服务&#xff0c;包括硬件抽象&#xff0c;底层设备控制&#xff0c;常用函数的实现&#xff0c;进程间消息传递&#xff0c;以及包管理。ROS的核心思想就是将机器人的软件功能做…

Redis开发05:使用stackexchange.redis库对redis进行增删改查

一、安装第三方库 二、官网 StackExchange.Redis |通用型 redis 客户端 三、连接示例 private static string redisConnectionString "localhost:6379,passwordyourpassword,defaultDatabase0,allowAdmintrue,asyncTimeout10000";private static string redisConn…

【golang】单元测试,以及出现undefined时的解决方案

单元测试 要对某一方法进行测试时&#xff0c;例如如下这一简单减法函数&#xff0c;选中函数名后右键->转到->测试 1&#xff09;Empty test file 就是一个空文件&#xff0c;我们可以自己写测试的逻辑 但是直接点绿色箭头运行会出问题&#xff1a; 找不到包。我们要在…

火语言RPA流程组件介绍--键盘按键

&#x1f6a9;【组件功能】&#xff1a;模拟键盘按键 配置预览 配置说明 按键 点击后,在弹出的软键盘上选择需要的按键 执行后等待时间(ms) 默认值300,执行该组件后等待300毫秒后执行下一个组件. 输入输出 输入类型 万能对象类型(System.Object)输出类型 万能对象类型…

护航开源大赛,赋能数字未来

近日&#xff0c;在2024世界互联网大会乌镇峰会互联网公益慈善论坛上&#xff0c;2024中国互联网发展创新与投资大赛&#xff08;开源&#xff09;圆满落幕。作为大赛的技术支持单位&#xff0c;棱镜七彩支撑了大赛的分析评估&#xff0c;定量化、科学化、体系化、专业化的对参…

Profinet转EtherNet/IP网关是如何解决西门子S7-1500PLC与AB PLC的通讯问题的

一、 案例背景 在一个工业现场&#xff0c;一端是AB的PLC&#xff0c;IP地址192.168.1.20;另一端西门子是S7-1500系列&#xff0c;IP地址192.168.2.248。AB的PLC内有 B3、N7、F8 三个寄存器文件涉及到通讯&#xff0c;分别对应西门子PLC的M、DB1、DB2三个存储区域。通过捷米特…

GateWay使用手册

好的&#xff0c;下面是优化后的版本。为了提高可读性和规范性&#xff0c;我对内容进行了结构化、简化了部分代码&#xff0c;同时增加了注释说明&#xff0c;便于理解。 1. 引入依赖 在 pom.xml 中添加以下依赖&#xff1a; <dependencies><!-- Spring Cloud Gate…

SpringBoot+Flowable快速实现工流_动态选择审批人员

前言 OA系统中的工作流不仅是企业日常运营的重要组成部分&#xff0c;也是实现企业数字化转型、提高工作效率和执行力的重要工具。 在国内大部分的工作流系统使用Activiti框架实现。 其实flowable也可以轻松实现工作流业务。在线体验JeecgFlow flowable简介 Flowable是一个使用…

【ONE·基础算法 || 动态规划(三)】

总言 主要内容&#xff1a;编程题举例&#xff0c;熟悉理解动态规划类题型&#xff08;回文串问题、两个数组的 dp问题&#xff09;。                文章目录 总言7、回文串问题7.1、 回文子串&#xff08;medium&#xff09;7.1.1、题解 7.2、 最长回文子串&#…

Python 3 教程第33篇(MySQL - mysql-connector 驱动)

Python MySQL - mysql-connector 驱动 MySQL 是最流行的关系型数据库管理系统&#xff0c;如果你不熟悉 MySQL&#xff0c;可以阅读我们的 MySQL 教程。 本章节我们为大家介绍使用 mysql-connector 来连接使用 MySQL&#xff0c; mysql-connector 是 MySQL 官方提供的驱动器。…

LLM*:路径规划的大型语言模型增强增量启发式搜索

路径规划是机器人技术和自主导航中的一个基本科学问题&#xff0c;需要从起点到目的地推导出有效的路线&#xff0c;同时避开障碍物。A* 及其变体等传统算法能够确保路径有效性&#xff0c;但随着状态空间的增长&#xff0c;计算和内存效率会严重降低。相反&#xff0c;大型语言…

【Db First】.NET开源 ORM 框架 SqlSugar 系列

.NET开源 ORM 框架 SqlSugar 系列 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列【Code First】.NET开源 ORM 框架 SqlSugar 系列【数据事务…

企业品牌曝光的新策略:短视频矩阵系统

企业品牌曝光的新策略&#xff1a;短视频矩阵系统 在当今数字化时代&#xff0c;短视频已经渗透到我们的日常生活之中&#xff0c;成为连接品牌与消费者的关键渠道。然而&#xff0c;随着平台于7月20日全面下线了短视频矩阵的官方接口&#xff0c;许多依赖于此接口的小公司和内…

006 MATLAB编程基础

01 M文件 MATLAB输入命令有两种方法&#xff1a; 一是在MATLAB主窗口逐行输入命令&#xff0c;每个命令之间用分号或逗号分隔&#xff0c;每行可包含多个命令。 二是将命令组织成一个命令语句文集&#xff0c;使用扩展名“.m”&#xff0c;称为M文件。它由一系列的命令和语句…

Java基于SpringBoot+Vue的IT技术交流和分享平台(附源码+lw+部署)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【计算机网络】实验3:集线器和交换器的区别及交换器的自学习算法

实验 3&#xff1a;集线器和交换器的区别及交换器的自学习算法 一、 实验目的 加深对集线器和交换器的区别的理解。 了解交换器的自学习算法。 二、 实验环境 • Cisco Packet Tracer 模拟器 三、 实验内容 1、熟悉集线器和交换器的区别 (1) 第一步&#xff1a;构建网络…

UICollectionView在xcode16编译闪退问题

使用xcode15运行工程&#xff0c;控制台会出现如下提示&#xff1a; Expected dequeued view to be returned to the collection view in preparation for display. When the collection views data source is asked to provide a view for a given index path, ensure that a …

Proteus8.17下载安装教程

Proteus是一款嵌入式系统仿真开发软件&#xff0c;实现了从原理图设计、单片机编程、系统仿真到PCB设计&#xff0c;真正实现了从概念到产品的完整设计&#xff0c;其处理器模型支持8051、HC11、PIC10/12/16/18/24/30/DsPIC33、AVR、ARM、8086和MSP430等&#xff0c;能够帮助用…

Vue教程|搭建vue项目|Vue-CLI2.x 模板脚手架

一、项目构建环境准备 在构建Vue项目之前&#xff0c;需要搭建Node环境以及Vue-CLI脚手架&#xff0c;由于本篇文章为上一篇文章的补充&#xff0c;也是为了给大家分享更为完整的搭建vue项目方式&#xff0c;所以环境准备部分采用Vue教程&#xff5c;搭建vue项目&#xff5c;V…

一款支持80+语言,包括:拉丁文、中文、阿拉伯文、梵文等开源OCR库

大家好&#xff0c;今天给大家分享一个基于PyTorch的OCR库EasyOCR&#xff0c;它允许开发者通过简单的API调用来读取图片中的文本&#xff0c;无需复杂的模型训练过程。 项目介绍 EasyOCR 是一个基于Python的开源项目&#xff0c;它提供了一个简单易用的光学字符识别&#xff…