MediaCodec是什么?
MediaCodec类为开发者提供了能访问到Android底层媒体Codec(Encoder/Decoder)的能力,它是Android底层多媒体基础架构的一部分(通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack一起使用)。
从广义上来讲,
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_STRIDE
和MediaFormat#KEY_SLICE_HEIGHT
的值去理解输出的原始视频数据的布局。
注意:在某些设备上slice-height被标记为0,这可能意味着slice-height的值和frame height的值相同,或者和frame height按着2的幂对齐。很遗憾,没有一个标准或者简单的方式告诉我们slice-height的真实值。此外,在planar 格式下U plane的stride也没有明确的指定,虽然通常它是slice height的一半。
键值MediaFormat#KEY_WIDTH
和MediaFormat#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的声明周期存在三种状态:Stoped
,Executing
,Released
。Stoped
状态是一个集合状态,它聚合了三种状态:Uninitialized
,Configured
,和Error
,同时Executing
状态的处理也是通过三个子状态来完成:Flushed
,Running
,End-of-Stream
。
当我们用工厂方法创建出一个
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#setFeatureEnabled
为Codec
注入任何我们想要的特性。然后调用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
可能不能处理我们期望的媒体类型数据。
网友评论