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

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

扫描二维码关注公众号,回复: 15511121 查看本文章
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

猜你喜欢

转载自blog.csdn.net/xiwenhec/article/details/89299829
今日推荐