天天看点

MediaCodec官网翻译

来自 Developer 官网:MediaCodec 。

MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。它是 Android 低电平多媒体支持基础设施的一部分(通常与它们一起使用:MediaExtractor , MediaSync , MediaMuxer , MediaCrypto , MediaDrm , Image , Syrface , 和 AudioTrack )。

广义而言,编解码器处理输入数据以生成输出数据。它异步处理数据,并使用一组输入和输出缓冲区。在一个简单的级别上,您请求(或接受)一个空的输入缓冲区,将其填充数据并将其发送到编解码器进行处理。编解码器用完数据并将其转换为空的输出缓冲区之一。最后,您请求(或接受)已填充的输出缓冲区,使用其内容并将其释放回编解码器。

数据类型

编解码器对于三种数据进行操作:压缩数据,原始音频数据和原始视频数据。可以使用来处理所有三种数据 ButeBuffer,但是对于原始视频数据应提供一个Surface以提高编解码器的性能。Surface 使用本机视频缓冲区,而不将其映射或复制到 ByteBuffer ;因此,它效率更高。使用 Surface 时,通常不能访问原始视频数据,但是可以使用 ImageReader 该类来访问不安全的解码(原始)视频帧。这可能仍比使用 ByteBuffer 更有效,因为某些本机缓冲区可能已映射到 ByteBuffer 。使用 ByteBuffer 模式时,可以使用 Image 类和 getInput / OutputImage(int) 方法访问原始视频帧。

压缩缓冲区

输入缓冲区(用于解码器)和输出缓冲区(用于编码器)包含由多媒体格式类型决定的压缩数据。对于视频类型,这通常是单个压缩的视频帧。对于音频数据,这通常是一个访问单元(一个编码的音频段,通常包含几毫秒的音频,这由格式类型决定),但是由于缓冲区中可能包含多个编码的访问单元,因此这一要求并不严格。在这两种情况下,缓存不会在任意的字节边界上开始或结束,而是在帧或可访问单元的边界上开始或结束。

原始音频缓冲区

原始音频缓冲区包换 PCM 音频数据的整个帧,这是按通道顺序每个通道的样本。每个 PCM 音频样本都是16位带符号证书或浮点数(以本机字节顺序)。

原始视频缓冲区

在 ByteBuffer 模式下,视频缓冲区根据它们的颜色格式 color format 进行展现,您可以从下面获取支持的颜色格式作为数组: getCodecInfo().getCapabilitiesForType(…).colorFormats 。视频编解码器可以支持三种颜色格式:

 · 本地原始视频格式:这种格式通过COLOR_FormatSurface标记,并可以与输入或输出Surface一起使用。

 · YUV 缓存:(如:COLOR_FormatYUV420Flexible)利用一个输入或输出Surface,或在在ByteBuffer模式下,可以通过调用getInput/OutputImage(int)方法使用这些格式。

 · 其它特定格式:通常只在ByteBuffer模式下被支持。有些颜色格式是特定供应商指定的。其他的一些被定义在 MediaCodecInfo.CodecCapabilities中。这些颜色格式同 flexible format相似,你仍然可以使用 getInput/OutputImage(int)方法。

从 Android5.1 开始,所有视频编解码器均支持灵活的 YUV4:2:0 缓冲区。

状态

在其生命周期内,编解码器从概念上讲处于以下三种状态之一:停止( Stopped ),执行( Executing )或释放( Released )。停止( Stopped )的状态包含了三个子状态:未初始化( Uninitialized ),已配置( Configured )和错误( Error )。执行( Executing )状态从概念上讲经过三个子状态:刷新( Flushed ),运行( Running )和流结束( End-of-Stream )。

使用工厂方法之一创建编解码器时,编解码器处于未初始化状态( Uninitialized )。首先,您需要使用 configure() 对编解码器进行配置,使它进入已配置状态( Configured ),然后调用 start 将其移动到执行状态( Executing )。在这状态下,您可以通过上诉缓冲区队列操作来处理数据。

执行状态具有三个子状态:刷新( Flushed ),运行( Running )和流结束( End-of-Stream )。 start 编解码器立即处于刷新状态( Flushed ),它将保存所有缓冲区。一旦第一个输入缓冲区出队,编解码器将移至运行状态( Running ),在此状态将花费大部分的时间。当您将输入缓冲区与流结束标记排队时,编解码器将装华为流结束状态( End-of-Stream )。在这种状态下,编解码器将不再接受其它输入缓冲区,但仍会生成输出缓冲区,直到在输出端达到流结束为止。在处于执行状态( Executing )时,您可以随时返回到刷新状态( Flushed )。

