9、MediaCodec编解码基础:MediaCodec工作原理、异步处理模式、编解码器配置

各位好,我是老张。今天我们来聊聊 Android 音视频开发里最核心的一个组件——MediaCodec。

说实话,我刚接触 MediaCodec 那会儿,也是一头雾水。什么 Buffer、什么状态机、什么异步回调……文档翻来覆去看了好几遍,代码一跑还是崩。后来踩的坑多了,慢慢就摸清了它的脾气。

这一章,我就把 MediaCodec 的工作原理、异步处理模式、以及如何配置编解码器,掰开了揉碎了讲给你听。嗯,咱们直接开始。

9.1 MediaCodec 是什么?

MediaCodec 是 Android 官方提供的底层音视频编解码 API。说白了,它就是一个能让你直接操作硬件编解码器的接口。

你想想看,平时我们用 MediaPlayer 播放视频,那是封装好的高级 API。但如果你想做自定义的播放器、视频编辑器、或者直播推流,MediaPlayer 就不够用了。这时候,MediaCodec 就派上了用场。

我个人习惯把 MediaCodec 理解成一个“黑盒子”:

  • 你往里面塞原始数据(比如 YUV 视频帧、PCM 音频数据)
  • 它给你吐出编码后的数据(比如 H.264 视频流、AAC 音频流)
  • 反过来,你塞压缩数据,它吐原始数据

就这么简单。但真正用起来,细节就多了。

9.2 MediaCodec 的工作原理

MediaCodec 内部维护了一套 状态机。这是理解它的关键。我刚开始学的时候,就是没搞懂状态机,结果代码跑着跑着就报 IllegalStateException。

它的状态流转大致是这样的:

Stopped → Executing → Stopped

Stopped 状态包含:Uninitialized、Configured、Error
Executing 状态包含:Flushed、Running、End-of-Stream

咱们用一张图来直观感受一下:

MediaCodec 状态机流转图 Uninitialized Configured Flushed Running End-of-Stream configure() start() dequeueInputBuffer() queueInputBuffer(BUFFER_FLAG_END_OF_STREAM) flush() Error 异常 正常流转 异常/重置

看到这张图,你应该能明白:MediaCodec 不是你想用就能随便用的。你得按照它的规矩来——先创建、再配置、然后启动、接着处理数据、最后停止释放。

我曾经在一个项目里,因为忘记调用 start() 就直接去取 Buffer,结果崩溃了半小时才找到原因。嗯,这种低级错误,犯过一次就记住了。

9.3 异步处理模式

MediaCodec 支持两种模式:同步模式异步模式

同步模式是 API 21 之前的老方式。你得自己写循环,不断去轮询 Buffer 的状态。代码写起来又臭又长,还容易出问题。

异步模式是 API 21 引入的。MediaCodec 会通过回调告诉你:“嘿,我这边有空闲的输入 Buffer 了”或者“嘿,我这边有编码好的输出数据了”。

我个人强烈建议你使用异步模式。为什么?

  • 代码更简洁:不用自己写 while 循环
  • 性能更好:回调机制避免了忙等
  • 更容易集成:和 Android 的 Handler 机制天然契合

来看一段异步模式的代码骨架:

// 创建 MediaCodec 实例
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);

// 配置 MediaFormat
MediaFormat format = MediaFormat.createVideoFormat(
    MediaFormat.MIME_TYPE_AVC, 1280, 720);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);

// 设置异步回调
codec.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // 有输入 Buffer 可用了
        ByteBuffer inputBuffer = codec.getInputBuffer(index);
        // 往 inputBuffer 里塞数据
        // ...
        codec.queueInputBuffer(index, 0, size, pts, flags);
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index,
                                         MediaCodec.BufferInfo info) {
        // 有编码好的输出数据了
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
        // 处理 outputBuffer 中的数据
        // ...
        codec.releaseOutputBuffer(index, false);
    }

    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 出错了,处理异常
    }

    @Override
    public void onOutputFormatChanged(MediaCodec codec,
                                       MediaFormat format) {
        // 输出格式变了(比如分辨率变化)
    }
});

// 配置并启动
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();

小提示:异步回调是在 Binder 线程池里执行的。如果你要在回调里更新 UI,记得 post 到主线程 Handler 上。我刚开始没注意这个,结果在回调里直接操作 TextView,App 直接闪退。

9.4 编解码器配置

配置 MediaCodec 的核心就是 MediaFormat。这个对象决定了编解码器的行为。

咱们以视频编码为例,看看常用的配置项:

配置项 Key 常量 说明 我的建议
MIME 类型 MediaFormat.KEY_MIME 指定编码格式,如 video/avc 根据需求选,H.264 兼容性最好
分辨率 KEY_WIDTH / KEY_HEIGHT 视频宽高 建议 16 的倍数,否则编码器可能报错
码率 KEY_BIT_RATE 单位 bps,控制视频质量 720p 建议 2-4 Mbps
帧率 KEY_FRAME_RATE 每秒多少帧 一般 30fps 就够用
关键帧间隔 KEY_I_FRAME_INTERVAL 单位秒,-1 表示无关键帧 直播用 1-2 秒,点播可以长一些
颜色格式 KEY_COLOR_FORMAT 输入数据的像素格式 Surface 输入用 COLOR_FormatSurface

注意:不是所有设备都支持所有颜色格式。我曾经在一台老款平板上,用了 COLOR_FormatYUV420SemiPlanar,结果编码出来的视频全是绿的。后来查文档才发现,那台设备只支持 COLOR_FormatYUV420Planar。所以,一定要用 CodecCapabilities 去查询设备支持的能力

配置完 MediaFormat 后,调用 configure() 方法。这个方法有四个参数:

codec.configure(
    format,        // MediaFormat 对象
    surface,       // 输出 Surface(解码时用,编码传 null)
    crypto,        // 加密相关(一般传 null)
    flags          // CONFIGURE_FLAG_ENCODE 或 0
);

这里有个细节:如果你做编码,flags 要传 CONFIGURE_FLAG_ENCODE。做解码就传 0。我见过有人编码时忘了传这个 flag,结果 configure 不报错,但 start 之后直接崩。

9.5 避坑指南

最后,我把自己这些年踩过的坑总结一下:

  1. Buffer 用完要释放releaseOutputBuffer() 别忘了调。否则 MediaCodec 的 Buffer 池会耗尽,导致死锁。
  2. PTS 要连续:Presentation Time Stamp 必须单调递增。我见过有人用 System.currentTimeMillis() 当 PTS,结果因为时间回退导致编码器罢工。
  3. 结束要发 EOS:数据送完后,记得在 queueInputBuffer() 的 flags 参数里加上 BUFFER_FLAG_END_OF_STREAM。否则编码器会一直等你送数据。
  4. Surface 输入效率更高:如果你做视频编码,用 Surface 输入比手动塞 ByteBuffer 快得多。因为 Surface 输入走的是 GPU 管线,省去了内存拷贝。
  5. 别忘了 release():MediaCodec 用完后一定要调用 release() 释放资源。否则会泄漏硬件编解码器,导致其他 App 也用不了。

嗯,这一章的内容就到这里。MediaCodec 虽然看起来复杂,但只要你理解了它的状态机、掌握了异步回调的用法、配对了 MediaFormat,剩下的就是多写多练了。

记住,纸上得来终觉浅,绝知此事要躬行。去写代码吧。


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