5、MediaCodec编码基础:MediaCodec工作流程、输入输出缓冲区、编码器配置、异步模式
各位同学,今天我们来啃一块硬骨头——MediaCodec。说实话,我刚接触Android音视频开发那会儿,最怵的就是这玩意儿。API看着简单,但坑是真不少。不过别怕,今天我把它的底裤扒干净,咱们一起看透它。
5.1 MediaCodec是什么?
MediaCodec是Android官方提供的底层编解码器接口。说白了,它就是个音视频数据的"翻译官"——把原始数据(比如摄像头采集的YUV帧)转成压缩格式(比如H.264),或者反过来。
我个人习惯把它理解成一个"黑盒子":你往里面塞原始数据,它吐出来压缩后的数据。至于里面怎么编的,你不用管,但怎么塞、怎么取,这个流程你必须门儿清。
5.2 核心工作流程:状态机驱动
MediaCodec内部是个严格的状态机。我刚开始学的时候没注意这个,结果各种IllegalStateException砸脸。嗯,这里要注意——它只有三种状态:
- Stopped状态:刚创建或已释放,啥也干不了
- Executing状态:正在干活,包括Flushed、Running、End-of-Stream三个子状态
- Released状态:彻底拜拜
你想想看,如果你在Stopped状态去调用dequeueOutputBuffer,那肯定报错。所以写代码前,先画个状态图贴在工位上。
核心原则:任何操作前,先确认当前状态是否允许。我见过太多人栽在这个坑里。
5.3 输入输出缓冲区:生产者-消费者模型
MediaCodec使用了一组输入缓冲区和输出缓冲区。这其实是个经典的生产者-消费者模型:
- 输入缓冲区:你往里面填原始数据(生产者)
- 输出缓冲区:你从里面取编码后的数据(消费者)
具体操作流程是这样的:
- 调用
dequeueInputBuffer()获取一个空闲的输入缓冲区索引 - 把原始数据塞进去,调用
queueInputBuffer()还给MediaCodec - MediaCodec开始编码,完成后把结果放到输出缓冲区
- 调用
dequeueOutputBuffer()获取输出缓冲区索引 - 取出编码后的数据,处理完调用
releaseOutputBuffer()释放
我在项目中遇到过一个问题:如果生产速度远大于消费速度,缓冲区会爆掉。所以一定要控制好节奏,别一股脑往里塞。
小技巧:dequeueInputBuffer的超时参数别设太大,我一般用1000微秒。设成-1会一直阻塞,容易出问题。
5.4 编码器配置:关键参数详解
配置编码器是重中之重。我见过有人把参数写错,结果编码出来的视频全是马赛克。来看一个典型的H.264编码器配置:
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, // H.264
width, height
);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔1秒
这里有几个参数我要重点说一下:
| 参数 | 说明 | 我的建议 |
|---|---|---|
| KEY_COLOR_FORMAT | 颜色格式,Surface模式用COLOR_FormatSurface | 能用Surface就用Surface,性能好很多 |
| KEY_BIT_RATE | 码率,单位bps | 720p视频建议1.5-2Mbps,1080p建议4-8Mbps |
| KEY_FRAME_RATE | 帧率 | 直播用24或30,别设太高,手机扛不住 |
| KEY_I_FRAME_INTERVAL | 关键帧间隔,单位秒 | 直播推流建议1-2秒,方便seek |
避坑指南:我曾经把KEY_I_FRAME_INTERVAL设成0,结果编码器一个关键帧都不生成,推流出去播放器死活拖不动进度条。后来查了半天文档才发现,0表示不强制插关键帧,全靠编码器自己决定。
5.5 异步模式:告别阻塞烦恼
早期MediaCodec只有同步模式,你得自己写循环去轮询缓冲区。代码又臭又长,还容易卡UI线程。Android 5.0之后引入了异步模式,用回调来处理,清爽多了。
异步模式的核心是两个回调:
- onInputBufferAvailable:编码器告诉你"我有空闲输入缓冲区了,快来填数据"
- onOutputBufferAvailable:编码器告诉你"我编码完了,快来取结果"
来看代码怎么写:
MediaCodec codec = MediaCodec.createByCodecName("OMX.google.h264.encoder");
codec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 获取输入缓冲区,填入数据
ByteBuffer inputBuffer = codec.getInputBuffer(index);
// ... 填入数据 ...
codec.queueInputBuffer(index, 0, data.length, pts, flags);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
MediaCodec.BufferInfo info) {
// 获取输出缓冲区,取出编码数据
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
// ... 处理编码数据 ...
codec.releaseOutputBuffer(index, false);
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
// 处理错误
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 输出格式变了,比如H.264的SPS/PPS来了
}
});
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
我个人习惯用异步模式,代码结构清晰,不容易出bug。但要注意一点:回调是在编码器的工作线程里执行的,别在里面做耗时操作,否则会阻塞编码流程。
经验之谈:onOutputFormatChanged这个回调特别重要。H.264的SPS/PPS就是通过它传出来的。我第一次做推流时没处理这个回调,结果播放器一直黑屏,折腾了两天才发现是少了SPS/PPS。
5.6 知识体系总览
说了这么多,咱们用一张图把MediaCodec的核心知识串起来:
这张图把MediaCodec的四个核心模块串起来了。你写代码的时候,脑子里要有这张图,每一步操作都知道自己在哪个环节。
好了,MediaCodec的基础就讲到这里。记住,这东西就是个工具,多写几遍就熟了。我当年也是被它虐了无数遍,现在闭着眼睛都能写出来。你们也行的。