调用 stop 已将编解码器返回到未初始化状态( Uninitialized ),然后可以再次对其进行配置。使用编解码器完成操作后,必须通过调用释放它 release 。

在极少数情况下,编解码器可能遇到错误并进入到错误状态( Error )。使用来自排队操作额无效返回值后有时通过异常来传达此信息。调用 reset 已使编解码器再次可用。您可以从任何状态调用它,已将编解码器移回未初始化状态( Uninitialized )。否则,请调用 release 已移至终端的已释放状态( Released )。

创建

根据指定的 MediaFormat 使用 MeidaCidecList 创建一个 MediaCodec 的实例。解码文件或流时,可以从 MediaExtractor.getTrackFormat 获得所需的格式。并调用 MediaFormat.setFeatureEnabled 添加您想添加的任何属性,然后调用 MediaCodecList.findDecoderForFormat 获取可以处理该特定媒体格式的编解码器的名称。最后,使用创建编解码器 createByCodecName。

注意

在 Android5.0 上,传递给 MediaCodecList.findDecoder/EncoderForFormat 不能包含帧率 frame rate 。通过调用 format.setString(MediaFormat.KEY_FRAME_RAT,null) 以清除当前任何格式的帧率。

您还可以使用 createDecoder / EncoderByType(String) 方法为特定的 MIME 类型创建首选编解码器。但是,这不能注入特征,而且创建的编解码器可能不能处理你期望的格式。

创建安全的解码器

在Android 4.4 及之前版本,安全的编解码器(secure codecs)没有被列在MediaCodecList中,但是仍然可以在系统中使用。安全编解码器只能够通过名字进行实例化,其名字是在常规编解码器的名字后附加.secure标识(所有安全编解码器的名字都必须以.secure结尾),调用createByCodecName(String)方法创建安全编解码器时,如果系统中不存在指定名字的编解码器就会抛出IOException异常。

从Android 5.0 及之后版本,你可以在媒体格式中使用FEATURE_SecurePlayback属性来创建一个安全编解码器。

初始化

创建编解码器后,如果要异步处理数据,通过设置 setCallback 设置回调。然后,指定特定的媒体格式配置编解码器。此时,您可以 Surface 为视频制作指定输出,即产生原始视频数据的编解码器( 例如:视频解码器 )。这也是您可以设置安全编解码器的解密参数的时候(请参阅参考资料 MediaCrypto )。最后,由于某些编解码器可以在多种模式下运行,因此必须指定该编码器是作为编码器还是解码器使用。

您可以在已配置状态下查询结果输入和输出格式。您可以在启动编解码器之前使用它来验证最终的配置,如颜色格式。

如果要使用原始视频数据送视频消费者处理(将原始视频数据作为输入的编解码器,例如视频编码器),你可以在配置好视频消费者编解码器(encoder)后调用createInputSurface方法创建一个目的surface来存放输入数据,如此,调用视频生产者(decoder)的setInputSurface(Surface)方法将前面创建的目的Surface配置给视频生产者作为输出缓存位置。

编解码器专用数据(Codec-specific 数据)

某些格式,尤其是 AAC 音频和MPEG4,H.264 和 H.265 视频格式,要求实际数据的前缀是许多包含设置数据或编解码器特定数据的缓冲区。处理此类压缩格式时,必须在调用 start() 方法后并且在处理任何帧数据之前交给编码器。这些数据必须在调用 queueInputBuffer 方法时使用 BUFFER_FLAG_CODEC_CONFIG 标记。

Codec-specific 数据也可以被包含在传递给 configure 方法的格式信息(MediaFormat)中,在 ByteBuffer 条目中以"csd-0", "csd-1"等key标记。这些 keys 一直包含在通过 MediaExtractor 获得的 Audio Track or Video Track的MediaFormat 中。一旦调用 start() 方法,MediaFormat 中的 Codec-specific 数据会自动提交给编解码器;你不能显示的提交这些数据。如果 MediaFormat 中不包含编解码器指定的数据,你可以根据格式要求,按照正确的顺序使用指定数目的缓存来提交 codec-specific 数据。在 H264 AVC 编码格式下,你也可以连接所有的 codec-specific 数据并作为一个单独的 codec-config buffer 提交。

