7、MediaCodec基础:MediaCodec架构、异步与同步模式、编解码器配置
各位同学,今天我们来聊聊Android音视频开发中最核心的一个组件——MediaCodec。说实话,我当年刚接触这个API的时候,也被它那套状态机搞得晕头转向。但只要你理解了它的设计思路,就会发现它其实非常优雅。
7.1 MediaCodec的整体架构
MediaCodec说白了,就是一个音视频数据的"加工厂"。你给它原始数据,它给你吐出编码或解码后的结果。这个工厂内部有一套标准的流水线作业流程。
核心概念:MediaCodec采用输入输出队列模型。你往输入队列塞数据,它处理完以后,你去输出队列取结果。就这么简单。
我画了一张图,帮你理解这个架构:
你看,整个流程非常清晰。应用层作为生产者,把原始数据塞进输入队列。MediaCodec核心处理完后,把结果放到输出队列。应用层再去取。嗯,这里要注意,队列是循环复用的,不是无限大的。
7.2 同步模式 vs 异步模式
MediaCodec支持两种工作模式:同步和异步。我刚开始用的时候,一直搞不清楚该选哪个。后来踩过坑才明白,这俩各有各的适用场景。
7.2.1 同步模式
同步模式是最传统的用法。说白了,就是你在主线程或者工作线程里,循环调用dequeueInputBuffer和dequeueOutputBuffer。代码写起来很直观:
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();
// 同步模式:循环处理
while (!isDone) {
// 获取输入缓冲区
int inputIndex = codec.dequeueInputBuffer(10000); // 超时10ms
if (inputIndex >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
// 填充数据...
codec.queueInputBuffer(inputIndex, 0, size, pts, flags);
}
// 获取输出缓冲区
BufferInfo info = new BufferInfo();
int outputIndex = codec.dequeueOutputBuffer(info, 10000);
if (outputIndex >= 0) {
// 处理输出(比如渲染到Surface)
codec.releaseOutputBuffer(outputIndex, true);
}
}
我的经验:同步模式适合简单的播放器场景。我在做一个短视频App时,就用同步模式,代码逻辑很清晰,调试也方便。但要注意,dequeueInputBuffer的超时时间别设太长,否则容易卡住UI。
7.2.2 异步模式
异步模式是Android 5.0引入的。它用回调的方式通知你:有输入缓冲区可用了,或者有输出缓冲区可用了。我个人更喜欢这种模式,因为它更符合事件驱动的思想。
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
// 设置异步回调
codec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
ByteBuffer inputBuffer = codec.getInputBuffer(index);
// 填充数据...
codec.queueInputBuffer(index, 0, size, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
// 处理输出
codec.releaseOutputBuffer(index, true);
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 输出格式变了,比如分辨率变化
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 出错了
}
});
codec.start();
避坑指南:我曾经在异步模式下犯过一个低级错误——在回调里直接做耗时操作。记住,回调是在Binder线程里执行的,千万别在里面做I/O或者复杂计算。否则会阻塞整个编解码流程。
为什么会这样?因为MediaCodec的回调线程和编解码引擎是共享同一个线程池的。你在回调里卡住了,编解码引擎也卡住了。
7.3 编解码器配置详解
配置MediaCodec,说白了就是告诉它:你要处理什么格式的数据,输出到哪里,期望什么效果。我见过很多新手在configure这一步就翻车了。
7.3.1 核心配置参数
| 参数 | 说明 | 示例值 |
|---|---|---|
| MIME类型 | 指定音视频编码格式 | "video/avc"、"audio/mp4a-latm" |
| 宽高 | 视频分辨率 | 1920x1080 |
| 比特率 | 编码时的目标码率 | 2000000 (2Mbps) |
| 帧率 | 视频帧率 | 30 |
| 颜色格式 | 像素数据格式 | COLOR_FormatYUV420SemiPlanar |
| I帧间隔 | 关键帧间隔(编码器) | 1 (全I帧) 或 30 (每30帧一个I帧) |
7.3.2 配置代码示例
我们来看一个完整的配置流程。以H.264编码器为例:
// 创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
// 配置MediaFormat
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1280, 720);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); // 30fps
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 每2秒一个I帧
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
// 配置编码器
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
关键点:解码器配置时,最后一个参数是0(解码)。编码器配置时,最后一个参数是CONFIGURE_FLAG_ENCODE。这个别搞反了,我见过有人传错参数,结果编解码器直接罢工。
7.3.3 解码器配置的特殊之处
解码器配置和编码器不太一样。解码器通常需要指定输出Surface(如果你要做直接渲染的话):
// 创建解码器
MediaCodec decoder = MediaCodec.createDecoderByType("video/avc");
// 配置MediaFormat
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
// 解码器不需要设置比特率、帧率等编码参数
// 但可以设置一些解码相关的参数
format.setInteger(MediaFormat.KEY_MAX_WIDTH, 1920);
format.setInteger(MediaFormat.KEY_MAX_HEIGHT, 1080);
// 配置解码器,输出到Surface
decoder.configure(format, surface, null, 0); // 最后一个参数0表示解码
decoder.start();
我的建议:如果你只是做解码渲染,强烈建议用Surface模式。这样解码器直接输出到Surface,省去了你手动处理YUV数据的麻烦。我在做视频播放器时,用Surface模式性能提升了至少30%。
7.4 状态机与生命周期
MediaCodec内部有一套严格的状态机。你想想看,如果不按状态来操作,API会直接抛异常。我刚开始学的时候,经常在错误的状态调用错误的方法,然后一脸懵逼地看异常日志。
状态流转是这样的:
- Uninitialized:刚创建完,啥也没干
- Configured:调用了configure,但还没start
- Executing:调用了start,正在干活
- Flushed:调用了flush,清空所有缓冲区
- End of Stream:收到了EOS标志,处理完最后的数据
- Released:调用了release,资源释放
避坑指南:我曾经在Executing状态下直接调了release,结果导致内存泄漏。正确的做法是先stop,再release。虽然API文档说可以直接release,但为了保险起见,我建议你按部就班来。
7.5 缓冲区管理技巧
缓冲区管理是MediaCodec使用中最容易出问题的地方。我总结了几条经验:
- 及时释放:输出缓冲区用完就release,别占着茅坑不拉屎。缓冲区数量有限,你不释放,编解码器就没法继续干活。
- 不要缓存引用:ByteBuffer对象是复用的,别把它存起来以后再用。我见过有人把getInputBuffer返回的ByteBuffer存到列表里,结果数据全乱了。
- 注意PTS:Presentation Time Stamp一定要正确设置。尤其是编码时,PTS乱了,播放端就会卡顿或者花屏。
- 处理EOS:在输入队列塞入BUFFER_FLAG_END_OF_STREAM后,要等到输出队列也收到这个标志,才算真正结束。
好了,关于MediaCodec的基础知识就讲到这里。这些内容虽然基础,但非常重要。你把这些搞明白了,后面学音视频合成、实时处理、硬编硬解这些高级话题,就会轻松很多。