内容简介:这是若是对
这是 MediaCodeC
系列的第三章,主题是如何使用MediaCodeC将图片集编码为视频文件。在Android多媒体的处理上,MediaCodeC是一套非常有用的API。此次实验中,所使用的图片集正是 MediaCodeC硬解码视频,并将视频帧存储为图片文件 文章中,对视频解码出来的图片文件集,总共332张图片帧。
若是对 MediaCodeC
视频解码感兴趣的话,也可以浏览之前的文章: MediaCodeC解码视频指定帧,迅捷、精确
核心流程
MediaCodeC
的常规工作流程是:拿到可用输入队列,填充数据;拿到可用输出队列,取出数据,如此往复直至结束。在一般情况下,填充和取出两个动作并不是即时的,也就是说并不是压入一帧数据,就能拿出一帧数据。当然,除了编码的视频每一帧都是关键帧的情况下。
一般情况下,输入和输出都使用buffer的代码写法如下:
for (;;) { //拿到可用InputBuffer的id int inputBufferId = codec.dequeueInputBuffer(timeoutUs); if (inputBufferId >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(…); // inputBuffer 填充数据 codec.queueInputBuffer(inputBufferId, …); } // 查询是否有可用的OutputBuffer int outputBufferId = codec.dequeueOutputBuffer(…); 复制代码
本篇文章的编码核心流程,和以上代码相差不多。只是将输入Buffer替换成了Surface,使用Surface代替InputBuffer来实现数据的填充。
为什么使用Surface
在MediaCodeC官方文档里有一段关于Data Type的描述:
CodeC接受三种类型的数据,压缩数据(compressed data)、原始音频数据(raw audio data)以及原始视频数据(raw video data)。这三种数据都能被加工为ByteBuffer。但是对于原始视频数据,应该使用Surface去提升CodeC的性能。
在本次项目中,使用的是MediaCodeC createInputSurface
函数创造出Surface,搭配OpenGL实现Surface数据输入。
这里我画了一张简单的工作流程图:
整体流程上其实和普通的MediaCodeC工作流程差不多,只不过是将输入源由Buffer换成了Surface。
知识点
在代码中,MediaCodeC只负责数据的传输,而生成MP4文件主要靠的类是MediaMuxer。整体上,项目涉及到的主要API有:
- MediaCodeC,图片编码为帧数据
- MediaMuxer,帧数据编码为Mp4文件
- OpenGL,负责将图片绘制到Surface
接下来,我将会按照流程工作顺序,详解各个步骤:
流程详解
在详解流程前,有一点要注意的是,工作流程中所有环节都必须处在同一线程。
配置
首先,启动子线程。配置MediaCodeC:
var codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) // mediaFormat配置颜色格式、比特率、帧率、关键帧间隔 // 颜色格式默认为MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface var mediaFomat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, size.width, size.height) .apply { setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat) setInteger(MediaFormat.KEY_BIT_RATE, bitRate) setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval) } codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) var inputSurface = codec.createInputSurface() codec.start() 复制代码
将编码器配置好之后,接下来配置OpenGL的EGL环境以及GPU Program。由于OpenGL涉及到比较多的知识,在这里便不再赘述。视频编码项目中,为方便使用,我将OpenGL环境搭建以及GPU program搭建封装在了 GLEncodeCore 类中,感兴趣的可以看一下。
EGL环境在初始化时,可以选择两种和设备连接的方式,一种是 eglCreatePbufferSurface
;另一种是 eglCreateWindowSurface
,创建一个可实际显示的windowSurface,需要传一个Surface参数,毫无疑问选择这个函数。
var encodeCore = GLEncodeCore(...) encodeCore.buildEGLSurface(inputSurface) fun buildEGLSurface(surface: Surface) { // 构建EGL环境 eglEnv.setUpEnv().buildWindowSurface(surface) // GPU program构建 encodeProgram.build() } 复制代码
图片数据传入,并开始编码
在各种API配置好之后,开启一个循环,将File文件读取的Bitmap传入编码。
val videoEncoder = VideoEncoder(640, 480, 1800000, 24) videoEncoder.start(Environment.getExternalStorageDirectory().path + "/encodeyazi640${videoEncoder.bitRate}.mp4") val file = File(图片集文件夹地址) file.listFiles().forEachIndexed { index, it -> BitmapFactory.decodeFile(it.path)?.apply { videoEncoder.drainFrame(this, index) } } videoEncoder.drainEnd() 复制代码
在提要里面也提到了,编码项目使用的图片集是之前 MediaCodeC硬解码视频,并将视频帧存储为图片文件 中的视频文件解码出来的,332张图片。
循环代码中,我们逐次将图片Bitmap传入 drainFrame(...)
函数,用于编码。当所有帧编码完成后,使用 drainEnd
函数通知编码器编码完成。
视频帧编码
接着我们再来看 drameFrame(...)
函数中的具体实现。
/** * * @b : draw bitmap to texture * * @presentTime: frame current time * */ fun drainFrame(b: Bitmap, presentTime: Long) { encodeCore.drainFrame(b, presentTime) drainCoder(false) } fun drainFrame(b: Bitmap, index: Int) { drainFrame(b, index * mediaFormat.perFrameTime * 1000) } fun drainCoder(...){ 伪代码:MediaCodeC拿到输出队列数据,使用MediaMuxer编码为 Mp4文件 } 复制代码
首先使用OpenGL将Bitmap绘制纹理上,将数据传输到Surface上,并且需要将这个Bitmap所代表的时间戳传入。在传入数据后使用 drainCoder
函数,从MediaCodeC读取输出数据,使用MediaMuxer编码为Mp4视频文件。 drainCoder
函数具体实现如下:
loopOut@ while (true) { // 获取可用的输出缓存队列 val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut) Log.d("handleOutputBuffer", "output buffer id : $outputBufferId ") if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) { if (needEnd) { // 输出无响应 break@loopOut } } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出数据格式改变,在这里启动mediaMuxer } else if (outputBufferId >= 0) { // 拿到相应的输出数据 if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { break@loopOut } } } 复制代码
就像之前提到过的,并不是压入一帧数据就能即时得到一帧数据。在使用OpenGL将Bitmap绘制到纹理上,并传到Surface之后。要想得到输出数据,必须在一个无限循环的代码中,去拿MediaCodeC输出数据。
也就是在这里的代码中,当输出数据格式改变时,为MediaMuxer加上视频轨,并启动。
trackIndex = mediaMuxer!!.addTrack(codec.outputFormat) mediaMuxer!!.start() 复制代码
整体上的工作流程就是以上这些代码了,传入一帧数据到Surface-->MediaCodeC循环拿输出数据--> MediaMuxer写入Mp4视频文件。
当然,后两步的概念已经相对比较清晰,只有第一步的实现是一个难点,也是当时比较困扰我的一点。接下来我们将会详解,如何将一个Bitmap通过OpenGL把数据传输到Surface上。
Bitmap --> Surface
项目中,将Bitmap数据传输到Surface上,主要靠这一段代码:
fun drainFrame(b: Bitmap, presentTime: Long) { encodeProgram.renderBitmap(b) // 给渲染的这一帧设置一个时间戳 eglEnv.setPresentationTime(presentTime) eglEnv.swapBuffers() } 复制代码
其中encodeProgram是显卡绘制程序,它内部会生成一个纹理,然后将Bitmap绘制到纹理上。此时这个纹理就代表了这张图片,再将纹理绘制到窗口上。
之后,使用EGL的swapBuffer提交当前渲染结果,在提交之前,使用setPresentationTime提交当前帧代表的时间戳。
更加具体的代码实现,都在我的Github项目中。 GLEncodeCore 以及 EncodeProgram GPU Program 还有 EGL 环境构建
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 编码、摘要和加密(一)——字节编码
- 新媒体编码时代的技术:编码与传输
- MySQL数据库字符编码总结--数据传输编码
- PHP 开发者学 Golang 之 URL 编码 (Urlencode)、解编码 (Urldecode)
- ios平台实现视频H264硬编码及软编码(附完整demo)
- Python 中文编码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。