MediaCodec底层原理剖析
MediaCodec类可用于访问底层媒体编解码器,即编码器/解码器组件。它是Android底层多媒体支持基础结构的一部分(通常与MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用)。
广义而言,编解码器处理输入数据以生成输出数据。它异步处理数据,并使用一组输入和输出缓冲区。在一个简单的级别上,您请求(或接收)一个空的输入缓冲区,将其填充数据并将其发送到编解码器进行处理。编解码器用完数据并将其转换为空的输出缓冲区之一。最后,您请求(或接收)已填充的输出缓冲区,使用其内容并将其释放回编解码器。
MediaCodec工作流程
一、MediaCodec处理的数据类型
编解码器对三种数据进行操作:压缩数据,原始音频数据和原始视频数据。可以使用ByteBuffer处理所有这三种数据,但是对于原始视频数据,应使用Surface来提高编解码器性能。 Surface使用本机视频缓冲区,而不将其映射或复制到ByteBuffer;因此,它效率更高。使用Surface时,通常不能访问原始视频数据,但是可以使用ImageReader类访问不安全的解码(原始)视频帧。这可能仍比使用ByteBuffer更为有效,因为某些本机缓冲区可能已映射到ByteBuffer#isDirect ByteBuffers。使用ByteBuffer模式时,可以使用Image类和getInput / OutputImage(int)访问原始视频帧。
1.1 压缩缓冲区
输入缓冲区(用于解码器)和输出缓冲区(用于编码器)根据MediaFormat#KEY_MIME包含压缩数据。对于视频类型,这通常是单个压缩的视频帧。对于音频数据,这通常是一个访问单元(一个编码的音频段,通常包含几毫秒的音频,如格式类型所规定),但是由于缓冲区中可能包含多个编码的访问单元,因此这一要求稍有放松。在任何一种情况下,缓冲区都不会在任意字节边界处开始或结束,而是在帧/访问单元边界处开始或结束,除非使用BUFFER_FLAG_PARTIAL_FRAME对其进行标记;
1.2 原始音频缓冲区
原始音频缓冲区包含PCM音频数据的整个帧,这是按通道顺序每个通道的一个样本。每个PCM音频样本都是16位带符号整数或浮点数(以本机字节顺序)。只有在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.
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;
}
1.3 原始视频缓冲区
在ByteBuffer模式下,视频缓冲区根据其MediaFormat#KEY_COLOR_FORMAT进行布局。您可以从getCodecInfo()。MediaCodecInfo#getCapabilitiesForType.CodecCapabilities#colorFormats获取作为数组的受支持颜色格式。视频编解码器可能支持三种颜色格式:
- native raw video format:
这由CodecCapabilities#COLOR_FormatSurface标记,可以与输入或输出Surface一起使用。- flexible YUV buffers:
通过使用getInput / OutputImage(int),它们可以与输入/输出Surface以及ByteBuffer模式一起使用。- other, specific formats:
通常仅在ByteBuffer模式下支持这些功能。某些颜色格式是特定于供应商的。其他在CodecCapabilities中定义。等效于灵活格式的颜色格式,您仍然可以使用getInput / OutputImage(int)。
1.4 在较旧的设备上访问原始视频字节缓冲区
在支持Build.VERSION_CODES.LOLLIPOP和Image之前,您需要使用MediaFormat#KEY_STRIDE和MediaFormat#KEY_SLICE_HEIGHT输出格式值来了解原始输出缓冲区的布局。
二、状态
在其生命周期内,编解码器从概念上讲处于以下三种状态之一:停止,执行或释放。停止的集体状态实际上是三个状态的集合:未初始化,已配置和错误,而执行状态从概念上讲经过三个子状态:Flushed,Running和Stream-of-Stream。
使用工厂方法之一创建编解码器时,编解码器处于未初始化状态。首先,您需要通过configure(…)对其进行配置,使其进入已配置状态,然后调用start()将其移至执行状态。在这种状态下,您可以通过上述缓冲区队列操作来处理数据。
执行状态具有三个子状态:Flushed,Running和End-of-Stream。在start()之后,编解码器立即处于Flushed子状态,其中包含所有缓冲区。一旦第一个输入缓冲区出队,编解码器将移至“运行”子状态,在此状态下将花费大部分时间。当您将输入缓冲区与流结束标记排队时,编解码器将转换为流结束子状态。在这种状态下,编解码器将不再接受其他输入缓冲区,但仍会生成输出缓冲区,直到在输出端达到流结束为止。在执行状态下,您可以使用flush()随时返回到“刷新”子状态。
调用stop()使编解码器返回未初始化状态,随后可以再次对其进行配置。使用编解码器完成操作后,必须通过调用release()释放它。
在极少数情况下,编解码器可能会遇到错误并进入“错误”状态。使用来自排队操作的无效返回值或有时通过异常来传达此信息。调用reset()使编解码器再次可用。您可以从任何状态调用它,以将编解码器移回“未初始化”状态。否则,请调用release()以移至终端的“释放”状态。
三、创建
使用MediaCodecList为特定的MediaFormat创建MediaCodec。解码文件或流时,可以从MediaExtractor#getTrackFormat获得所需的格式。使用MediaFormat#setFeatureEnabled注入要添加的所有特定功能,然后调用MediaCodecList#findDecoderForFormat以获取可以处理该特定媒体格式的编解码器的名称。最后,使用createByCodecName(String)创建编解码器。
您还可以使用createDecoder / EncoderByType(java.lang.String)为特定的MIME类型创建首选编解码器。但是,这不能用于注入特征,并且可能会创建无法处理特定所需媒体格式的编解码器。
3.1 创建安全的解码器
在Build.VERSION_CODES.KITKAT_WATCH和更低版本上,安全编解码器可能未在MediaCodecList中列出,但在系统上仍然可用。存在的安全编解码器只能通过名称实例化,方法是在常规编解码器的名称后附加“ .secure”(所有安全编解码器的名称都必须以“ .secure”结尾。)createByCodecName(String)将抛出IOException。编解码器不存在于系统上。
从Build.VERSION_CODES.LOLLIPOP开始,您应该使用媒体格式的CodecCapabilities#FEATURE_SecurePlayback功能来创建安全的解码器。
四、初始化
创建编解码器后,如果要异步处理数据,则可以使用setCallback设置回调。然后,使用特定的媒体格式配置编码解码器。这是您可以为视频制作者指定输出Surface的时候-生成原始视频数据的编解码器(例如,视频解码器)。这也是您可以设置安全编解码器的解密参数的时候(请参阅MediaCrypto)。最后,由于某些编解码器可以在多种模式下运行,因此必须指定是希望其用作解码器还是编码器。
从Build.VERSION_CODES.LOLLIPOP开始,您可以在Configured状态下查询生成的输入和输出格式。您可以使用它来验证最终的配置,例如颜色格式,然后再启动编解码器。
如果要使用视频使用者(处理视频输入的编解码器,例如视频编码器)本地处理视频输入的原始输入视频缓冲区,请在配置后使用createInputSurface()为输入数据创建目标Surface。或者,通过调用setInputSurface(Surface)将编解码器设置为使用以前创建的持久输入表面。
4.1 编解码器专用数据
某些格式,尤其是AAC音频和MPEG4,H.264和H.265视频格式,要求实际数据的前缀是许多包含设置数据或编解码器特定数据的缓冲区。处理此类压缩格式时,必须在start()之后和任何帧数据之前将此数据提交给编解码器。此类数据必须在对queueInputBuffer的调用中使用标志BUFFER_FLAG_CODEC_CONFIG进行标记。
特定于编解码器的数据也可以包含在传递给ByteBuffer条目进行配置的格式中,其中包含键“ csd-0”,“ csd-1”等。这些键始终包含在从MediaExtractor#getTrackFormat获得的轨道MediaFormat中。格式特定的编解码器数据在start()时自动提交给编解码器;您不得明确提交此数据。如果格式不包含编解码器专用数据,则可以根据格式要求选择使用正确数量的指定缓冲区使用指定数量提交。对于H.264 AVC,您还可以连接所有特定于编解码器的数据,并将其作为单个编解码器配置缓冲区提交。
Android使用以下特定于编解码器的数据缓冲区。还需要将它们设置为轨道格式,以进行正确的MediaMuxer轨道配置。每个参数集和标有(*)的编解码器专用数据部分必须以“ \ x00 \ x00 \ x00 \ x01”的起始代码开头。
编码器(或生成压缩数据的编解码器)将在标有codec-config标志的输出缓冲区中的任何有效输出缓冲区之前,创建并返回特定于编解码器的数据。包含编解码器特定数据的缓冲区没有有意义的时间戳。
五、数据处理
每个编解码器维护一组输入和输出缓冲区,这些输入和输出缓冲区由API调用中的缓冲区ID引用。成功调用start()后,客户端“不拥有”输入缓冲区或输出缓冲区。在同步模式下,调用dequeueInput / OutputBuffer(…)从编解码器获取(或拥有)输入或输出缓冲区。在异步模式下,您将通过Callback#onInputBufferAvailable / Callback#onOutputBufferAvailable回调自动接收可用缓冲区。
获取输入缓冲区后,将其填充数据,然后使用queueInputBuffer –如果使用解密,则将其提交给编解码器。不要提交带有相同时间戳的多个输入缓冲区(除非它是特定于编解码器的数据标记为这样)。
反过来,编解码器将通过Callback#onOutputBufferAvailable回调在异步模式下或响应于同步模式下的dequeueOutputBuffer调用返回只读输出缓冲区。处理完输出缓冲区后,调用releaseOutputBuffer方法之一将缓冲区返回到编解码器。
尽管不需要立即将缓冲区重新提交/释放到编解码器,但保持输入和/或输出缓冲区可能会使编解码器停顿,并且此行为与设备有关。特别是,编解码器可能会推迟生成输出缓冲区,直到所有未完成的缓冲区都已释放/重新提交。因此,请尝试尽可能少地保留可用缓冲区。
5.1 使用缓冲区的异步处理
从Build.VERSION_CODES.LOLLIPOP开始,首选方法是在调用configure之前通过设置回调来异步处理数据。异步模式会稍微改变状态转换,因为必须在flush()之后调用start()才能将编解码器转换为Running子状态并开始接收输入缓冲区。同样,在首次启动编解码器时,将直接移至“运行中”子状态,并开始通过回调传递可用的输入缓冲区。
MediaCodec通常在异步模式下像这样使用:
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
5.2 使用缓冲区的同步处理
从Build.VERSION_CODES.LOLLIPOP开始,即使在同步模式下使用编解码器,也应使用getInput / OutputBuffer(int)和/或getInput / OutputImage(int)检索输入和输出缓冲区。这允许框架进行某些优化,例如处理动态内容时。如果调用getInput / OutputBuffers(),则会禁用此优化。
MediaCodec通常在同步模式下按以下方式使用:
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
5.3 End-of-stream 处理
到达输入数据的末尾时,必须在对queueInputBuffer的调用中指定BUFFER_FLAG_END_OF_STREAM标志,以将其发送给编解码器。您可以在最后一个有效的输入缓冲区上执行此操作,或者通过提交另外一个空输入缓冲区并设置流结束标志来执行此操作。如果使用空缓冲区,则时间戳将被忽略。
编解码器将继续返回输出缓冲区,直到最终通过在dequeueOutputBuffer或通过Callback#onOutputBufferAvailable返回的BufferInfo中指定相同的流结束标志来最终指示输出流的结束为止。这可以在最后一个有效的输出缓冲区上设置,也可以在最后一个有效的输出缓冲区后的空白缓冲区上设置。这种空缓冲区的时间戳应该被忽略。
除非已刷新,停止或重新启动编解码器,否则在发出输入流结束信号后不要提交其他输入缓冲区。
5.4 使用Output Surface
使用输出Surface时,数据处理几乎与ByteBuffer模式相同;但是,输出缓冲区将不可访问,并表示为空值。例如。 getOutputBuffer / Image(int)将返回null,getOutputBuffers()将返回仅包含null-s的数组。
使用输出Surface时,可以选择是否在表面上渲染每个输出缓冲区。您有三种选择:
- Do not render the buffer:
- Render the buffer with the default timestamp:
- Render the buffer with a specific timestamp:
从Build.VERSION_CODES.M开始,默认时间戳为缓冲区的BufferInfo#presentationTimeUs(转换为纳秒)。在此之前未定义。
同样从Build.VERSION_CODES.M开始,您可以使用setOutputSurface动态更改输出Surface。
当将输出渲染到Surface时,Surface可以配置为丢弃过多的帧(Surface不会及时消耗掉)。或者可以将其配置为不丢失过多的帧。在后一种模式下,如果Surface无法足够快地消耗输出帧,则它将最终阻塞解码器。在Build.VERSION_CODES.Q之前,确切的行为是不确定的,除了View曲面(SuerfaceView或TextureView)始终掉落过多的帧。从Build.VERSION_CODES.Q开始,默认行为是丢弃过多帧。应用程序可以通过针对SDK Build.VERSION_CODES.Q并将键“ allow-frame-drop”设置为其配置格式,将非View曲面(例如ImageReader或SurfaceTexture)的这种行为取消选择。
5.5 渲染到曲面时的变换
如果将编解码器配置为“Surface”模式,则将自动应用任何裁剪矩形,MediaFormat#KEY_ROTATION和视频缩放模式,但有一个例外:
在Build.VERSION_CODES.M发行之前,软件解码器在渲染到Surface时可能尚未应用旋转。不幸的是,没有标准和简单的方法来识别软件解码器,或者是否通过尝试旋转来应用软件旋转。
5.6 使用一个输入Surface
使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会自动从输入表面传递到编解码器。调用dequeueInputBuffer会抛出IllegalStateException,并且getInputBuffers()返回一个不可写入的伪造ByteBuffer []数组。
调用signalEndOfInputStream()以信号流结束。调用后,输入表面将立即停止向编解码器提交数据。
参考:https://developer.android.google.cn/reference/android/media/MediaCodec
感谢关注公众号JeffMony,持续给你带来音视频方面的知识。