Android音视频之MediaCodec

简介

从 API 16开始,Android提供了MediaCodec类以便开发者更加灵活的处理音视频的编解码,较MeidaPlay提供了更加丰富、完善的操作接口。具体详见:这里

正文

MediaCodec类可用于访问Android底层的媒体编解码器。例如:编码/解码组件。它是Android为多媒体支持提供的底层接口的一部分,通常(MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface 和AudioTrack)一起使用。

编解码流程

一个编解码器可以处理输入的数据来产生输出的数据,编解码器使用一组输入和输出缓冲器来异步处理数据。你可以创建一个空的输入缓冲区,填充数据后发送到编解码器进行处理。编解码器使用输入的数据进行转换,然后输出到一个空的输出缓冲区。最后你获取到输出缓冲区的数据,消耗掉里面的数据,释放回编解码器。如果后续还有数据需要继续处理,编解码器就会重复这些操作。输出流程如下:

[图片上传失败...(image-ad446d-1553137245958)]

Data Types

编解码器处理三种类型的数据:压缩数据、原始音频数据、原始视频数据。上述三种数据类型都可以通过ByteBuffers进行处理。但是需要提供一个Surface来为提高编解码体验(显示视频图像)。Surface直接使用本地视频数据buffers,而不是通过映射或复制的方式;因此,这样做的显得更加高效。通常在使用Surface的时候你不能够直接访问原始视频数据,{但是你可以使用ImageReader类来访问不可靠的解码后(或原始)的视频帧}。这可能仍然比使用ByteBuffers更加有效率,一些原始的buffers可能已经映射到了 direct ByteBuffers。当使用ByteBuffer模式,你可以通过使用Image类和getInput/OutputImage(int)来访问到原始视频数据帧。

Compressed Buffers

输入的buffers(用于解码)和输出的buffers(用于编码)包含的压缩数据是由媒体格式决定的。针对视频类型是一个压缩的单帧。针对音频数据通常是一个单个可访问单元(一个编码后的音频区段通常包含由特定格式类型决定的几毫秒音频数据),但这种通常也不是十分严格,一个buffer可能包含多个可访问的音频单元。在这两种情况下,buffers通常不开始或结束于任意的字节边界,而是结束于帧/可访问单元的边界。

Raw Audio Buffers

原始的音频数据buffers包含整个PCM音频帧数据,这是在通道顺序下每一个通道的样本。每一个样本就是一个 16-bit signed integer in native byte order。

Raw Video Buffers

ByteBuffer模式下视频buffers的的展现是由他们的 color format确定的。你可以通过调用 getCodecInfo().getCapabilitiesForType(…).colorFormats方法获得其支持的颜色格式数组。视频编解码器可能支持三种类型的颜色格式:

  • native raw video format: 被COLOR_FormatSurface标记,其可与输入或输出Surface一起使用。
  • flexible YUV buffers: 这些与输入/输出Surface一起使用,以及在ByteBuffer模式中,通过调用getInput/OutputImage(int)方法
  • other, specific formats: 通常只在ByteBuffer模式下被支持。有些颜色格式是特定供应商指定的。其他的一些被定义在 MediaCodecInfo.CodecCapabilities中。颜色格式是一个很灵活的格式,你仍然可以使用 getInput/OutputImage(int)方法。

从LOLLIPOP_MR1 API起所有视频编解码器支持灵活的YUV 4:2:0 buffers.

States

从概念上讲在整个生命周期中编解码器对象存在于三种状态之一:Stopped, Executing 或 Released。整体的Stoped状态实际是由三种状态的集成:Uninitialized, Configured以及 Error,而从概念上将Executing状态的执行时通过三个子状态:Flushed, Running 以及 End-of-Stream。

[图片上传失败...(image-5c907-1553137245958)]

当你使用工厂方法之一创建一个编解码器的时候,它的状态是处于Uninitialized状态。首先,你需要通过configure(…)方法配置它,以此进入Configured 状态。然后,通过调用start()方法转入Executing 状态。在这个状态下你可以通过上述buffer队列操作过程数据。

​ Executing状态包含三个子状态: Flushed, Running 以及 End-of-Stream。在调用start()方法后编解码器立即进入Flushed 的子状态,其同时包含所有的buffers。当第一个输入buffer一旦出队列,编解码器就转入Running 的子状态,这个状态占了编解码器的大部分生命周期时间。当你以end-of-stream marker标记一个入队列的输入buffer,则编解码器就转入End-of-Stream 子状态。在这个状态,编解码器不在接收以后传入的输入buffers,但它仍然产生输出buffers直到输出buffer到达end-of-stream状态。你可以在Executing状态的任何时候通过调用flush()状态返回Flushed 的子状态。或者通过调用stop()方法返回编解码器的Uninitialized 状态,因此这个编解码器需要再次configured 。当你使用完编解码器后,你必须调用release()方法释放其资源。

