最近公司要求提供一个支持 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 --