Android 使用下列的 codec-specific data buffers。对于适当的 MediaMuxer 轨道配置,这些也要在轨道格式中进行设置。每个参数集和标有(*)的特定于编解码器的数据段均必须以起始码开头"\ x00 \ x00 \ x00 \ x01"。

MediaCodec官网翻译

注意:当编解码器被立即刷新或 start 之后不久刷新,并且在任何输出 buffer 或输出格式变化被返回前需要特别地小心,因为编解码器的 codec specific data 可能会在 flush 过程中丢失。为保证编解码器的正常运行,你必须在刷新后使用标记为BUFFER_FLAG_CODEC_CONFIGbuffers 的 buffers 再次提交这些数据。

编码器(或者产生压缩数据的编解码器)将会在有效的输出缓存之前产生和返回编解码器指定的数据,这些数据会以codec-config flag进行标记。包含codec-specific-data的Buffers没有有意义的时间戳。 

数据处理

每个编解码器包含一组输入和输出缓冲区(input and output buffers),这些缓存在API调用中通过buffer-id进行引用。成功调用 start() 方法后,客户端将不会 "拥有" 输入和输出缓冲区。在同步模式下,调用 dequeueInput / OutputBuffer() 从编解码器获取输入或输出缓冲区。在异步模式下,您将通过 Callback.onInputBufferAvailable / Callback.onOutputBufferAvailable 回调自动接入可用的缓冲区。

数据获取缓冲区后,将其填充数据,然后使用 queueInputBuffer 提交给编解码器,若使用解密则使用 queueSecureInputBuffer 提交。不要提交带有相同时间戳的多个输入缓冲区(除非它是也被同样标记的codec-specific data)。

在异步模式下通过 onOutputBufferAvailable 方法的回调或者在同步模式下响应 dequeuOutputBuffer 的调用,编解码器返回一个只读的输出缓冲区。在这个输出缓冲区被处理后,调用一个 releaseOutputBuffer 方法将这个缓冲区返回给编解码器。

当你不需要立即将缓冲区重新提交/释放到编解码器,但保持输入/输出缓冲区可能会使编解码器停止工作,并且这与设备相关。特别是,编解码器可能会推迟生成输出缓冲区,直到所有未完成的缓冲区都已释放/重新提交。因此,请尝试尽可能少地保留可用缓冲区。

根据 API版本,您可以通过三种方式处理数据:

MediaCodec官网翻译

使用缓冲区的异步处理

从 Android5.0 开始,首选方法是调用 configure 之前设置回调来异步处理数据。因为你必须在调用 flush() 方法后再调用 start() 方法才能使编解码器的状态转换为 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();
           

使用缓冲区的同步处理

从 Android5.0 开始,即使在同步模式下使用编解码器,也应该使用 getInput / OutputBuffer(int) 和/或 getInput / OutputImage(int) 检索输入和输出缓冲区 OutputImage(int) 。这允许框架进行某些优化,例如在处理动态内容时。如果调用 getInput /,则会禁用此优化 OutputBuffers() 。

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();
           

流结束处理

到达输入数据的末尾时,必须调用 queueInputBuffer 方法中通过指定 BUFFER_FLAG_END_OF_STREAM 标记通知编解码器。您可以在最后一个有效的输入缓冲区上执行此操作,或者通过提交另外一个空输入缓冲区并设置流结束标志来执行此操作。如果使用空缓冲区,则时间戳将被忽略。

编解码器将继续返回输出缓冲区,直到它发出输出流结束的信号,这是通过指定 dequeueOutputBuffer 方法中 MediaCodec.BufferInfo的end-of-stream 标记来实现的,或者是通过回调方法 onOutputBufferAvailable 来返回 end-of-stream 标记。可以在最后一个有效的输出 buffer 中设置或者在最后一个有效的输出 buffer 后添加一个空的 buffer 来设置,这种空的buffer的时间戳应该被忽略。

在发出输入流结束信号后不要提交其他输入缓冲区。除非一刷新,停止或重新启动编解码器。

使用输出表面

使用输出时,数据处理几乎与 ByteBuffer 模式相同 Surface ;但是,输出缓冲区将不可访问,并表示为 null 值。如,getOutputBuffer / Image(int) 将返回 null ,并且 getOutputBuffers 返回包含 null 的数组。

使用输出 Surface 时,可以选择是否在表面上渲染每个输出缓冲区。您有三种选择:

· 不渲染缓冲区:调用 releaseOutputBuffer(bufferId , false)。

· 使用默认时间戳渲染缓冲区:调用 releaseOutputBuffer(bufferId , true)。