​ 在极少情况下编解码器可能会遇到错误并进入Error 状态。这个错误可能是在队列操作时返回一个错误的值或者有时候产生了一个异常导致的。通过调用reset()方法使编解码器再次可用。你可以在任何状态调用reset()方法使编解码器返回Uninitialized 状态。否则,调用release()方法进入最终的Released 状态。

Creation

通过MediaCodecList创建一个指定MediaFormat的MediaCodec对象。在解码文件或流时,你可以通过调用MediaExtractor.getTrackFormat方法获得所需的格式。通过调用MediaFormat.setFeatureEnabled方法你可以注入任意想要添加的特定特性,然后调用MediaCodecList.findDecoderForFormat方法获得可以处理这种特定媒体格式的编解码器的名字。最后,通过调用createByCodecName(String)方法创建一个编解码器。

注意,在API LOLLIPOP上,传递给MediaCodecList.findDecoder/EncoderForFormat的格式必须不能包含帧率。通过调用format.setString(MediaFormat.KEY_FRAME_RATE, null)方法清除任何存在于当前格式中的帧率。

你也可以通过调用createDecoder/EncoderByType(String)方法创建一个首选的MIME类型的编解码器。然而,不能够用于注入特性,以及创建了一个不能处理期望的特定媒体格式的编解码器。

Creating secure decoders

在版本API KITKAT_WATCH 及以前,secure 编解码器在MediaCodecList中没有列出来,但是仍然可以在这个系统中使用。secure 编解码器的存在只能够通过名字实例化,通过在通常的编解码器添加".secure"(所有的secure 解码器名称必须以".secure"结尾),如果系统上不存在指定的编解码器则createByCodecName(String)方法将抛出一个IOException 异常。

Initialization

在创建了编解码器后,如果你想异步地处理数据那么可以通过setCallback方法设置一个回调方法。然后,通过指定的媒体格式configure 这个编解码器。这段时间你可以为视频原始数据产生者(例如视频解码器)指定输出Surface。此时你也可以为secure 编解码器设置解码参数(详见MediaCrypto) 。最后,因为有些编解码器可以操作于多种模式,你必须指定是想让他作为一个解码器或编码器运行。

从API LOLLIPOP起,你可以在Configured 状态查询输入和输出格式的结果。在开始编解码前你可以通过这个结果来验证配置的结果,例如,颜色格式。

如果你想通过视频处理者处理原始输入视频buffers,一个处理原始视频输入的编解码器,例如视频编码器,在配置完成后通过调用createInputSurface()方法为你的输入数据创建一个目标Surface。通过先前创建的persistent input surface调用setInputSurface(Surface)配置这个编解码器。

Codec-specific Data

​ 有些格式,特别是ACC音频和MPEG4,H.264和H.265视频格式要求以包含特定数量的构建数据buffers或者codec-specific数据为前缀的实际数据。当处理这样的压缩格式时,这些数据必须在start()方法后和任何帧数据之前提交给编解码器。这些数据必须在调用queueInputBuffer方法时用BUFFER_FLAG_CODEC_CONFIG标记。

​ Codec-specific数据也可以被包含在传递给configure的ByteBuffer的格式里面,包含的keys是 "csd-0", "csd-1"等。这些keys通常包含在通过MediaExtractor获得的轨道MediaFormat中。这个格式中的Codec-specific数据将在接近start()方法时自动提交给编解码器;你不能显示的提交这些数据。如果这个格式不包含编解码器指定的数据,你也可以选择在这个编解码器中以这个格式所要求的并以正确的顺序传递特定数量的buffers来提交这些数据。还有,你也可以连接所有的codec-specific数据并作为一个单独的codec-config buffer提交。

​ Android 使用以下codec-specific数据buffers。{这些也被要求在轨道配置的格式轨道属性MediaMuxer中进行配置}。所有设置的参数以及被标记为(*)的codec-specific-data必须以 "\x00\x00\x00\x01"字符开头。

[图片上传失败...(image-e8373c-1553137245958)]

注意:当编解码器被立即flushed 或start之后不久,并且在任何输出buffer或输出格式变化被返回前需要特别地小心,编解码器的code specific 数据可能会在flush过程中丢失。为保证编解码器的正常运行,你必须在刷新后通过buffers再次提交被标记为BUFFER_FLAG_CODEC_CONFIGbuffers的这些数据。

​ 编码器(或者产生压缩数据的编解码器)将在任何合法的输出buffer前创建并返回被标记为 codec-config flag的codec specific data 。包含codec-specific-data 的Buffers含有没有意义的时间戳。

Data Processing

API中每一个编解码器维护一组被一个buffer-ID引用的输入和输出buffers。当成功调用start()方法后客户端将“拥有”输入和输出buffers。在同步模式下,通过调用dequeueInput/OutputBuffer(…) 从编解码器获得(具有所有权的)一个输入或输出buffer。在异步模式下,你可以通过MediaCodec.Callback.onInput/OutputBufferAvailable(…)的回调方法自动地获得可用的buffers.

