13、MediaCodec API 深度解析:异步模式、输入输出 Buffer 管理、编解码参数配置
MediaCodec 这个 API,说实话,是 Android 多媒体开发里绕不开的一座山。我早年刚接触它时,也被同步模式的复杂状态机搞得焦头烂额。后来 Android 5.0 引入了异步模式,整个开发体验才算真正「上道」了。今天我们就来彻底拆解它。
13.1 异步模式:别再死循环轮询了
同步模式下,你得自己写一个死循环,不断调用 dequeueInputBuffer() 和 dequeueOutputBuffer()。代码写起来像这样:
while (!isEOS) {
int inIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inIndex >= 0) {
// 填充数据...
codec.queueInputBuffer(inIndex, ...);
}
int outIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outIndex >= 0) {
// 处理输出...
codec.releaseOutputBuffer(outIndex, false);
}
}
这种写法有个大问题:TIMEOUT_US 设短了 CPU 空转,设长了延迟高。我在项目中遇到过,某些低端设备上同步模式会导致掉帧严重,因为主线程被 MediaCodec 的轮询卡住了。
异步模式就优雅多了。你只需要注册一个回调:
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 系统通知你:可以往 index 这个 buffer 里写数据了
ByteBuffer inputBuffer = codec.getInputBuffer(index);
// 填充 inputBuffer...
codec.queueInputBuffer(index, offset, size, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
// 系统通知你:index 这个 buffer 已经解码完成
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
// 渲染或处理...
codec.releaseOutputBuffer(index, true);
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 出错了,赶紧处理
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 输出格式变了,比如分辨率动态切换
}
});
你看,不需要自己写循环了。系统会在合适的时机回调你。我个人习惯用异步模式处理视频播放,因为 回调天然是线程安全的,而且不会阻塞主线程。
13.2 输入输出 Buffer 管理:别把内存搞炸了
Buffer 管理是 MediaCodec 最容易踩坑的地方。说白了,就是「谁借谁还」的问题。
输入 Buffer:你从 getInputBuffer(index) 拿到的是一个 直接引用,不是拷贝。你往里面写数据,然后调用 queueInputBuffer() 把所有权交还给编解码器。之后这个 index 就不能再用了,直到下一次 onInputBufferAvailable 回调。
输出 Buffer:类似,你从 getOutputBuffer(index) 拿到输出数据,处理完后必须调用 releaseOutputBuffer() 归还。如果不归还,编解码器会耗尽所有 buffer,然后卡死。
这里有个关键点:ByteBuffer 的 position 和 limit。系统返回的 buffer 可能已经设置了 position 和 limit,你直接读取即可。但如果你要往输入 buffer 里写数据,记得先 buffer.clear() 重置位置。
另外,从 Android 5.0 开始,推荐使用 getInputBuffer() 和 getOutputBuffer() 而不是 getInputBuffers()。后者返回的是数组,每次调用都可能重新分配,性能差一些。
13.3 编解码参数配置:别让参数「打架」
配置 MediaCodec 时,MediaFormat 是关键。我见过不少新手把参数写错,导致编解码失败。这里列几个容易出问题的点:
| 参数 | 说明 | 常见坑 |
|---|---|---|
KEY_WIDTH / KEY_HEIGHT |
视频宽高 | 必须与实际数据一致,否则解码花屏 |
KEY_COLOR_FORMAT |
颜色格式 | 编码器必须指定,解码器通常忽略 |
KEY_BIT_RATE |
编码码率 | 设太低画质差,设太高可能被厂商限制 |
KEY_FRAME_RATE |
帧率 | 编码时建议设置,解码器通常忽略 |
KEY_I_FRAME_INTERVAL |
关键帧间隔(秒) | 设为 1 表示每秒一个关键帧,适合直播 |
配置编码器时,我建议先查询设备的支持列表:
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
String codecName = list.findEncoderForFormat(format);
if (codecName == null) {
// 当前设备不支持这个格式,需要降级
}
嗯,这里要注意:不同厂商的编解码器能力差异很大。比如某些华为设备对 4K 编码支持不好,你硬要设 4K 分辨率,它可能直接报错。我一般会准备一套降级方案:如果 4K 不行,就降到 1080p。
13.4 核心流程:一张图看懂
下面这张图总结了 MediaCodec 异步模式下的完整数据流:
这张图里,输入源 到 onInputBufferAvailable 是「借 buffer → 填数据 → 归还」的过程。MediaCodec 内部解码完成后,通过 onOutputBufferAvailable 把结果交给你。你处理完再 release,循环继续。
如果中间出了错,onError 会被触发。我建议在 onError 里做两件事:一是释放当前 codec,二是尝试重新创建。不要试图在出错后继续使用同一个 codec,它的内部状态已经不可靠了。
13.5 参数配置实战:编码器示例
最后,给一个完整的编码器配置示例。这是我项目中常用的模板:
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1280, 720);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000); // 2 Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 每 2 秒一个关键帧
format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.setCallback(new MediaCodec.Callback() { ... });
encoder.start();
这里我用了 COLOR_FormatSurface,因为编码器通常配合 Surface 输入效率更高。如果你用 ByteBuffer 输入,记得选 COLOR_FormatYUV420Flexible,这是大多数设备都支持的格式。
好了,MediaCodec 的核心内容就这些。实际开发中,你还会遇到各种厂商兼容性问题,但掌握了今天讲的这些,你已经能应对 90% 的场景了。