原文地址
Android MediaCodec stuff
这篇文章是关于 MediaCodec
这一系列类,它主要是用来编码和解码音视频数据。并且包含了一些源码示例的集合以及常见问题的解答。
在API23之后,官方的文档 official 就已经十分的详细了。这里的一些信息可以帮你了解一些编解码方面的知识,为了考虑兼容性,这里的代码大部分都是运行在API18及以上的环境中,当然如果你的目标是Lollipop 以上的用户,你可以有更多的选择,这些都没有在这里提及。
概述
MediaCodec
第一次可用是在 Android 4.1版本(API16 ),一开始是用来直接访问设备的媒体编解码器。它提供了一种极其原始的接口。MediaCodec类同时存在 Java和C++层中,但是只有前者是公共访问方法。
在Android 4.3 (API18)中,MediaCodec被扩展为包含一种通过 Surface 提供输入的方法(通过 createInputSurface
方法),这允许输入来自于相机的预览或者是经过OpenGL ES呈现。而且Android4.3也是 MediaCodec 的第一个经过CTS测试(Compatibility Test Suite,CTS是google推出的一种设备兼容性测试规范,用来保证不同设备一致的用户体验,同时Google也提供了一份兼容性标准文档 CDD)的 release 版本。
而且Android4.3还引入了 MediaMuxer,它允许将AVC编解码器(原始H.264基本流)的输出转换为.MP4格式,可以和音频流一起转码也可以单独转换。
Android5.0(API21)引入了“异步模式”,它允许应用程序提供一个回调方法,在缓冲区可用时执行。但是整个文章链接里的代码都没有用到这个,因为兼容性保持到API 18+。
基本使用
所有的同步模式的 MediaCodec
API都遵循一个模式:
- 创建并配置一个
MediaCodec
对象 - 循环直到完成:
如果输入缓冲区就绪,读取一个输入块,并复制到输入缓冲区中
如果输出缓冲区就绪,复制输出缓冲区的数据 - 释放
MediaCodec
对象
MediaCodec的一个实例会处理一种类型的数据,(比如,MP3音频或H.264视频),编码或是解码。它对原始数据操作,所有任何的文件头,比如ID3(一般是位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息)这些信息会被擦除。它不与任何高级的系统组件通信,也不会通过扬声器来播放音频,或是通过网络来获取视频流数据,它只是一个会从缓冲区取数据,并返回数据的中间层。
一些编解码器对于它们的缓冲区是比较特殊的,它们可能需要一些特殊的内存对齐或是有特定的最小最大限制,为了适应广泛的可能性,buffer缓冲区分配是由编解码器自己实现的,而不是应用程序的层面。你并不需要一个带有数据的缓冲区给 MediaCodec
,而是直接向它申请一个缓冲区,然后把你的数据拷贝进去。
这看起来和“零拷贝”原则是相悖的,但大部分情况发生拷贝的几率是比较小的,因为编解码器并不需要复制或调整这些数据来满足要求,而且大多数我们可以直接使用缓冲区,比如直接从磁盘或网络读取数据到缓冲区中,不需要复制。
MediaCodec的输入必须在“access units”中完成,在编码H.264视频时意味着一帧,在解码时意味着是一个NAL单元,然而,它看起来感觉更像是流,你不能提交一个单块,并期望不久后就出现,实际上,编解码器可能会在输出前入队好几个buffers。
这里强烈建议直接从下面的示例代码中学习,而不是直接从官方文档上手。
例子
EncodeAndMuxTest.java (requires 4.3, API 18)
使用OpenGL ES生成一个视频,通过 MediaCodec 使用H.264进行编码,而且通过 MediaMuxer 将流转换成一个.MP4文件,这里通过CTS 测试编写,也可以直接转成其他环境的代码。
CameraToMpegTest.java (requires 4.3, API 18)
通过相机预览录制视频并且编码成一个MP4文件,同样通过 MediaCodec 使用H.264进行编码,以及 MediaMuxer 将流转换成一个.MP4文件,作为一个扩展版,还通过GLES片段着色器在录制的时候改变视频,同样是一个CTS test,可以转换成其他环境的代码。
Android Breakout game recorder patch (requires 4.3, API 18)
这是 Android Breakout v1.0.2版本的一个补丁,添加了游戏录制功能,游戏是在60fps的全屏分辨率下,通过一个30fps 720p的配置使用AVC编解码器来录制视频,录制文件被保存在一个应用的私有空间,比如 ./data/data/com.faddensoft.breakout/files/video.mp4。这个本质上和 EncodeAndMuxTest.java 是一样的,不过这个是完全的真实环境不是CTS test,一个关键的区别在于EGL的创建,这里允许通过将显示和视频context以共享纹理的方式。
EncodeDecodeTest.java (requires 4.3, API 18)
CTS test,总共有三种test做着相同的事情,但是是不同的方式。每一个都是:
生成video的帧,通过AVC进行编码,生成解码流,看看是否和原始数据一样
上面的生成,编码,解码,检测基本是同时的,帧被生成后,传递给编码器,编码器拿到的数据会传递给解码器,然后进行校验,三种方式分别是
Buffer到Buffer,buffers是软件生成的YUV帧数据,这种方式是最慢的,但是能够允许应用程序去检测和修改YUV数据。
Buffer到Surface,编码是一样的,但是解码会在surface中,通过OpenGL ES的 getReadPixels()进行校验
Surface到Surface,通过OpenGL ES生成帧并解码到Surface中,这是最快的方式,但是需要YUV和RGB数据的转换。
DecodeEditEncodeTest.java (requires 4.3, API 18)
CTS test,主要是生成一系列视频帧,通过AVC进行编码,编码数据流保存在内存中,使用 MediaCodec解码,通过OpenGL ES片段着色器编辑帧数据(交换绿/蓝颜色信道),解码编辑后的视频流,验证输出。
ExtractMpegFramesTest.java (requires 4.1, API 16)
ExtractMpegFramesTest.java (requires 4.2, API 17)
提取一个.mp4视频文件的开始10帧,并保持成一个PNG文件到sd卡中,使用 MediaExtractor 提取 CSD 数据,并将单个 access units给 MediaCodec 解码器,帧被解码到一个SurfaceTexture的surface中,离屏渲染,并通过 glReadPixels() 拿到数据后使用 Bitmap#compress() 保存成一个PNG 文件。
常见问题
Q1:我怎么播放一个由MediaCodec创建的“video/avc”格式的视频流?
A1.这个被创建的流是原始的H.264流数据,Linux的Totem Movie Player可以播放,但大部分其他的都播放不了,你可以使用 MediaMuxer 将其转换为MP4文件,看前面的EncodeAndMuxTest例子。
Q2:当我创建一个编码器时,调用 MediaCodec的configure()方法会失败并抛出一个IllegalStateException异常?
A2.这通常是因为你没有指定所有编码器需要的关键命令,可以看一个这个例子 this stackoverflow item。
Q3:我的视频解码器配置好了但是不接收数据,这是为什么?
A3.一个比较常见的错误就是忽略设置Codec-Specific Data(CSD),这个在文档中简略的提到过,有两个key,“csd-0”,“csd-1”,这个相当于是一系列元数据的序列参数集合,我们只需要直到这个会在MediaCodec 编码的时候生成,并且在MediaCodec 解码的时候需要它。
如果你直接把编码器输出传递给解码器,就会发现第一个包里面有BUFFER_FLAG_CODEC_CONFIG 的flag,这个参数需要确保传递给了解码器,这样解码器才会开始接收数据,或者你可以直接设置CSD数据给MediaFormat,通过 configure()
方法设置给解码器,这里可以参考 EncodeDecodeTest sample 这个例子。
实在不行也可以使用 MediaExtractor ,它会帮你做好一切。
Q4:我可以直接将流数据给解码器么?
A4.不一定,解码器需要的是 "access units"格式的流,不一定是字节流。对于视频解码器,这意味着你需要保存通过编码器(比如H.264的NAL单元)创建的“包边界”,这里可以参考 DecodeEditEncodeTest sample 是如何操作的,一般不能读任意的块数据并传递给解码器。
Q5:我在编码由相机预览拿到的YUV数据时,为什么看起来颜色有问题?
A5.相机输出的颜色格式和MediaCodec 在编码时的输入格式是不一样的,相机支持YV12(平面 YUV 4:2:0) 以及 NV21 (半平面 YUV 4:2:0),MediaCodec支持以下一个或多个:
.#19 COLOR_FormatYUV420Planar (I420)
.#20 COLOR_FormatYUV420PackedPlanar (also I420)
.#21 COLOR_FormatYUV420SemiPlanar (NV12)
.#39 COLOR_FormatYUV420PackedSemiPlanar (also NV12)
.#0x7f000100 COLOR_TI_FormatYUV420PackedSemiPlanar (also also NV12)
I420的数据布局相当于YV12,但是Cr和Cb却是颠倒的,就像NV12和NV21一样。所以如果你想要去处理相机拿到的YV12数据,可能会看到一些奇怪的颜色干扰,比如这样 these images。直到Android4.4版本,依然没有统一的输入格式,比如Nexus 7(2012),Nexus 10使用的COLOR_FormatYUV420Planar,而Nexus 4, Nexus 5, and Nexus 7(2013)使用的是COLOR_FormatYUV420SemiPlanar,而Galaxy Nexus使用的COLOR_TI_FormatYUV420PackedSemiPlanar。
一种可移植性更高,更有效率的方式就是使用API18 的Surface input API,这个在 CameraToMpegTest sample 中已经演示了,这样做的缺点就是你必须去操作RGB而不是YUV数据,这是一个图像处理的问题,如果你可以通过片段着色器来实现图像操作,可以利用GPU来处理这些转换和计算。
Q6: EGL_RECORDABLE_ANDROID
flag是用来干什么的?
A6.这会告诉EGL,创建surface的行为必须是视频编解码器能兼容的,没有这个flag,EGL可能使用 MediaCodec 不能理解的格式来操作。
Q7:我是不是必须要在编码时设置 presentation time stamp (pts)?
A7.是的,一些设备如果没有设置合理的值,那么在编码的时候就会采取丢弃帧和低质量编码的方式。
需要注意的一点就是MediaCodec所需要的time格式是微秒,大部分java代码中的都是毫秒或者纳秒。
Q8:为什么有时输出混乱(比如都是零,或者太短等等)?
A8.这常见的错误就是没有去适配ByteBuffer的position和limit,这些东西MediaCodec并没有自动的去做,
我们需要手动的加上一些代码:
int bufIndex = codec.dequeueOutputBuffer(info, TIMEOUT);
ByteBuffer outputData = outputBuffers[bufIndex];
if (info.size != 0) {
outputData.position(info.offset);
outputData.limit(info.offset + info.size);
}
在输入端,你需要在将数据复制到缓冲区之前调用 clear()
。
Q9: 有时候会发现 storeMetaDataInBuffers
会打出一些错误log?
A9.是的,比如在Nexus 5上,看起来是这样的
E OMXNodeInstance: OMX_SetParameter() failed for StoreMetaDataInBuffers: 0x8000101a
E ACodec : [OMX.qcom.video.encoder.avc] storeMetaDataInBuffers (output) failed w/ err -2147483648
不过可以忽略这些,不会出现什么问题。