Android MediaCodec 使用说明

2021-03-16 15:35:33 浏览数 (1)

最近公司要求提供一个支持 Android 硬件转码的底层库,所以自己从头去看了 MediaCodec 相关的知识,费了老大的劲终于完成了。

目前的硬件转码使用 MediaCodec 进行解码和编码,然后使用 FFmpeg 进行文件封装(为了支持文件分块)。

这篇文章主要介绍一些 MediaCodec 的基础知识和使用方式,后面会写如何利用 FFmpeg 封装 MediaCodec 编码后的数据以及 FFmpeg 分块封装的文章。

MediaCodec 可以用来获得安卓底层的多媒体编码,可以用来编码和解码,它是安卓 low-level 多媒体基础框架的重要组成部分。

MediaCodec 的作用是处理输入的数据生成输出数据。首先生成一个输入数据缓冲区,将数据填入缓冲区提供给 codec,codec 会采用异步的方式处理这些输入的数据,然后将填满输出缓冲区提供给消费者,消费者消费完后将缓冲区返还给 codec。

接收的数据

MediaCodec 接受三种数据格式:压缩数据,原始音频数据和原始视频数据。

这三种数据都可以使用 ByteBuffer 作为载体传输给 MediaCodec 来处理。但是当使用原始视频数据时,最好采用 Surface 作为输入源来替代 ByteBuffer,这样效率更高,因为 Surface 使用的更底层的视频数据,不会映射或复制到 ByteBuffer 缓冲区。

压缩数据

压缩数据可以作为解码器的输入数据或者编码器的输出数据,需要指定数据格式,这样 codec 才能知道如何处理这些压缩数据。

对于视频数据而言,通常是一帧数据;音频数据,一般是单个处理单元。

原始音频数据

原始音频数据即编码器的输入数据,解码器的输出数据。包含整个 PCM 音频数据帧,这是通道顺序中每个通道的一个样本。每个采样都是以本地字节顺序的 16 位有符号整数。

原始视频数据

原始视频数据也是编码器的输入数据,解码器的输出数据。即yuv数据,MediaCodec主要支持的格式为:

  • native raw video format : COLOR_FormatSurface,用来处理 Surface 模式的数据输入输出
  • flexible YUV buffers : 例如 COLOR_FormatYUV420Flexible
  • specific formats: 支持ByteBuffer模式,有一些厂家会定制

使用流程

编解码器处理输入数据并产生输出数据,MediaCodec 使用输入输出缓存,异步处理数据。

  • 请求一个空的输入 input buffer
  • 填入数据、并将其交给 MediaCodec
  • MediaCodec 处理数据后,将处理后的数据放在一个空的 output buffer
  • 获取填充数据了的 output buffer,得到其中的数据,然后将其返还给 MediaCodec

首先了解下 MediaCodec 中的生命周期

同步状态

MediaCodec 大体上分为三种状态:Stopped、Executing 和 Released。

创建 MediaCodec

首先是如何创建 MediaCodec,在知道 MimeType 的情况下,可以通过 createDecoderByType, createEncoderByType, createByCodecName 方法来获取实例。

如果不知道 MimeType,可以使用 MediaCodecList.findDecoderForFormat、 MediaCodecList.findEncoderForFormat 来获取。

创建成功之后,MediaCodec 进入 Uninitialized 状态。

Configuration

在创建好 MediaCodec 之后,需要对其进行设置,这样 MediaCodec 的状态就可以由 uninitialized 变成 configured

代码语言:javascript复制
public void configure(
            @Nullable MediaFormat format,
            @Nullable Surface surface, @Nullable MediaCrypto crypto,
            @ConfigureFlag int flags) {
        configure(format, surface, crypto, null, flags);
}
public void configure(
            @Nullable MediaFormat format, @Nullable Surface surface,
            @ConfigureFlag int flags, @Nullable MediaDescrambler descrambler) {
        configure(format, surface, null,
                descrambler != null ? descrambler.getBinder() : null, flags);
}
代码语言:javascript复制

这里最重要的参数是 MediaFormat, 如果某些参数没有设置的话,会导致 MediaCodec 抛出 IllegalStateException.

Video 所必须的 Format Setting

Encoder

Decoder

KEY_MIME

✔️

✔️

KEY_BIT_RATE

✔️

KEY_WIDTH

✔️

✔️

KEY_HEIGHT

✔️

✔️

KEY_COLOR_FORMAT

✔️

KYE_FRAME_RATE

✔️

KEY_I_FRAME_INTERVAL

✔️

Audio 所必须的 Format Setting

Encoder

Decoder

KEY_MIME

✔️

✔️

KEY_BIT_RATE

✔️

KEY_CHANNEL_COUNT

✔️

✔️

KEY_SAMPLE_RATE

✔️

✔️

输入数据与获取编解码后的数据

从 5.0 开始,首选方法是在调用 configure 方法之前通过设置回调来异步处理数据。所以这里就直接介绍异步模式下如何输入需要编解码的数据,以及如何获取编解码后的数据。

异步模式

异步状态

官方示例代码:

代码语言:javascript复制
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
// 设置回调方法
codec.setCallback(new MediaCodec.Callback() {
   /**
    * mediacodec 存在可用输入缓冲
    */
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // 可通过 MediaExtractor 读取 video 或 audio 数据,然后填充数据到缓冲区
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   /**
    * 输出缓冲填充完数据后
    */
   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 获取输出缓冲(其中包含编解码后数据)
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); 
     // 处理编解码后的数据
     …
     // 返还输出缓冲给 codec
     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; 
   }

    /**
     * 发生错误
     */
   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); 
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

看一个几个重要的方法

代码语言:javascript复制
ByteBuffer getInputBuffer(int index)

该方法返回一个已清空、可写入的 input 缓冲区,通过调用 ByteBuffer.put(data) 方法将 data 中的数据放到缓冲区,然后调用

代码语言:javascript复制
/**
 * @param index              - 缓冲区索引
 * @param offset             - 缓冲区提交数据的起始位置
 * @param size               - 提交的数据长度
 * @param presentationTimeUs - 时间戳
 * @param flags              - BUFFER_FLAG_CODEC_CONFIG:配置信息;
 *                             BUFFER_FLAG_END_OF_STREAM:结束标志;
 *                             BUFFER_FLAG_KEY_FRAME:关键帧
 */
void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)

就可以将缓冲区返回给 codec。

代码语言:javascript复制
ByteBuffer getOutputBuffer(int index)
代码语言:javascript复制

该方法返回一个 output 缓冲区,包含解码或编码后的数据。

代码语言:javascript复制
void releaseOutputBuffer(int index, boolean render)
void releaseOutputBuffer(int index, long renderTimeStampNs)

这两个方法都会释放 index 所指向的缓冲区。

处理完需要编/解码的数据之后,调用 stop & release 方法释放 MediaCodec。

-- END --

0 人点赞