Android MediaCodec

MediaCodec是什么?

MediaCodec类为开发者提供了能访问到Android底层媒体Codec(Encoder/Decoder)的能力,它是Android底层多媒体基础架构的一部分(通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack一起使用)。

Codec示意图

从广义上来讲,Codec就是处理输入数据产生输出数据。它使用一组输入、输出缓冲区异步的处理数据,简而言之,你请求一个空的input Buffer,给它填充数据,然后将这个buffer送入Codec进行处理,Codec会消耗掉输入的数据,然后将InputBuffer转换成一空的Buffer以供下次请求时使用。最后,你请求一个填充满数据的outputBuffer,消费掉Buffer里的内容,然后释放掉这个Buffer返回给Codec

数据类型

Codec对三种类型类型的数据起作用:编码后的压缩数据,原始视频数据,原始音频数据。这三种类型的数据都可以通过ByteBuffer来传递给Codec,但是对于原始视频数据我们建议使用Surface来传递,这样可以提高Codec的性能,Surface使用的是native video buffer,不用映射或者拷贝成ByteBuffer,因此这样的方式更高效。当你使用Surface来传递原始视频数据时,也就无法获取到了原始视频数据,Android 提供了ImageReader帮助你获取到解码后的原始视频数据。这种方式可能仍然有要比ByteBuffer的方式更加高效,因为某些native video buffer会直接映射成byteBuffer。当然如果你ByteBuffer的模式,你可以使用Image类提供的getInput/OutputImage(int)来获取原始视频数据

压缩数据Buffer

Decoder输入的InputBuffer或者Encoder输出的outputBuffer包含的都是编码后的压缩数据,数据的压缩类型由MediaFormat#KEY_MIME指明。对于视频类型而言,这个数据通常是一个压缩后的视频帧。对于音频数据而言,通常是一个访问单元(一个编码的音频段,通常包含几毫秒的音频数据,数据类型format type 指定),有时候,一个音频单元对于一个buffer而言可能有点宽松,所以一个buffer里可能包含多个编码后的音频数据单元。无论Buffer包含的是视频数据还是音频数据,Buffer都不会再任意字节边界上开始或者结束,而是在帧(视频)或者单元(音频)的边界上开始或者结束。除非它们被BUFFER_FLAG_PARTIAL_FRAME标记。

原始音频Buffer

原始音频Buffer包含PCM音频数据的整个帧,是每一个通道按着通道顺序的采样数据。每一个采样按16Bit量化。

short[] getSamplesForChannel(MediaCodec codec , int bufferId , int channelIx){
    ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
    MediaFormat format = codec.getOutputFormat(bufferId);
    ShortBuffer samples = oputBuffer.order(ByteBuffer.nativeOrder()).asShortBuffer();
    int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
    if(channelIx <0 || channelIx >= numChannels){
        return null;
    }
    shor[] res = new short[samples.remaining()/numChannels];
    for(int i = 0 ; i < res.length ; i++){
        res[i] = samples.get(i*numChannels+channelIx);
    }
    return res;
}

原始视频数据Buffer

