MediaCodec专题(一):简介

前言

MediaCodec大坑绝对是大坑,坑的很直溜。
本系列是参考 [奇卓社]的文章,喜欢的小伙伴可以直接去看[奇卓社]。

参考文章

  1. 官方MediaCodec
  2. Android视频处理之MediaCodec-1-简介

MediaCodec是什么?

   从API 16(Android 4.1)开始,Android提供了MediaCodec类以便开发者更加灵活的处理音视频的编解码,MediaCodec类可以访问底层媒体编解码器框架(StageFright或openMAX),即编码器/解码器组件。这是Android low-level多媒体支持基础设施的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.一起使用))。

MediaCodec 大纲

一图胜千言::


MediaCodec工作原理.png

广义而言,编解码器处理输入数据以生成输出数据。
它异步处理数据,并使用一组输入和输出缓冲区。
在一个简单的级别上,您请求(或接收)一个空的输入缓冲区,将其填充数据并将其发送到编解码器进行处理。
编解码器用完了数据并将其转换为空的输出缓冲区之一。
最后,您请求(或接收)已填充的输出缓冲区,使用其内容并将其释放回编解码器。

数据类型

编解码器对三种数据进行操作:compressed data 压缩数据(即为经过H264. H265等编码的视频数据或AAC等编码的音频数据),raw audio data 原始音频数据和raw video data 原始视频数据。
三种类型的数据均可以利用ByteBuffers进行处理,但是对于原始视频数据应提供一个Surface以提高编解码器的性能。
Surface直接使用本地视频数据缓存(native video buffers),而没有映射或复制数据到ByteBuffers,因此,这种方式会更加高效。在使用Surface的时候,通常不能直接访问原始视频数据,但是可以使用ImageReader类来访问非安全的解码(原始)视频帧。这仍然比使用ByteBuffers更加高效,因为一些本地缓存(native buffer)可以被映射到 direct ByteBuffers。当使用ByteBuffer模式,你可以利用Image类和getInput/OutputImage(int)方法来访问到原始视频数据帧。

Compressed Buffers 压缩缓冲区

输入缓冲区(用于解码器)和输出缓冲区(用于编码器)根据MediaFormat#KEY_MIME包含压缩数据。
对于视频类型,通常是单个压缩视频帧。
对于音频数据,这通常是一个访问单元(一个编码的音频段,通常包含几毫秒的音频,这由格式类型决定),但是由于缓冲区中可能包含多个编码的访问单元,因此这一要求稍微有所放松。
无论哪种情况,缓冲区都不会在任意字节边界处开始或结束,而不会在帧/访问单元边界处开始或结束,除非使用BUFFER_FLAG_PARTIAL_FRAME对其进行了标记。

Raw Audio Buffers 原始音频缓冲区

原始的音频数据缓存包含完整的PCM(脉冲编码调制)音频数据帧,这是每一个通道按照通道顺序的一个样本。
每一个PCM音频样本都是一个按照本机字节顺序的16位带符号整数或浮点数(16 bit signed integer or a float, in native byte order.)。

使用浮点PCM编码的原始音频缓冲区 :仅当在MediaCodec configure(…)期间将MediaFormat的MediaFormat#KEY_PCM_ENCODING设置为AudioFormat#ENCODING_PCM_FLOAT并由解码器的getOutputFormat()或编码器的getInputFormat()确认时,才可以使用浮点PCM编码的原始音频缓冲区。

检查MediaFormat中的float PCM的示例方法如下:

static boolean isPcmFloat(MediaFormat format) {
 return format.getInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
     == AudioFormat.ENCODING_PCM_FLOAT;
}

为了在短数组中提取包含16位带符号整数音频数据的缓冲区的一个通道,可以使用以下代码:

// Assumes the buffer PCM encoding is 16 bit. 假定缓冲区PCM编码为16位。
 short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
  ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
  MediaFormat format = codec.getOutputFormat(bufferId);
  ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
  int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
  if (channelIx < 0 || channelIx >= numChannels) {
    return null;
  }
  short[] res = new short[samples.remaining() / numChannels];
  for (int i = 0; i < res.length; ++i) {
    res[i] = samples.get(i * numChannels + channelIx);
  }
  return res;
 }