· 使用特定的时间戳渲染缓冲区:调用 releaseOutputBuffer(bufferId , timestamp)。

从 Android6.0 开始,默认时间戳是缓冲区的 presentationTimeUs(转换为纳秒)。在此之前未定义。

另外从 Android6.0 开始,您可以使用 setOutputSurface 方法动态更改输出 Surface 。

当使用输入 Surface 时,将没有可访问的输入 buffers ,因为这些buffers将会从输入 surface 自动地向编解码器传输。调用 dequeueInputBuffer 时将抛出一个 IllegalStateException 异常,调用 getInputBuffers() 将要返回一个不能写入的伪 ByteBuffer[] 数组。

调用 signalEndOfInputStream() 方法发送 end-of-stream 信号。调用这个方法后,输入 surface 将会立即停止向编解码器提交数据。

搜寻和自适应播放支持

视频解码器(通常指处理压缩视频数据的编解码器)关于搜索-seek和格式转换(不管它们是否支持)表现不同,且被配置为adaptive playback。你可以通过调用 CodecCapabilities.isFeatureSupported(String) 方法来检查解码器是否支持 adaptive playback 。支持 Adaptive playback 的解码器只有在编解码器被配置在 Surface 上解码时才被激活。

流边界和关键帧

在调用 start() 或 flush() 之后,输入数据在合适的流边界开始是非常重要的:第一帧必须是关键帧。一个关键帧可以完全独立的被解码(对于大多数编解码器它意味着I-frame),并且关键帧之后显示的帧不会引用关键帧之前的帧。

下面总结适用于各种视频格式的关键帧:

MediaCodec官网翻译

对于不支持自适应播放的编解码器(包括解码到Surface上解码器)

为了开始解码与先前提交的数据(也就是seek后)不相邻的数据你必须刷新解码器。由于所有输出buffers会在flush的一刻立即撤销,你可能希望在调用flush方法前等待这些buffers首先被标记为end-of-stream。在调用flush方法后输入数据在一个合适的流边界或关键帧开始是非常重要的。

注意:flush后提交的数据的格式不能改变;flush()方法不支持格式的不连续性;为此,一个完整的stop()-configure(...)-start()的过程是必要的。

另外注意:如果你调用start()方法后过快地刷新编解码器,通常,在收到第一个输出buffer或输出format变化前,你需要向这个编解码器再次提交codec-specific-data。具体查看codec-specific-data部分以获得更多信息。

对于支持并配置为自适应播放的解码器

为了开始解码与先前提交的数据(也就是seek后)不相邻的数据,不必刷新解码器;但是,在间断后传入的数据必须开始于一个合适的流边界或关键帧。

针对一些视频格式-也就是H.264、H.265、VP8和VP9,也可以修改图片大小或者配置 mid-stream 。为了做到这些你必须将整个新 codec-specific 配置数据与关键帧一起打包到一个单独的buffer中(包括所有的开始数据),并将它作为一个常规的输入数据提交。

在picture-size被改变后以及任意具有新大小的帧返回之前,你可以从 dequeueOutputBuffer 方法或 onOutputFormatChanged 回调中得到 INFO_OUTPUT_FORMAT_CHANGED的返回值。

注意:就像使用 codec-specific data 时的情况,在你修改图片大小后立即调用fush()方法时需要非常小心。如果你没有接收到图片大小改变的确认信息,你需要重试修改图片大小的请求。

摘要

嵌套类

BufferInfo : 每个缓冲区元数据包含一个偏移量和大小,用于指定关联的编解码器(输出)缓冲区中有效数据的范围。

Callback : MediaCodec回调接口。

CodecException : 在发生内部编解码器错误时抛出。

CryptoException : 对于安全输入缓冲区进行排队时发生加密错误时抛出。

CryptoInfo : 描述加密输入样本结构的元数据。

OnFrameRenderredListener : 在输入表面渲染了输出帧时将调用的侦听器。

常数

BUFFER_FLAG_CODEC_CONFIG  : 这表明这样标记的缓冲区包含编解码器初始化/编解码器特定的数据,而不是媒体数据。

BUFFER_FLAG_END_OF_STREAM : 标志流的结束。

BUFFER_FLAG_KEY_FRAME : 这表明这样标记的(编码)缓冲区包含关键帧的数据。

BUFFER_FLAG_SYNC_FRAME : 这表明这样标记的(编码)缓冲区包含关键帧的数据。

CONFIGUER_FLAG_ENCODE : 如果将此编解码器作为编码器,传此标志。

继续阅读