前言
MediaCodec大坑绝对是大坑,坑的很直溜。
本系列是参考 [奇卓社]的文章,喜欢的小伙伴可以直接去看[奇卓社]。
参考文章
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)。
在支持Build.VERSION_CODES.LOLLIPOP
和Image
之前,您需要使用MediaFormat#KEY_STRIDE和MediaFormat#KEY_SLICE_HEIGHT
输出格式值来了解原始输出缓冲区的布局。
MediaFormat#KEY_WIDTH
和MediaFormat#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状态。
网友评论