Raw Video Buffers 原始视频缓冲区

在ByteBuffer模式下,视频缓冲区根据他们的MediaFormat#KEY_COLOR_FORMAT进行布局。

你可以通过调用getCodecInfo().MediaCodecInfo#getCapabilitiesForType.CodecCapabilities#colorFormats方法获得编解码器支持的颜色格式数组。
视频编解码器可以支持三种类型的颜色格式:

  • native raw video format 原始原始视频格式:CodecCapabilities#COLOR_FormatSurface标记,可以与输入或输出Surface一起使用。
  • flexible YUV buffers (such as CodecCapabilities#COLOR_FormatYUV420Flexible) 灵活的YUV缓冲区: 利用一个输入或输出Surface,或者在ByteBuffer模式下,可以通过调用getInput/OutputImage(int)方法使用这些格式。
  • other, specific formats 其他特定格式:通常只在ByteBuffer模式下被支持。有些颜色格式是特定供应商指定的。其他的一些被定义在 MediaCodecInfo.CodecCapabilities中。这些颜色格式同 flexible format相似,你仍然可以使用 getInput/OutputImage(int)方法(API 21)。

从Android 5.1(API 22)开始,所有的视频编解码器都支持灵活的YUV4:2:0缓存(flexible YUV420 buffers)。

\color{red}{注意:在较旧的设备上访问原始视频字节缓冲区。 以下内容可以略过::}

在支持Build.VERSION_CODES.LOLLIPOPImage之前,您需要使用MediaFormat#KEY_STRIDE和MediaFormat#KEY_SLICE_HEIGHT输出格式值来了解原始输出缓冲区的布局。

MediaFormat#KEY_WIDTHMediaFormat#KEY_HEIGHT键指定视频帧的大小; 但是,在大多数情况下,视频(图片)仅占据视频帧的一部分。
这由“裁剪矩形”表示。 这由“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

The right and bottom coordinates can be understood as the coordinates of the right-most valid column/bottom-most valid row of the cropped output image.
右坐标和底坐标可以理解为裁剪后的输出图像的最右边的有效列/最下面的有效行的坐标。

旋转前视频帧的大小可以这样计算:

 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_HEIGHT);
 if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
    height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
 }

生命周期

在编解码器的生命周期内有三种理论状态:停止态-Stopped、执行态-Executing、释放态-Released。

  • 停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。
  • 执行状态(Executing)在概念上会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)。
    一图胜千言::
    生命周期
  • 当你使用任意一种工厂方法(factory methods)创建了一个编解码器,此时编解码器处于未初始化状态(Uninitialized)。
    首先,你需要使用configure(…)方法对编解码器进行配置,这将使编解码器转为配置状态(Configured)。
    然后调用start()方法使其转入执行状态(Executing)。在这种状态下你可以通过上述的缓存队列操作处理数据。

  • 执行状态(Executing)包含三个子状态: 刷新(Flushed)、运行( Running) 以及流结束(End-of-Stream)。
    在调用start()方法后编解码器立即进入刷新子状态(Flushed),此时编解码器会拥有所有的缓存。
    一旦第一个输入缓存(input buffer)被移出队列,编解码器就转入运行子状态(Running),编解码器的大部分生命周期会在此状态下度过。
    当你将一个带有end-of-stream 标记的输入缓存入队列时,编解码器将转入流结束子状态(End-of-Stream)。在这种状态下,编解码器不再接收新的输入缓存,但它仍然产生输出缓存(output buffers)直到end-of- stream标记到达输出端(直到在输出端达到流结束为止)。你可以在执行状态(Executing)下的任何时候通过调用flush()方法使编解码器重新返回到刷新子状态(Flushed)。

  • 通过调用stop()方法使编解码器返回到未初始化状态(Uninitialized),此时这个编解码器可以再次重新配置 。当你使用完编解码器后,你必须调用release()方法释放其资源。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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