Android MediaCodec

MediaCodec的官方文档

一、Android MediaCodec简单介绍

Android中可以使用MediaCodec来访问底层的媒体编解码器,可以对媒体进行编/解码。
MediaCodec可以处理的数据有以下三种类型:压缩数据、原始音频数据、原始视频数据。这三种类型的数据均可以利用ByteBuffers进行处理,但是对于原始视频数据应提供一个Surface以提高编解码器的性能。Surface直接使用native视频数据缓存,而没有映射或复制它们到ByteBuffers,因

MediaCodec采用异步方式处理数据,并且使用了一组输入输出缓存(ByteBuffer)。通过请求一个空的输入缓存(ByteBuffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(ByteBuffer)中。使用完输出缓存的数据之后,将其释放回编解码器:

MediaCodec的生命周期有三种状态:停止态-Stopped、执行态-Executing、释放态-Released,停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。执行状态(Executing)会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream):

三、MediaCodec兼容问题

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的 使用

所有的同步模式的 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: 0x8000101aE ACodec : [OMX.qcom.video.encoder.avc] storeMetaDataInBuffers (output) failed w/ err -2147483648

参考文章

http://www.jianshu.com/p/1e298f993398
http://www.jianshu.com/p/3c479c0f4876

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容