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) {
        // 输出格式变了,比如分辨率动态切换
    }
});

你看,不需要自己写循环了。系统会在合适的时机回调你。我个人习惯用异步模式处理视频播放,因为 回调天然是线程安全的,而且不会阻塞主线程。

小提示:异步模式下,onInputBufferAvailable 和 onOutputBufferAvailable 是在同一个内部线程里串行回调的。所以你不必担心并发问题,但也不要在这个回调里做耗时操作,否则会阻塞后续回调。

13.2 输入输出 Buffer 管理:别把内存搞炸了

Buffer 管理是 MediaCodec 最容易踩坑的地方。说白了,就是「谁借谁还」的问题。

输入 Buffer:你从 getInputBuffer(index) 拿到的是一个 直接引用,不是拷贝。你往里面写数据,然后调用 queueInputBuffer() 把所有权交还给编解码器。之后这个 index 就不能再用了,直到下一次 onInputBufferAvailable 回调。

输出 Buffer:类似,你从 getOutputBuffer(index) 拿到输出数据,处理完后必须调用 releaseOutputBuffer() 归还。如果不归还,编解码器会耗尽所有 buffer,然后卡死。

我曾经踩过的坑:有一次做实时滤镜,我在 onOutputBufferAvailable 里把 outputBuffer 的数据拷贝出来做处理,但忘了 release。结果跑了 30 秒后,所有输出 buffer 都被占满了,画面直接卡住。排查了半天才发现是 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 异步模式下的完整数据流:

MediaCodec 异步模式核心流程 输入源 文件/摄像头/网络 onInputBufferAvailable 获取输入 Buffer 填充数据 → queueInputBuffer MediaCodec 编解码引擎 内部状态机 onOutputBufferAvailable 获取输出 Buffer 处理 → releaseOutputBuffer 渲染/处理 Surface/滤镜/存储 循环处理下一帧 onError 异常处理/重试

这张图里,输入源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,这是大多数设备都支持的格式。

核心要点:异步模式 + 正确的 Buffer 管理 + 合理的参数配置 = 稳定的 MediaCodec 应用。三者缺一不可。

好了,MediaCodec 的核心内容就这些。实际开发中,你还会遇到各种厂商兼容性问题,但掌握了今天讲的这些,你已经能应对 90% 的场景了。

公众号:蓝海资料掘金营,微信deep3321