在获得一个输入buffer,在使用解密方式下通过queueInputBuffer或queueSecureInputBuffer向编解码器填充相应数据。不要提交多个具有相同时间戳的输入bufers(除非他的codec-specific 数据时那样标记的)。

在异步模式下,编解码器将通过onOutputBufferAvailable的回调返回一个只读的输出buffer,或者在同步模式下响应dequeuOutputBuffer的调用。在输出buffer被处理后,调用releaseOutputBuffer方法中其中一个将这个buffer返回给编解码器。

在异步模式下,编解码器将通过onOutputBufferAvailable的回调返回一个只读的输出buffer,或者在同步模式下响应dequeuOutputBuffer的调用。在输出buffer被处理后,调用releaseOutputBuffer方法中其中一个将这个buffer返回给编解码器。

你不需要立即向编解码器重新提交或释放buffers,{获得的输入或输出buffers可能失去编解码器},当然这些行为依赖于设备情况。具体地说,编解码器可能推迟产生输出buffers直到输出的buffers被释放或重新提交。因此,尽可能保存可用的buffers。

根据API版本情况,你有三种方式处理相关数据:

[图片上传失败...(image-a3f2ee-1553137245958)]

Asynchronous Processing using Buffers

从LOLLIPOP API版本开始,首选的异步处理数据的方法是通过在调用configure前设置异步的回调方法。异步模式将间接地修改状态转换情况,因为你必须在flush()方法后调用start()方法将编解码器的状态转换为Running 子状态并开始接收输入buffers。同样,初始化调用start方法将编解码器的状态直接变化为Running 子状态并通过回调方法开始传递可用的输入buufers。

[图片上传失败...(image-5790fe-1553137245958)]

Synchronous Processing using Buffers

从LOLLIPOP API版本开始,在同步模式下使用编解码器你应该通过getInput/OutputBuffer(int) 和/或 getInput/OutputImage(int) 检索输入和输出buffers。这允许通过框架进行某些优化,例如,在处理动态内容过程中。如果你调用getInput/OutputBuffers()方法这种优化是不可用的。

注意,不要在同时使用buffers和buffer时产生混淆。特别地,仅仅在调用start()方法后或取出一个值为 INFO_OUTPUT_FORMAT_CHANGED的输出buffer ID后你才可以直接调用getInput/OutputBuffers方法。

End-of-stream Handling

当到达输入数据的结尾,你必须向这个编解码器在调用queueInputBuffer方法中指定BUFFER_FLAG_END_OF_STREAM 来标记输入数据。你可以在最后一个合法的输入buffer上做这些操作,或者提交一个以 end-of-stream 标记的额外的空的输入buffer。如果使用一个空的buffer,它的时间戳将被忽略。

编解码器将继续返回输出buffers,直到在这个设置在indequeueOutputBuffer 里的 MediaCodec.BufferInfo 中被同样标记为 end-of-stream 的输出流结束的时候或者通过onOutputBufferAvailable返回。这些可以被设置在最后一个合法的输出buffer上,或者在最后一个合法的buffer后的一个空buffer。那样的空buffer的时间戳将被忽略。

不要在输入流被标记为结束后提交额外的输入buffers,除非这个编解码器被flushed,或者stopped 和restarted。

Using an Output Surface

在使用一个输出Surface时,其数据处理基本上与处理ByteBuffer模式相同。然而,这个输出buffers将不可访问,并且被描述为null值。例如,调用getOutputBuffer/Image(int)将返回null,以及调用getOutputBuffers()将返回一个只包含null-s的数组。

Using an Input Surface

当使用输入Surface时,将没有可访问的输入buffers,因为这些buffers将会从输入surface自动地向编解码器传输。调用dequeueInputBuffer时将抛出一个IllegalStateException,调用getInputBuffers()将要返回一个不能写入的假的ByteBUffer[]数组。调用signalEndOfInputStream() 方法标记end-of-stream。调用这个方法后,输入surface将会立即停止向编解码器提交数据。

Seeking & Adaptive Playback Support

{视频解码器(通常是消费压缩视频数据的编解码器)关于seek和格式变化的行为是不同的,不管他们是否支持以及被配置为adaptive playback}。你可以通过调用CodecCapabilities.isFeatureSupported(String)方法来检查解码器是否支持adaptive playback 。只有在编解码器被配置在Surface上解码时支持Adaptive playback播放的解码器才被激活。

Stream Boundary and Key Frames

在调用start()或flush()方法后输入数据以合适的流边界开始是非常重要的:其第一帧必须是关键帧。一个关键帧能够通过其自身完全解码(针对大多数编解码器它是一个I帧),没有帧能够在关键帧之前或之后显示。

下面的表格针对不同的格式总结了合适的关键帧。

[图片上传失败...(image-56fcbf-1553137245958)]

MediaCodec API 说明

MediaCodec可以处理具体的视频流,主要有这几个方法:

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

推荐阅读更多精彩内容