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 不是你想用就能随便用的。你得按照它的规矩来——先创建、再配置、然后启动、接着处理数据、最后停止释放。
我曾经在一个项目里,因为忘记调用 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 避坑指南
最后,我把自己这些年踩过的坑总结一下:
- Buffer 用完要释放:
releaseOutputBuffer()别忘了调。否则 MediaCodec 的 Buffer 池会耗尽,导致死锁。 - PTS 要连续:Presentation Time Stamp 必须单调递增。我见过有人用 System.currentTimeMillis() 当 PTS,结果因为时间回退导致编码器罢工。
- 结束要发 EOS:数据送完后,记得在
queueInputBuffer()的 flags 参数里加上BUFFER_FLAG_END_OF_STREAM。否则编码器会一直等你送数据。 - Surface 输入效率更高:如果你做视频编码,用 Surface 输入比手动塞 ByteBuffer 快得多。因为 Surface 输入走的是 GPU 管线,省去了内存拷贝。
- 别忘了 release():MediaCodec 用完后一定要调用
release()释放资源。否则会泄漏硬件编解码器,导致其他 App 也用不了。
嗯,这一章的内容就到这里。MediaCodec 虽然看起来复杂,但只要你理解了它的状态机、掌握了异步回调的用法、配对了 MediaFormat,剩下的就是多写多练了。
记住,纸上得来终觉浅,绝知此事要躬行。去写代码吧。
公众号:蓝海资料掘金营,微信deep3321