ByteBuffer模式下,视频数据的排布由MediaFormat#KEY_COLOR_FORMAT指定,我们可以通过getCodecInfo().MediaCodecInfo#getCapabilitiesForType.CodecCapabilities#colorFormats获取到一个设备支持的color format数组。视频Codec可能支持三种类型的Color Format:

  • native raw video format:由CodecCapabilities#COLOR_FormatSurface标记,可以用做Input/output Surface.
  • flexible YUV buffer(例如:CodecCapabilities#COLOR_FormatYUV420Flexible):这些buffer能用作input/output surface,同时也能用于byte buffer模式,通过getInput/OutputBuffer(int)
  • 其他指定的格式:这些数据通常只支持byteBuffer模式。

Build.VERSION_CODES.LOLLIPOP_MR1开始所有的视频Codec都支持flexible YUV 4:2:0

在一些老设备上如何获取原始视频数据buffer

对于Build.VERSION_CODES.LOLLIPOP之前并且支持Image类时,我们需要使用MediaFormat#KEY_STRIDEMediaFormat#KEY_SLICE_HEIGHT的值去理解输出的原始视频数据的布局。

注意:在某些设备上slice-height被标记为0,这可能意味着slice-height的值和frame height的值相同,或者和frame height按着2的幂对齐。很遗憾,没有一个标准或者简单的方式告诉我们slice-height的真实值。此外,在planar 格式下U plane的stride也没有明确的指定,虽然通常它是slice height的一半。

键值MediaFormat#KEY_WIDTHMediaFormat#KEY_HEIGHT指明了视频Frame的size。然而,对于大多数用于编码的视频图像,他们只占用了video Frame的一部分。这部分用一个'crop rectangle来表示。

我们需要用下面的一些keys从获取原始视频数据的crop rectangle,如果out format中没有包含这些keys,则表示视频占据了整个video Frame,这个crop rectangle的解释应该立足于应用任何MediaFormat#KEY_ROTATION之前。

Format Key Type Description
"crop-left" Integer The left-coordinate (x) of the crop rectangle
"crop-top" Integer The top-coordinate (y) of the crop rectangle
"crop-right" Integer The right-coordinate (x) MINUS 1 of the crop rectangle
"crop-bottom" Integer The bottom-coordinate (y) MINUS 1 of the crop rectangle

下面是在旋转之前计算视频的尺寸的案例:

MediaFormat format = decoder.getOutputFormat(...);
int width = format.getInteger(MediaFormat.KEY_WIDTH);
if(format.containsKey("crop-left") && format.containsKey("crop-right")){
    width = format.getInteger("crop-right")+1-format.getInteger("crop-left");
}
int height = format.getInteger(MediaFormat.KEY_WIDTH);
if(format.containsKey("crop-top") && format.containsKey("crop-bottom"){
    height = format.getInteger("crop-bottom")+1-format.getInteger("crop-top"); 
}

Codec的状态

从概念上讲Codec的声明周期存在三种状态:StopedExecutingReleasedStoped状态是一个集合状态,它聚合了三种状态:UninitializedConfigured,和Error,同时Executing状态的处理也是通过三个子状态来完成:FlushedRunningEnd-of-Stream

Codec状态转换图

当我们用工厂方法创建出一个Codec后,Codec就处于Uninitialized状态,首先我们需要通过Configure(...)去Configure这个Codec,这将会将Codec转换成Configured状态,然后我们可以调用start()函数,将Codec转换成Executing状态,在这个状态下我们才能通过buffer处理数据。

Executing状态有三个子状态:Flushed,Running,和End-of-Stream,当我们调用玩Start()函数后,Codec就立刻进入Flushed子状态,这个状态下,它持有全部的buffer,只要第一个Input buffer被dequeued,Codec就转变成Running子状态,这个状态占据了Codec的生命周期的绝大部分。当入队一个带有 end-of-stream标志的InputBuffer后,Codec将转换成End of Stream子状态,在这个状态下,Codec将不会再接收任何输入的数据,但是仍然会产生output buffer ,直到end-of-Stream标记的buffer被输出。我们可以在Executing状态的任何时候,使用flush()函数,将Codec切换成Flushed状态。

调用stop()函数会将Codec返回到Uninitialized状态,这样我们就可以对Codec进行重新配置,当你用完了Codec后,你必须要调用release()函数去释放这个Codec

在极少数情况下,Codec可能也会遇到错误,此时Codec将会切换到Error状态,我们可以通过queuing操作获取到一个无效的返回值,或者有时会通过异常来的得知Codec发生了错误。通过调用reset()函数,将Codec进行重置,这样Codec将切换成Uninitalized状态,我们可以在任何状态下调用rest()函数将Codec将切换成Uninitalized`状态。

Codec的创建

使用MediaCodecList创建一个指定MediaFormat的MediaCodec。当我们解码一个文件或者一个流时,我们可以通过MediaExtractor#getTrackFormat获取期望的Fromat,同时我们可以通过MediaFormat#setFeatureEnabledCodec注入任何我们想要的特性。然后调用MediaCodecList#findDecoderForFormat获取能够处理对应format数据Codec的name,最后我们使用createByCodecName(String)创建出这个Codec

注意:在 Build.VERSION_CODES.LOLLIPOP ,MediaCodecList.findDecoder/EncoderForFormat 获取的format不能包含MediaFormat#KEY_FRAME_RATE,用format.setString(MediaFormat.KEY_FRAME_RATE, null)这个函数可以清楚已经在这个format中设置的frame rate。

我们也可以使用createDecoder/EncoderByType(java.lang.String)函数来创建指定的MIME类型的Codec,但是这样我们无法向其中注入一些指定的特性,这样创建的Codec可能不能处理我们期望的媒体类型数据。

创建安全的Decoder

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

推荐阅读更多精彩内容