最近公司要求提供一个支持 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
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);
}这里最重要的参数是 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 方法之前通过设置回调来异步处理数据。所以这里就直接介绍异步模式下如何输入需要编解码的数据,以及如何获取编解码后的数据。
异步模式

异步状态
官方示例代码:
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();看一个几个重要的方法
ByteBuffer getInputBuffer(int index)该方法返回一个已清空、可写入的 input 缓冲区,通过调用 ByteBuffer.put(data) 方法将 data 中的数据放到缓冲区,然后调用
/**
* @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。
ByteBuffer getOutputBuffer(int index)该方法返回一个 output 缓冲区,包含解码或编码后的数据。
void releaseOutputBuffer(int index, boolean render)
void releaseOutputBuffer(int index, long renderTimeStampNs)这两个方法都会释放 index 所指向的缓冲区。
处理完需要编/解码的数据之后,调用 stop & release 方法释放 MediaCodec。
作者:GeorgeMR
链接:https://www.jianshu.com/p/7cdf5b495ada
-- END --
进技术交流群,扫码添加我的微信:Byte-Flow

获取相关资料和源码
推荐:
全网最全的 Android 音视频和 OpenGL ES 干货,都在这了
面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?
