目录
一、什么是MediaCodec
二、视频编码的最低质量底线
三、数据类型
压缩缓冲区
原始音频缓冲区
原始视频缓冲区
在旧设备上访问原始视频字节缓冲区
四、状态
五、创建
创建安全解码器
六、初始化
特定于编解码器的数据
七、数据处理
使用缓冲区的异步处理
使用缓冲区的同步处理
使用缓冲区数组的同步处理(已弃用)
流结束处理
使用输出Surface
渲染到表面时的变换
使用输入Surface
搜索和自适应播放支持
流边界和关键帧
对于不支持自适应播放的解码器(包括不解码到 Surface 时)
对于支持并配置为自适应播放的解码器
错误处理
原文来源于:MediaCodec | Android Developers
MediaCodec 类可用于访问低级媒体编解码器,即编码器/解码器组件。 它是 Android 底层多媒体支持基础架构的一部分(通常与 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface 和 AudioTrack 一起使用。)
从广义上讲,编解码器处理输入数据以生成输出数据。 它异步处理数据并使用一组输入和输出缓冲区。 在简单的层面上,您请求(或接收)一个空的输入缓冲区,用数据填充它并将其发送到编解码器进行处理。 编解码器用完数据并将其转换为空输出缓冲区之一。 最后,您请求(或接收)一个填充的输出缓冲区,使用其内容并将其释放回编解码器。
从 Build.VERSION_CODES.S 开始,Android 的 Video MediaCodecs 强制执行最低质量标准。目的是消除质量差的视频编码。当编解码器处于可变比特率 (VBR) 模式时执行此质量底线;当编解码器处于恒定比特率 (CBR) 模式时则不适用。质量底线的执行也仅限于特定的尺寸范围;此尺寸范围目前适用于大于 320x240 到 1920x1080 的视频分辨率。
当此质量底线生效时,编解码器和支持框架代码将确保生成的视频至少具有“一般”或“良好”质量。用于选择这些目标的指标是 VMAF(视频多方法评估函数),所选测试序列的目标分数为 70。
典型的效果是某些视频会生成比原始配置更高的比特率。这对于以非常低的比特率配置的视频最为显着;编解码器会使用被确定为更有可能生成“一般”或“良好”质量视频的比特率。另一种情况是视频包含非常复杂的内容(大量动作和细节);在这种配置中,编解码器将根据需要使用额外的比特率以避免丢失所有内容的更精细的细节。
这个质量底线不会影响以高比特率捕获的内容(高比特率应该已经为编解码器提供了足够的容量来编码所有细节)。质量底线不对 CBR 编码进行操作。质量底线目前不适用于 320x240 或更低的分辨率,也不适用于分辨率高于 1920x1080 的视频。
编解码器对三种数据进行操作:压缩数据、原始音频数据和原始视频数据。所有三种数据都可以使用 ByteBuffers 进行处理,但您应该使用 Surface 处理原始视频数据以提高编解码器性能。 Surface 使用原生视频缓冲区,无需将它们映射或复制到 ByteBuffers;因此,它的效率要高得多。使用 Surface 时,您通常无法访问原始视频数据,但您可以使用 ImageReader 类访问不安全的解码(原始)视频帧。这可能仍然比使用 ByteBuffers 更有效,因为一些本机缓冲区可能会映射到直接的 ByteBuffers。使用 ByteBuffer 模式时,您可以使用 Image 类和 getInput/OutputImage(int) 访问原始视频帧。
输入缓冲区(用于解码器)和输出缓冲区(用于编码器)根据格式的类型包含压缩数据。对于视频类型,这通常是单个压缩视频帧。对于音频数据,这通常是一个单一的访问单元(一个编码的音频段,通常包含由格式类型规定的几毫秒的音频),但这个要求稍微放宽,因为缓冲区可能包含多个编码的音频访问单元。在任何一种情况下,缓冲区都不会在任意字节边界上开始或结束,而是在帧/访问单元边界上开始或结束,除非它们被标记为 BUFFER_FLAG_PARTIAL_FRAME。
原始音频缓冲区包含完整的 PCM 音频数据帧,按通道顺序为每个通道提供一个样本。每个 PCM 音频样本是一个 16 位有符号整数或一个浮点数,按本机字节顺序排列。只有在 MediaCodec configure(…) 期间将 MediaFormat 的 MediaFormat#KEY_PCM_FLOAT 设置为 AudioFormat#ENCODING_PCM_FLOAT 并由解码器的 getOutputFormat() 或编码器的 getInputFormat() 确认时,浮点 PCM 编码中的原始音频缓冲区才可能。在 MediaFormat 中检查浮动 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;
}
在 ByteBuffer 模式下,视频缓冲区根据它们的颜色格式进行布局。您可以从 getCodecInfo().getCapabilitiesForType(...).colorFormats 以数组形式获取支持的颜色格式。视频编解码器可能支持三种颜色格式:
自 Build.VERSION_CODES.LOLLIPOP_MR1 以来,所有视频编解码器都支持灵活的 YUV 4:2:0 缓冲区。
在 Build.VERSION_CODES.LOLLIPOP 和 Image 支持之前,您需要使用 MediaFormat#KEY_STRIDE 和 MediaFormat#KEY_SLICE_HEIGHT 输出格式值来了解原始输出缓冲区的布局。
请注意,在某些设备上,切片高度被建议设置为 0。这可能意味着切片高度与帧高度相同,或者切片高度是与某个值对齐的帧高度(通常是2)。不幸的是,在这种情况下没有标准和简单的方法来判断实际切片高度。此外,平面格式中 U 平面的垂直步幅也未指定或定义,尽管通常是切片高度的一半。
MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT Keys指定视频帧的大小;然而,对于大多数编码,视频(图片)仅占据视频帧的一部分。这由“裁剪矩形”表示。
您需要使用以下Keys从输出格式中获取原始输出图像的裁剪矩形。如果这些Keys不存在,则视频占据整个视频帧。在应用任何旋转之前,在输出帧的上下文中理解裁剪矩形。
格式keys |
类型 |
说明 |
"crop-left" |
Integer |
裁剪矩形的左坐标 (x) |
"crop-top" |
Integer |
裁剪矩形的顶部坐标 (y) |
"crop-right" |
Integer |
裁剪矩形的右坐标 (x) MINUS 1 |
"crop-right" |
Integer |
裁剪矩形的底部坐标 (y) MINUS 1 |
右下坐标可以理解为裁剪后的输出图像最右侧有效列/最底部有效行的坐标。
视频帧的大小(旋转前)可以这样计算:
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。
当您使用工厂方法之一创建编解码器时,编解码器处于未初始化状态。首先,您需要通过 configure(...) 对其进行配置,使其进入 Configured 状态,然后调用 start() 将其移动到 Executing 状态。在这种状态下,您可以通过上述缓冲区队列操作来处理数据。
Executing 状态具有三个子状态:Flushed、Running 和 End-of-Stream。在 start() 之后,编解码器立即处于 Flushed 子状态,它保存所有缓冲区。一旦第一个输入缓冲区出列,编解码器就会移动到运行子状态,在那里度过大部分时间。当您使用流结束标记对输入缓冲区进行排队时,编解码器将转换到流结束子状态。在这种状态下,编解码器不再接受更多的输入缓冲区,但仍会生成输出缓冲区,直到在输出上到达流结束。您可以在处于 Executing 状态时使用 flush() 随时移回 Flushed 子状态。
调用 stop() 将编解码器返回到未初始化状态,然后可以再次对其进行配置。当您使用完编解码器后,您必须通过调用 release() 来释放它。
在极少数情况下,编解码器可能会遇到错误并进入错误状态。这是使用来自排队操作的无效返回值或有时通过异常来传达的。调用 reset() 使编解码器再次可用。您可以从任何状态调用它以将编解码器移回未初始化状态。否则,调用 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(java.lang.String) 为特定 MIME 类型创建首选编解码器。 但是,这不能用于注入功能,并且可能会创建无法处理特定所需媒体格式的编解码器。
在 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) 将编解码器设置为使用先前创建的持久输入表面。
某些格式,特别是 AAC 音频和 MPEG4、H.264 和 H.265 视频格式要求实际数据以包含设置数据或编解码器特定数据的多个缓冲区为前缀。处理此类压缩格式时,必须在 start() 之后和任何帧数据之前将此数据提交给编解码器。此类数据必须在调用 queueInputBuffer 时使用标志 BUFFER_FLAG_CODEC_CONFIG 进行标记。
特定于编解码器的数据也可以包含在传递给 ByteBuffer 条目中配置的格式中,键为“csd-0”、“csd-1”等。这些键始终包含在从 MediaExtractor 获得的轨道 MediaFormat 中。格式中的编解码器特定数据在 start() 时自动提交给编解码器;您不得明确提交此数据。如果格式不包含编解码器特定数据,您可以根据格式要求选择使用指定数量的缓冲区以正确的顺序提交它。对于 H.264 AVC,您还可以连接所有特定于编解码器的数据并将其作为单个编解码器配置缓冲区提交。
Android 使用以下特定于编解码器的数据缓冲区。这些也需要在轨道格式中设置,以便正确的 MediaMuxer 轨道配置。每个参数集和标有 (*) 的编解码器特定数据部分必须以“\x00\x00\x00\x01”的起始代码开头。
格式 |
CSD 缓冲区 #0 |
CSD 缓冲区 #1 |
CSD 缓冲区 #2 |
AAC |
来自 ESDS 的解码器特定信息* |
不曾用过 |
不曾用过 |
沃比斯 |
标识头 |
设置标题 |
不曾用过 |
OPUS |
标识头 |
以纳秒为单位的预跳过 整数。) |
以纳秒为单位搜索预卷 整数。) |
FLAC |
“fLaC”,ASCII 格式的 FLAC 流标记, |
不曾用过 |
不曾用过 |
MPEG-4 |
来自 ESDS 的解码器特定信息* |
不曾用过 |
不曾用过 |
H.264 AVC |
SPS(序列参数集*) |
PPS(图片参数集*) |
不曾用过 |
H.265 HEVC |
VPS(视频参数集*)+ |
不曾用过 |
不曾用过 |
VP9 |
VP9编解码器私有 数据(可选) |
不曾用过 |
不曾用过 |
注意:如果在任何输出缓冲区或输出格式更改返回之前立即或在启动后不久刷新编解码器,则必须小心,因为编解码器特定数据可能会在刷新期间丢失。您必须BUFFER_FLAG_CODEC_CONFIG在此类刷新后使用标记为 的缓冲区重新提交数据, 以确保正确的编解码器操作。
编码器(或生成压缩数据的编解码器)将在标有codec-config 标志的输出缓冲区中的任何有效输出缓冲区之前创建并返回编解码器特定数据。包含编解码器特定数据的缓冲区没有有意义的时间戳。
每个编解码器维护一组由 API 调用中的缓冲区 ID 引用的输入和输出缓冲区。成功调用start()客户端后,既不“拥有”输入缓冲区,也不拥有输出缓冲区。在同步模式下,调用dequeueInput/OutputBuffer(…)以从编解码器获取(获得所有权)输入或输出缓冲区。在异步模式下,您将通过MediaCodec.Callback.onInput/OutputBufferAvailable(…)回调自动接收可用缓冲区。
获得输入缓冲区后,用数据填充它并使用queueInputBuffer- 或者queueSecureInputBuffer如果使用解密将其提交给编解码器。不要提交具有相同时间戳的多个输入缓冲区(除非它是特定于编解码器的数据标记为这样)。
编解码器反过来将通过onOutputBufferAvailable异步模式下的回调返回只读输出缓冲区,或响应dequeueOutputBuffer同步模式下的调用。处理输出缓冲区后,调用其中一种releaseOutputBuffer方法将缓冲区返回给编解码器。
虽然您不需要立即向编解码器重新提交/释放缓冲区,但保持输入和/或输出缓冲区可能会停止编解码器,并且此行为取决于设备。 具体来说,编解码器可能会推迟生成输出缓冲区,直到 所有未完成的缓冲区都已释放/重新提交。因此,尝试尽可能少地保留可用缓冲区。
根据 API 版本,您可以通过三种方式处理数据:
加工方式 |
API 版本 <= 20 |
API 版本 >= 21 |
使用缓冲区数组的同步 API |
支持的 |
已弃用 |
使用缓冲区的同步 API |
无法使用 |
支持的 |
使用缓冲区的异步 API |
无法使用 |
支持的 |
因为Build.VERSION_CODES.LOLLIPOP,首选方法是通过在调用之前设置回调来异步处理数据configure。异步模式会稍微改变状态转换,因为您必须调用start()afterflush()将编解码器转换为 Running 子状态并开始接收输入缓冲区。类似地,在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();
因为Build.VERSION_CODES.LOLLIPOP,即使在同步模式下使用编解码器,您也应该使用getInput/OutputBuffer(int)和/或 getInput/检索输入和输出缓冲区OutputImage(int)。这允许框架进行某些优化,例如在处理动态内容时。如果您调用getInput/ ,则会禁用此优化OutputBuffers()。
注意:不要同时混合使用缓冲区和缓冲区数组的方法。具体来说,仅在使值为 的输出缓冲区 ID 出列之后或之后直接调用getInput/ 。 OutputBuffersstart()INFO_OUTPUT_FORMAT_CHANGED
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();
在版本Build.VERSION_CODES.KITKAT_WATCH和之前的版本中,输入和输出缓冲区集由ByteBuffer[]数组表示。成功调用 后 start(),使用getInput/检索缓冲区数组OutputBuffers()。使用缓冲区 ID-s 作为这些数组的索引(当非负时),如下面的示例所示。请注意,尽管数组大小提供了上限,但数组大小与系统使用的输入和输出缓冲区的数量之间没有内在的相关性。
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(…);
if (inputBufferId >= 0) {
// fill inputBuffers[inputBufferId] with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId] is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
MediaFormat format = codec.getOutputFormat();
}
}
codec.stop();
codec.release();
当您到达输入数据的末尾时,您必须通过BUFFER_FLAG_END_OF_STREAM在调用中指定标志来将其发送给编解码器 queueInputBuffer。您可以在最后一个有效的输入缓冲区上执行此操作,或者通过提交设置了流结束标志的附加空输入缓冲区来执行此操作。如果使用空缓冲区,时间戳将被忽略。
该编解码器将继续返回的输出缓冲区,直到它最终通过指定相同的结束流标志信号输出流的端部BufferInfo在集dequeueOutputBuffer或经由返回onOutputBufferAvailable。这可以在最后一个有效输出缓冲区上设置,或者在最后一个有效输出缓冲区之后的空缓冲区上设置。应忽略此类空缓冲区的时间戳。
在发出输入流结束信号后不要提交额外的输入缓冲区,除非编解码器已被刷新或停止并重新启动。
使用输出Surface,数据处理与 ByteBuffer 模式几乎相同;但是,输出缓冲区将不可访问,并表示为null 值。例如getOutputBuffer/Image(int)返回null和getOutputBuffers()将返回仅含有一个数组null-s。
使用输出Surface时,您可以选择是否在Surface上渲染每个输出缓冲区。你有三个选择:
由于Build.VERSION_CODES.M,默认时间戳是缓冲区的呈现时间戳(转换为纳秒)。在此之前没有定义。
此外Build.VERSION_CODES.M,您可以使用setOutputSurface。
当将输出渲染到 Surface 时,Surface 可能会被配置为丢弃过多的帧(这些帧没有被 Surface 及时消耗)。或者它可以配置为不丢弃过多的帧。在后一种模式下,如果 Surface 没有足够快地消耗输出帧,它最终会阻塞解码器。
在Build.VERSION_CODES.Q确切的行为未定义之前,除了 View 表面(SurfaceView 或 TextureView)总是丢弃过多的帧。由于Build.VERSION_CODES.Q默认行为是丢弃过多的帧。应用程序可以选择的这种行为对于非View表面(如ImageReader的或表面纹理)通过针对SDK出来Build.VERSION_CODES.Q,关键设置MediaFormat#KEY_ALLOW_FRAME_DROP,以0 在他们的配置格式。
如果编解码器配置为 Surface 模式,则将自动应用任何裁剪矩形、旋转和视频缩放模式,但有一个例外:
在Build.VERSION_CODES.M发布之前,软件解码器在渲染到 Surface 时可能没有应用旋转。不幸的是,没有标准和简单的方法来识别软件解码器,或者除了尝试之外它们是否应用旋转。
还有一些注意事项。
请注意,在 Surface 上显示输出时不考虑像素纵横比。这意味着如果您正在使用VIDEO_SCALING_MODE_SCALE_TO_FIT模式,您必须定位输出 Surface 以使其具有正确的最终显示纵横比。相反,您只能VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING对具有方形像素(像素长宽比或 1:1)的内容使用模式。
另请注意,在Build.VERSION_CODES.N发布时,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能无法正常用于旋转 90 或 270 度的视频。
设置视频缩放模式时,注意每次输出缓冲区变化后必须重新设置。由于该INFO_OUTPUT_BUFFERS_CHANGED事件已被弃用,您可以在每次输出格式更改后执行此操作。
使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会自动从输入Surface传递到编解码器。调用dequeueInputBuffer将抛出IllegalStateException,并getInputBuffers() 返回一个虚假的ByteBuffer[]数组,不能被写入。
调用signalEndOfInputStream()信号流结束。输入Surface将在此调用后立即停止向编解码器提交数据。
视频解码器(以及通常使用压缩视频数据的编解码器)在搜索和格式更改方面表现不同,无论它们是否支持和配置为自适应播放。您可以通过CodecCapabilities.isFeatureSupported(String)检查解码器支是否持自适应播放。仅当您将编解码器配置为解码到 Surface 时,才会激活视频解码器的自适应播放支持功能。
在合适的流边界 之后start()或flush()开始的输入数据很重要:第一帧必须是关键帧。关键帧可以自己解码(对大都数编解码来说,这个关键帧就是I帧),并且没有其它帧在参照关键帧之前显示。
下表总结了适用于各种视频格式的关键帧。
格式 |
合适的关键帧 |
VP9/VP8 |
一个合适的帧内帧,其中没有后续帧引用该帧之前的帧。 |
H.265 HEVC |
IDR 或 CRA |
H.264 AVC |
IDR |
MPEG-4 |
一个合适的 I 帧,其中没有后续帧引用该帧之前的帧。 |
为了开始解码与先前提交的数据不相邻的数据(即在查找之后),您必须刷新解码器。由于所有输出缓冲区都在刷新时立即撤销,因此您可能需要先发出信号,然后在调用之前等待流结束flush。刷新后的输入数据从合适的流边界/关键帧开始很重要。
注意: flush后提交的数据格式必须不能改变;flush()不支持不连的格式;为此,一个完整的stop()- configure(…)-start()生命周期是必要的。
另外请注意:如果start()后过早刷新编解码器,通常在收到第一个输出缓冲区或输出格式更改之前,您将需要重新提交编解码特定数据到编解码器。有关详细信息,请参阅编解码特定数据部分。
为了开始解码与先前提交的数据不相邻的数据(即在查找之后),刷新解码器不是必要的;但是,中断后的输入数据必须从合适的流边界/关键帧开始。
对于某些视频格式 - 即 H.264、H.265、VP8 和 VP9 - 还可以在中途更改图片大小或配置。为此,您必须将整个新的特定于编解码器的配置数据与关键帧一起打包到单个缓冲区(包括任何起始代码)中,并将其作为常规输入缓冲区提交。
您将在图片大小更改发生之后和具有新大小的任何帧返回之前收到INFO_OUTPUT_FORMAT_CHANGED来自dequeueOutputBuffer或onOutputFormatChanged回调的返回值。
注意:就像编解码器特定数据的情况一样,在调用flush()方法来改图片大小要小心 。如果您还没有收到更改图片大小的确认,您将需要重新请求新的图片大小。
您必须捕获或声明放弃工厂方法createByCodecName和createDecoder/EncoderByType引发IOException失败。MediaCodec 会抛出IllegalStateException,当从不允许它的编解码器状态调用方法时;这通常是由于不正确的应用程序 API 使用造成的。涉及安全缓冲区的方法可能会抛出 CryptoException,其中包含可从CryptoException#getErrorCode.
内部编解码器错误会导致CodecException,这可能是由于媒体内容损坏、硬件故障、资源耗尽等,即使应用程序正确使用 API。当抛出CodecException 时,调用CodecException#isRecoverable和CodecException#isTransient来确定推荐的操作:
isRecoverable()和isTransient()都不会在同一时间返回true。