MediaCodec类可以访问基础的媒体编解码器,即编码器/解码器组件。它是Android基础多媒体支持基础架构的一部分(通常跟MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用)。
从广义上讲,一个编解码器可以处理输入数据来产生输出数据。它使用一组输入和输出缓冲器异步处理数据。简单来说,你请求(或者接收)一个空的输入缓冲区,填充数据后发送到编解码器进行处理。编解码器使用这些数据进行转换,并输出到它其中一个输出缓冲器中。最后你请求(或接收)一个填充的输出缓冲器,消费掉里面的数据后释放回编解码器。其流程如下:
编解码器可处理三种数据:压缩数据、原始音频数据、原始视频数据。可以使用ByteBuffers处理这三种数据,但是你在处理原始视频数据时需要使用Surface来提升编解码的性能。Surface使用的是本地缓冲区,它不会映射或拷贝到ByteBuffers;所以,效率更高。在使用Surface时通常无法访问原始视频数据,但是你可以使用ImageReader类来访问不安全的解码(原始)视频帧。这可能仍然比使用ByteBuffers更有效率,因为一些本地缓冲区可能会直接映射到ByteBuffers。使用ByteBuffers模式时,你可以使用Image类和getInput/OutputImage(int)来访问原始视频帧。
输入缓冲区(用于解码器)和输出缓冲区(用于编码器)包含各种格式类型的压缩数据。对于视频这通常是一个压缩的视频帧,对于音频它通常是一个可访问的单元(一个编码音频段通常包含格式类型所指定的几毫秒音频),但是这个要求会适当放宽一点,如果一个缓冲区包含多个可访问的编码音频单元。在任何情况下,缓冲区不会在任意字节边界开始或结束,而是在帧/访问单元边界,除非使用了BUFFER_FLAG_PARTIAL_FRAME标记。
原始音频缓冲区包含所有的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 = formet.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;
}
ByteBuffers模式下,视频缓冲区根据其颜色格式进行布局。你可以从getCodecInfo().getCapabilitiesForType(…).colorFormats获取支持的颜色格式作为数组。视频编解码器可支持三种颜色格式。
自Build.VERSION_CODES.LOLLIPOP_MR1以来,所有的视频编解码器都支持灵活的YUV 4:2:0 缓冲区。
在支持 Build.VERSION_CODES.LOLLIPOP 和Image 之前,你需要使用 MediaFormat.KEY_STRIDE 和MediaFormat.KEY_SLICE_HEIGHT 输出格式值来了解原始输出缓冲区的布局。
MediaFormat.KEY_WIDTH和MediaFormat.KEY_HEIGHT键指定视频帧的大小;然而,对于大多数内容,视频(图片)仅占据视频帧的一部分。这由“裁剪矩形”表示
你需要使用以下键从输出格式获取原始输出图像的裁剪矩形。如果这些键不存在,则视频占据整个视频帧。在应用任何旋转之前,在输出帧的上下文中理解裁剪矩形。
计算视频帧的大小(旋转之前):
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");
}
另请注意,BufferInfo.offset的含义在各设备之间并不一致。在某些设备上,偏移量指向裁剪矩形的左上角像素,而在大多数设备上,它指向整个帧的左上角像素
在其生命周期中,编解码器存在三种状态:Stopped, Executing 和 Released。Stopped状态也分为三个子状态:Uninitialized, Configured 和Error。而Executing 也包含三个子状态:Flushed, Running 和 End-of-Stream。其状态图如下:
使用工厂方法创建编解码器时,编解码器处于Uninitialized状态。首先,您需要通过configure(…)进行配置,将其置于Configured状态,然后调用start()将其移至Executing状态。在此状态下,您可以通过上述缓冲区队列操作处理数据。
Executing 有三个子状态:Flushed,Running和End-of-Stream。在start()之后,编解码器立即处于Flushed子状态,它持有所有缓冲区。一旦第一个输入缓冲区出列,编解码器就会运行到 Running 子状态,Running状态占了其生命周期的大部分时间。当带有End-of-Stream标记的输入缓冲区入列时,编解码器将转换为End-of-Stream子状态。在此状态下,编解码器不再接受输入缓冲区,但仍会生成输出缓冲区,直到输出端到达End-of-Stream标记。在Executing 状态下使用flush(),你可以随时返回Flushed子状态
调用stop()将编解码器返回到Uninitialized状态,然后可以重新配置。 完成使用编解码器后,你必须通过调用release()来释放它。
在极少数情况下,编解码器可能会遇到错误并转到Error 状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用reset()使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回Uninitialized 状态。 否则,调用 release()动到终端Released 状态。
使用 MediaCodecList 为特定的 MediaFormat 创建 MediaCodec。解码文件或流时,你可以从 MediaExtractor.getTrackFormat获取所需的格式。使用 MediaFormat.setFeatureEnabled 注入要添加的任何特定功能,然后调用MediaCodecList.findDecoderForFormat 来获取可以处理特定媒体格式的编解码器的名称。最后,使用createByCodecName(String)创建编解码器
注意:在Build.VERSION_CODES.LOLLIPOP上,MediaCodecList.findDecoder / EncoderForFormat的格式不得包含帧速率。使用format.setString(MediaFormat.KEY_FRAME_RATE,null)清除格式中的任何现有帧速率设置。
您还可以使用createDecoder / EncoderByType(String)为特定MIME类型创建首选编解码器。但是,这不能用于注入功能,并且可能会创建无法处理特定所需媒体格式的编解码器。
在Build.VERSION_CODES.KITKAT_WATCH及更早版本中,安全编解码器可能未在MediaCodecList中列出,但可能仍在系统上可用。存在的安全编解码器只能通过名称实例化,方法是将“.secure”附加到常规编解码器的名称(所有安全编解码器的名称必须以“.secure”结尾。)createByCodecName(String)如果编解码器不存在于系统中,将抛出IOException。
从Build.VERSION_CODES.LOLLIPOP开始,您应该使用媒体格式中的MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback功能来创建安全解码器。
创建编解码器后,如果要异步处理数据,可以使用setCallback设置回调。然后,使用特定媒体格式配置编解码器。这时您可以为视频制作者指定输出Surface - 生成原始视频数据的编解码器(例如视频解码器)。您也可以为安全编解码器设置解密参数(请参阅MediaCrypto)。最后,由于某些编解码器可以在多种模式下运行,因此必须指定是要将其用作解码器还是编码器。
从Build.VERSION_CODES.LOLLIPOP开始,您可以在Configured状态下查询生成的输入和输出格式。您可以在启动编解码器之前使用它来验证结果配置,例如颜色格式。
如果要与视频使用者本地处理原始输入视频缓冲区 - 处理原始视频输入的编解码器(如视频编码器),请在配置后使用createInputSurface()为输入数据创建目标Surface。或者,通过调用setInputSurface(Surface)来设置编解码器以使用先前创建的持久输入Surface。
某些格式,特别是AAC音频和MPEG4,H.264和H.265视频格式要求实际数据以包含设置数据或编解码器特定数据的多个缓冲区为前缀。处理此类压缩格式时,必须在start()之后和任何帧数据之前将此数据提交给编解码器。必须在调用queueInputBuffer时使用标志BUFFER_FLAG_CODEC_CONFIG标记此类数据。
特定于编解码器的数据也可以包含在通过键“csd-0”,“csd-1”等的ByteBuffer条目中配置的格式中。这些键总是包含在从MediaExtractor获得的轨道MediaFormat中。格式的编解码器特定数据在start()时自动提交给编解码器;你不能明确提交这些数据。如果格式不包含编解码器特定数据,则可以根据格式要求选择以正确顺序使用指定数量的缓冲区提交。对于H.264 AVC,您还可以连接所有特定于编解码器的数据,并将其作为单个编解码器配置缓冲区提交。
Android使用以下特定于编解码器的数据缓冲区。这些也需要以轨道格式设置,以便正确配置MediaMuxer轨道。每个参数集和标有(*)的编解码器特定数据部分必须以起始代码“\x00 \x00 \x00 \x01”开头。
注意:如果在返回任何输出缓冲区或输出格式更改之前立即或在启动后不久刷新编解码器,则必须小心,因为在刷新期间编解码器特定数据可能会丢失。在进行此类刷新后,必须使用标记为BUFFER_FLAG_CODEC_CONFIG的缓冲区重新提交数据,以确保正确的编解码器操作。
编码器(或生成压缩数据的编解码器)将在标记有codec-config标志的输出缓冲区中的任何有效输出缓冲区之前创建并返回编解码器特定数据。包含特定于编解码器的数据的缓冲区没有有意义的时间戳。
每个编解码器都维护一组输入和输出缓冲区,这些缓冲区由API调用中的缓冲区ID引用。在成功调用start()之后,客户端“既不拥有”输入也不拥有输出缓冲区。在同步模式下,调用dequeueInput / OutputBuffer(…)以从编解码器获取(获得所有权)输入或输出缓冲区。在异步模式下,您将通过MediaCodec.Callback.onInput / OutputBufferAvailable(…)回调自动接收可用缓冲区。
获取输入缓冲区后,使用queueInputBuffer将其填入数据或使用queueSecureInputBuffer(如果使用解密)将其提交给编解码器。不要提交具有相同时间戳的多个输入缓冲区(除非它是标记为此类的特定于编解码器的数据)。
反过来,编解码器将通过异步模式下的onOutputBufferAvailable回调返回只读输出缓冲区,或者响应同步模式下的dequeueOutputBuffer调用。处理完输出缓冲区后,调用releaseOutputBuffer方法之一将缓冲区返回给编解码器。
虽然您不需要立即向编解码器重新提交/释放缓冲区,但保持输入和/或输出缓冲区可能会使编解码器停止运行,并且此行为取决于设备。**具体地说,编解码器可能会在生成输出缓冲区之前停止,直到所有未完成的缓冲区都被释放/重新提交。**因此,尽可能少地保持可用的缓冲区。
从Build.VERSION_CODES.LOLLIPOP开始,首选方法是通过在调用configure之前设置回调来异步处理数据。异步模式稍微改变状态转换,因为必须在flush()之后调用start()以将编解码器转换为Running子状态并开始接收输入缓冲区。类似地,在初始调用启动时,编解码器将直接移动到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();
从Build.VERSION_CODES.LOLLIPOP开始,即使在同步模式下使用编解码器,也应使用getInput / OutputBuffer(int)和/或getInput / OutputImage(int)检索输入和输出缓冲区。这允许框架的某些优化,例如,处理动态内容时。如果调用getInput / OutputBuffers(),则禁用此优化。
注意:不要混合使用缓冲区和缓冲区数组的方法。具体来说,只能在start()之后或在将输出缓冲区ID以INFO_OUTPUT_FORMAT_CHANGED值出列后直接调用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();
此方式标记已过时,不再记录
当您到达输入数据的末尾时,必须通过在queueInputBuffer调用中指定BUFFER_FLAG_END_OF_STREAM标志来将其发送到编解码器。您可以在最后一个有效的输入缓冲区上执行此操作,或者通过设置一个带有end-of-stream标志的其他空输入缓冲区来执行此操作。如果使用空缓冲区,则将忽略时间戳。
编解码器将继续返回输出缓冲区,直到它最终通过在dequeueOutputBuffer中设置的MediaCodec.BufferInfo中指定相同的end-of-stream标志或通过onOutputBufferAvailable返回来通知输出流的结尾。这可以在最后一个有效输出缓冲区上设置,也可以在最后一个有效输出缓冲区后的空缓冲区上设置。应忽略此类空缓冲区的时间戳。
除非编解码器已刷新,或已停止并重新启动,否则在发出输入流结束信号后,请勿提交其他输入缓冲区。
使用输出Surface时,数据处理几乎与ByteBuffer模式相同;但是,输出缓冲区将不可访问,并表示为空值。例如。 getOutputBuffer / Image(int)将返回null,getOutputBuffers()将返回仅包含null-s的数组。
使用输出Surface时,可以选择是否在曲面上渲染每个输出缓冲区。你有三个选择:
从Build.VERSION_CODES.M开始,默认时间戳是缓冲区的显示时间戳(转换为纳秒)。之前没有定义。
此外,自Build.VERSION_CODES.M起,您可以使用setOutputSurface动态更改输出Surface。
如果编解码器配置为“曲面”模式,则会自动应用任何裁剪矩形,旋转和视频缩放模式,但有一个例外:
在Build.VERSION_CODES.M版本之前,软件解码器在渲染到Surface上时可能没有应用旋转。不幸的是,没有标准和简单的方法来识别软件解码器,或者它们是否应用旋转而不是通过尝试它。
注意,在Surface上显示输出时不考虑像素长宽比。这意味着如果您使用的是VIDEO_SCALING_MODE_SCALE_TO_FIT模式,则必须定位输出Surface,使其具有正确的最终显示宽高比。相反,您只能对具有方形像素(像素长宽比或1:1)的内容使用VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式。
另请注意,从Build.VERSION_CODES.N版本开始,对于旋转90度或270度的视频,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能无法正常工作。
设置视频缩放模式时,请注意每次输出缓冲区更改后必须复位。由于不推荐使用INFO_OUTPUT_BUFFERS_CHANGED事件,因此可以在每次输出格式更改后执行此操作。
使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会自动从输入表面传递到编解码器。调用dequeueInputBuffer将抛出IllegalStateException,并且getInputBuffers()返回一个绝不能写入的伪造的ByteBuffer []数组。
调用signalEndOfInputStream()来表示流结束。输入表面将在此调用后立即停止向编解码器提交数据。
视频解码器(以及使用压缩视频数据的一般编解码器)在搜索和格式改变方面表现不同,无论它们是否支持并配置为自适应回放。您可以通过CodecCapabilities.isFeatureSupported(String)检查解码器是否支持自适应播放。只有将编解码器配置为在Surface上解码时,才会激活对视频解码器的自适应回放支持。
重要的是start()或flush()之后的输入数据在合适的流边界处开始:第一帧必须是关键帧。关键帧可以完全自己解码(对于大多数编解码器,这意味着I帧),并且在关键帧之后没有要显示的帧指的是关键帧之前的帧。
为了开始解码与先前提交的数据不相邻的数据(即在搜索之后),你必须刷新解码器。由于所有输出缓冲区在刷新时立即被撤销,因此您可能需要首先发出信号,然后在调用flush之前等待流结束。重要的是,刷新之后的输入数据在合适的流边界/关键帧处开始。
注意:刷新后提交的数据格式不得更改; flush()不支持格式不连续;为此,一个完整的停止() - configure(…) - start()循环是必要的。
另请注意:如果在start()后过早刷新编解码器 - 通常,在收到第一个输出缓冲区或输出格式更改之前 - 您需要将编解码器特定数据重新提交给编解码器。有关详细信息,请参阅编解码器特定数据部分。
为了开始解码与先前提交的数据不相邻的数据(即,在搜索之后),不必刷新解码器;但是,不连续后的输入数据必须从合适的流边界/关键帧开始。
对于某些视频格式 - 即H.264,H.265,VP8和VP9 - 也可以在中途改变图像大小或配置。为此,您必须将整个新的特定于编解码器的配置数据与关键帧一起打包到单个缓冲区(包括任何起始代码)中,并将其作为常规输入缓冲区提交。
在发生图片大小更改之后以及返回任何具有新大小的帧之前,您将从dequeueOutputBuffer或onOutputFormatChanged回调中收到INFO_OUTPUT_FORMAT_CHANGED返回值。
注意:就像编解码器特定数据的情况一样,在更改图片大小后立即调用flush()时要小心。如果您尚未收到图片尺寸更改的确认,则需要重复新图片尺寸的请求。
工厂方法createByCodecName和createDecoder / EncoderByType在失败时抛出IOException,您必须捕获或声明要传递。当从不允许它的编解码器状态调用方法时,MediaCodec方法抛出IllegalStateException;这通常是由于应用程序API使用不正确。涉及安全缓冲区的方法可能会抛出MediaCodec.CryptoException,它可以从MediaCodec.CryptoException.getErrorCode()获得更多错误信息。
内部编解码器错误导致MediaCodec.CodecException,这可能是由于媒体内容损坏,硬件故障,资源耗尽等等,即使应用程序正确使用API也是如此。接收CodecException时建议的操作可以通过调用MediaCodec.CodecException.isRecoverable()和MediaCodec.CodecException.isTransient()来确定:
isRecoverable()和isTransient()不会同时返回true。
其他介绍请参考MediaCodec。
查找资料时发现一片文章可能翻译的更加准确,原文地址