MediaCodec类可以用来访问底层媒体编解码器,即编码器/解码器的组件。 它是Android底层多媒体支持架构的一部分(通常与MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用)。
从广义上讲,一个编解码器处理输入数据以生成输出数据。 它异步地处理数据,并使用一组输入和输出缓冲器。 从一个简单的层面上看,可请求(或接收)一个空的输入缓冲器,然后用数据填满它,并将其发送到编解码器去处理。 编解码器使用这些数据并转换这些数据到它某个空的输出缓冲区。 最后,您请求(或接收)一个已填充数据的输出缓冲区,消耗其内容并将其释放回并回到编解码器。
编解码器对3种数据进行操作:压缩后的数据,原始音频数据和原始视频数据。可以使用ByteBuffers处理所有三种数据,但对原始视频数据,您应该使用Surface以提高编解码的性能。Surface使用本地视频缓冲区而不是映射或复制到ByteBuffers,因此,效率更高。 通常在使用Surface时无法访问原始视频数据,但您可以使用ImageReader类访问不安全的解码(原始)视频帧。 这可能比使用ByteBuffers更有效,因为一些本机缓冲区可能被映射到直接ByteBuffers。 当使用ByteBuffer模式时,您可以使用Image类和getInput / OutputImage(int)访问原始视频帧。
输入缓冲器(用于解码器)和输出缓冲器(用于编码器)根据格式的类型来存放已压缩的数据。 对于视频类型,这是一个单一的压缩后的视频帧。 对于音频数据,这通常是单个访问单元(一个编码后的音频片段段通常包含由格式类型指定的几毫秒音频),但是这个要求可以稍微放宽,因为缓冲器可以包含多个编码的音频访问单元。 在任一情况下,缓冲区不会以随意的字节边界开始或结束,而是在帧/访问单元边界上开始或者结束。
原始音频缓冲器包含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;
}
在ByteBuffer模式下,视频缓冲区根据其颜色格式进行布局。 您可以通过
getCodecInfo().getCapabilitiesForType(...).colorFormats
获取支持的颜色格式数组。 视频编解码器可能支持三种颜色格式:
MediaCodecInfo.CodecCapabilities
中定义。 对于等同于灵活格式的颜色格式,您仍然可以使用 getInput/OutputImage(int)
。从LOLLIPOP_MR1开始,所有视频编解码器都支持灵活的YUV 4:2:0缓冲区。
在LOLLIPOP和Image支持之前,您需要使用KEY_STRIDE和KEY_SLICE_HEIGHT输出格式值来了解原始输出缓冲区的布局。
请注意,在某些设备上,切片高度被标示为0.这可能意味着切片高度与帧高度相同,或者切片高度与帧高度对齐为某个值(通常是2的幂)。 不幸的是,在这种情况下,没有标准和简单的方式来说明实际的切片高度。此外,平面格式的U平面的垂直跨度也没有被指定或定义,尽管通常它是切片高度的一半。
KEY_WIDTH和KEY_HEIGHT键指定视频帧的大小; 然而,对于大多数包络,视频(图片)仅占据视频帧的一部分。 这由“裁剪矩形”表示。
您需要使用以下键从输出格式获取原始输出图像的裁剪矩形。 如果这些键不存在,则视频占据整个视频帧。在应用任何旋转之前,裁剪矩形在输出帧应结合上下文中来理解。
格式键 | 类型 | 描述 |
---|---|---|
“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_HEIGHT);
if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
}
另请注意,BufferInfo.offset的含义在设备之间不一致。 在某些设备上,偏移指向裁剪矩形的左上角像素,而在大多数设备上,它指向整个帧的左上角像素。
在其生命周期中,编解码器在概念上处于三种状态中的某一个:停止,执行或释放。 停止状态实际上是三个状态的集合:未初始化,配置就绪和出错,而执行状态在概念上通过三个子状态进行:刷新,运行和结束。
当您使用出厂方法之一创建编解码器时,编解码器处于未初始化状态。 首先,您需要通过configure(…)进行配置,使其进入Configured状态,然后调用start()让其进入到执行状态。 在这种状态下,您可以通过上述缓冲区队列操作来处理数据。
执行状态有三个子状态:已刷新,运行和流结束。 在start()后,编解码器立即处于Flushed子状态,它拥有所有的缓冲区。 一旦第一个输入缓冲区出出队,编解码器就会移动到运行的子状态,生命周期中的大部分时间都处于该状态。 当您对输入缓冲区入队时带有流末尾标记时,编解码器将转换到流末端子状态。 在这种状态下,编解码器不再接受以后的输入缓冲器,但仍然产生输出缓冲器,直到输出端到达流末尾。 在执行状态下,可以随时使用flush()来回到Flushed子状态。
调用stop()将编解码器返回到未初始化状态,然后可以重新配置。 使用完毕编解码器后,您必须通过调用release()来释放它。
在极少数情况下,编解码器可能会遇到错误并移动到错误状态。 这通过来自排队操作的无效返回值告知,或有时通过异常来传递。 调用reset()使编解码器再次可用。 您可以从任何状态调用它,将编解码器移回未初始化状态。 否则,调用release()到最终的释放状态。
使用MediaCodecList为特定的MediaFormat创建一个MediaCodec。 解码文件或流时,可以从MediaExtractor.getTrackFormat获取所需的格式。 使用MediaFormat.setFeatureEnabled添加任何特定的特性,然后调用MediaCodecList.findDecoderForFormat得到可以处理特定媒体格式的编解码器的名称。 最后,使用createByCodecName(String)创建编解码器。
注意:在LOLLIPOP上,MediaCodecList.findDecoder / EncoderForFormat的格式不能包含帧速率。 使用format.setString(MediaFormat.KEY_FRAME_RATE,null)以格式清除任何现有的帧速率设置。
您也可以使用createDecoder / EncoderByType(String)为特定的MIME类型创建首选编解码器。 然而,这不能用于添加特性,并且可能创建无法处理特定所需媒体格式的编解码器。
在KITKAT_WATCH及更早版本上,安全编解码器可能未列在MediaCodecList中,但仍可能在系统上可用。 存在的安全编解码器可以通过名称实例化,通过将“.secure”附加到常规编解码器的名称(所有安全编解码器的名称必须以“.secure”结尾)。如果编解码器在系统上不存在,createByCodecName(String)将抛出IOException。
从LOLLIPOP开始,您应该使用媒体格式的FEATURE_SecurePlayback功能来创建安全解码器。
创建编解码器后,如果要异步处理数据,则可以使用setCallback设置回调。 然后,使用特定的媒体格式配置编解码器。 这时您可以为视频生产者—生成原始视频数据的编解码器(例如视频解码器)指定输出的Surface。这时您可以设置安全编解码器的解密参数(请参阅MediaCrypto)。最后,由于某些编解码器可以在多种模式下运行,因此您必须指定是否要将其用作解码器或编码器。
从LOLLIPOP开始,您可以在Configured状态下查询生成的输入和输出格式。在启动编解码器之前,您可以使用它来验证结果配置,例如颜色格式。
如果您想要与视频消费者——一个处理原始视频输入的编解码器(如视频编码器)本地处理原始输入视频缓冲区,可在配置后使用createInputSurface()创建输入目标Surface。 或者,通过调用setInputSurface(Surface)来设置编解码器以使用先前创建的持久输入Surface。
一些格式,特别是AAC音频和MPEG4,H.264和H.265视频格式要求实际数据以包含设置数据或编解码器特定数据的缓冲区为前缀。 处理这种压缩格式时,必须在start()之后和任何帧数据之前将该数据提交到编解码器。 在调用queueInputBuffer时,必须使用标志BUFFER_FLAG_CODEC_CONFIG标记此类数据。
Codec特定的数据也可以以传递给配置的格式包含在带有“csd-0”,“csd-1”等字符串的ByteBuffer条目中。这些键始终包含在从MediaExtractor获取的MediaFormat中。 在其中的编解码器特定数据在start()时自动提交到编解码器;您不能显示地提交此数据。 如果结构不包含编解码器特定数据,您可以根据格式要求选择使用指定数量的缓冲区以正确的顺序提交它。 在H.264 AVC的情况下,您还可以连接所有编解码器专用数据,并将其作为单个编解码器配置缓冲区提交。
Android使用以下编解码器特定的数据缓冲区。 这些还需要按照适合MediaMuxer轨道配置的轨道格式进行设置。 每个参数集和标有(*)的编解码器特定数据段必须以起始代码“\x00\x00\x00\x01”开头。
Format | CSD buffer #0 | CSD buffer #1 | CSD buffer #2 |
---|---|---|---|
AAC | Decoder-specific information from ESDS* | Not Used | Not Used |
VORBIS | Identification header | Setup header | Not Used |
OPUS | Identification header | Pre-skip in nanosecs(unsigned 64-bit native-order integer.)This overrides the pre-skip value in the identification header. | Seek Pre-roll in nanosecs(unsigned 64-bit native-order integer.) |
MPEG-4 | Decoder-specific information from ESDS* | Not Used | Not Used |
H.264 AVC | SPS (Sequence Parameter Sets*) | PPS (Picture Parameter Sets*) | Not Used |
H.265 HEVC | VPS (Video Parameter Sets*) +SPS (Sequence Parameter Sets*) +PPS (Picture Parameter Sets*) | Not Used | Not Used |
VP9 | VP9 CodecPrivate Data (optional) | Not Used | Not Used |
注意:当编解码器在启动后立刻或稍后不久久刷新时,当任何输出缓冲区或输出格式的更改返回之前,必须要小心,因为在刷新过程中编解码器特定数据可能会丢失。 您必须在这样的刷新之后使用标记为BUFFER_FLAG_CODEC_CONFIG的缓冲区重新提交数据,以确保正确的编解码器操作。
编码器(或产生压缩数据的编解码器)将创建带有codec-config标志的输出缓冲器,并在其中存放配置编解码器特定的数据并返回,这个动作将在输出任何存放有效数据的缓冲器之前进行。 包含编解码器特定数据的缓冲区的时间戳是没有意义的。
每一个编解码器拥有一套在API调用中由buffer-ID引用的输入和输出缓冲器,在一次对start()成功调用后,客户端“拥有”既不输入也不输出的缓冲区。 在同步模式下,调用dequeueInput / OutputBuffer(…)从编解码器获取(获得)输入或输出缓冲区的所有权。 在异步模式下,您将通过MediaCodec.Callback.onInput / OutputBufferAvailable(…)回调自动接收可用的缓冲区。
在获得输入缓冲区时,填写数据并使用queueInputBuffer或queueSecureInputBuffer将其提交到编解码器(如果使用解密)。不要提交具有相同时间戳的多个输入缓冲区(除非是特定于编解码器的数据)。
反过来该编解码器将在异步模式下经由onOutputBufferAvailable回调返回只读输出缓冲器,或者响应于在同步模式中的dequeuOutputBuffer呼叫。 输出缓冲器已被处理后,调用的releaseOutputBuffer方法中的一个将缓冲器返回到编解码器。
尽管你不需要立即重新提交/释放缓冲器到编解码器,但持续持有输入或输出缓冲器会使编解码器停止,而这种行为是设备相关的。 具体来说,编解码器可能会在生成输出缓冲区之前停止,直到所有未完成的缓冲区被释放/重新提交为止。 因此,尽可能短地持有可用的缓冲区。
取决于API版本,您可以通过三种方式处理数据:
Processing Mode | API version <= 20 Jelly Bean/KitKat | API version >= 21 Lollipop and later |
---|---|---|
Synchronous API using buffer arrays | Supported | Deprecated |
Synchronous API using buffers | Not Available | Supported |
Asynchronous API using buffers | Not Available | Supported |
从LOLLIPOP开始,优选的方法是在配置之前设置一个回调然后采用异步方式来处理数据。 异步模式稍微地更改状态,因为必须在flush()之后调用start()将编解码器转换为r运行子状态并开始接收输入缓冲区。 类似地,在初始调用启动编解码器将直接移动到运行子状态,并通过回调开始传递可用的输入缓冲区。
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();
从LOLLIPOP开始,即使在同步模式下使用编解码器,也应该使用getInput / OutputBuffer(int)和/或getInput / OutputImage(int)来检索输入和输出缓冲区。 这允许框架进行某些优化,例如。 处理动态内容时。 如果您调用getInput /OutputBuffers(),则此优化将被禁用。
注意:不要同时混合使用缓冲区和缓冲区数组的方法。 具体来说,只有在start()之后才能直接调用getInput / OutputBuffers,或者在输出缓冲区ID的值为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();
在版本KITKAT_WATCH和之前,输入和输出缓冲区的集合由ByteBuffer[] 数组表示。 在成功调用start()之后,使用getInput/OutputBuffers()获得缓冲区数组。如下面的示例所示,使用缓冲区ID作为这些数组中的索引(当为非负数时)。 请注意,数组的大小与系统使用的输入和输出缓冲区的数量之间没有固有的相关性,尽管数组大小有一个上限。
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();
当您到达输入数据的末尾时,必须通过在对queueInputBuffer的调用中指定BUFFER_FLAG_END_OF_STREAM标志来将其发送到编解码器。 您可以在最后一个有效的输入缓冲区中执行此操作,也可以通过提交一个额外的空输入缓冲区来设置流终止标志。 如果使用空的缓冲区,时间戳将被忽略。
编解码器将继续返回输出缓冲区,直到最终通过在dequeueOutputBuffer中设置的MediaCodec.BufferInfo中指定相同的流出尾标志,或通过onOutputBufferAvailable返回信号来输出流的结尾。 这可以在最后一个有效的输出缓冲区上设置,也可以在最后一个有效输出缓冲区之后的空缓冲区中设置。 这样的空缓冲区的时间戳应该被忽略。
在信号输入流结束后,请勿提交其他输入缓冲区,除非编解码器已被刷新,或停止并重新启动。
当使用输出Surface时,数据处理与ByteBuffer模式几乎相同; 然而,输出缓冲区将不可访问,并被表示为空值。 例如,getOutputBuffer/Image(int)将返回null,而getOutputBuffers()将返回一个仅包含null的数组。
使用输出Surface时,可以选择是否在Surface上渲染每个输出缓冲区。 你有三个选择:
从M开始,默认时间戳是缓冲区的显示时间戳(转换为纳秒),之前没有定义。
此外,从M开始,您可以使用setOutputSurface动态更改输出Surface。
如果编解码器配置为Surface模式,任何裁剪矩形,旋转和视频缩放模式将被自动应用,但有一个例外:
在Android M之前,当渲染到Surface上时,软件解码器可能没有应用旋转。 不幸的是,没有标准和简单的方法来识别软件解码器,或者除了通过尝试外,它们应用旋转。
还有一些注意事项。
请注意,当在Surface上显示输出时,不考虑像素长宽比。 这意味着如果您使用VIDEO_SCALING_MODE_SCALE_TO_FIT模式,则必须定位输出的Surface,使其具有正确的最终显示宽高比。 相反,您只能对具有方形像素(像素宽高比或1:1)的内容使用VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式。
另请注意,从Android N开始,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式在视频旋转90或270度时可能无法正常工作。
当设置视频缩放模式时,请注意,每当输出缓冲区更改后,必须进行Reset。 由于已不推荐使用INFO_OUTPUT_BUFFERS_CHANGED事件,因此可以在每次输出格式更改后执行此操作(Reset操作)。
当使用Surface作为输入时,没有输入缓冲区可供访问,因为缓冲区会从输入Surface自动传递到编解码器。 调用dequeueInputBuffer将抛出一个IllegalStateException,getInputBuffers()将返回一个伪造的ByteBuffer[]数组,切记不要去对该数组执行写入操作
调用signalEndOfInputStream来发送流末尾信号。 输入Surface将在此调用后立即停止向编解码器提交数据。
视频解码器(以及消耗经过压缩的视频数据的一般编解码器)的行为不同关于寻求和格式改变他们是否支持并配置了自适应播放。 您可以通过CodecCapabilities.isFeatureSupported(String)来检查解码器是否支持自适应播放。 支持自适应播放的视频解码器仅在将其的输出配置为Surface时才能使用自适应播放。
很重要的一点,在start()或flush()之后的输入数据开始于合适的流边界:第一帧必须是关键帧。关键帧可以自己完全解码(对于大多数编解码器,这代表I帧),并且在关键帧之后,没有帧会以该关键帧之前的帧作为参考帧。
下表总结了各种视频格式的合适关键帧。
Format | Suitable key frame |
---|---|
VP9/VP8 | a suitable intraframe where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.) |
H.265 HEVC | IDR or CRA |
H.264 AVC | IDR |
MPEG-4 H.263 MPEG-2 | a suitable I-frame where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.) |
为了开始解码与先前提交的数据不相邻的数据(即在进度拖动之后),您必须刷新解码器。由于所有输出缓冲区在刷新点立即被撤销,因此您可能需要首先发出信号,然后在等待流末尾到来再调用flush。 重要的是,刷新后的输入数据起始于合适的流边界/关键帧。
注意:数据的格式在一次flush后不得改变;flush()不支持格式不连续;为此,需要一个完整的stop()-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,错误信息可从getErrorCode()获取。
在应用程序正确调用了API时,编解码器的内部错误会引起MediaCodec.CodecException,导致该异常的原因可能为媒体内容损坏、硬件故障、资源耗尽等。 接收到CodecException时推荐的操作可通过调用isRecoverable()和isTransient()来确定:
isRecoverable()和isTransient()都不会同时返回true。