13、多实例解码:多路视频同时解码
多路视频同时解码,这需求现在越来越常见了。
监控墙、直播聚合、画中画、多机位剪辑……说白了,就是让手机或平板同时跑好几个解码器实例。我最早接触这个场景,是在做一款车载环视系统的时候——四个摄像头,四路视频,要同时解码、同时渲染。嗯,那会儿踩了不少坑。
13.1 资源竞争:谁抢了谁的蛋糕?
多实例解码,第一个要面对的问题就是资源竞争。你想想看,一个解码器实例就要占用不少硬件资源,四个实例同时跑,GPU、内存、总线带宽,全都在抢。
主要竞争点有三个:
- 硬件解码器单元:大多数芯片只有 2-4 个硬件解码通道。超过这个数,就得排队等。
- 显存/内存带宽:每路解码都要读写大量 YUV 数据。带宽不够,所有实例一起卡。
- CPU 负载:虽然硬解不占 CPU,但数据搬运、同步、回调处理,还是需要 CPU 参与。
核心结论:多实例解码的瓶颈,往往不在解码本身,而在数据搬运和内存带宽。
我在项目中遇到过一件事:四路 1080p 同时解码,帧率直接掉到 15fps。排查了半天,发现是内存带宽被占满了。后来把输出格式从 NV12 改成 NV21,减少了内存拷贝,问题就解决了。
13.2 资源隔离:别让一路崩了全盘
多实例解码,最怕什么?最怕一路解码出问题,把其他路也拖下水。
我曾经遇到过:某路视频码流异常,解码器内部状态乱了,结果其他几路也跟着花屏。嗯,这就是典型的资源隔离没做好。
隔离策略:
- 独立解码器实例:每个 MediaCodec 实例都是独立的,不要共享。
- 独立 Surface/纹理:每路视频的输出目标要分开,避免渲染冲突。
- 独立线程池:每路解码有自己的线程或线程池,不要混用。
- 独立缓冲区:输入输出缓冲区各自管理,不要交叉引用。
注意:Android 的 MediaCodec 本身是线程安全的,但回调函数不是。千万别在 onOutputBufferAvailable 里做耗时操作。
13.3 线程模型设计:怎么安排这些解码器?
线程模型,是多实例解码的核心设计。我见过两种主流方案:
方案一:每路独立线程
最简单直接。每路视频开一个线程,循环拉数据、送解码、取输出。优点是逻辑清晰,缺点是线程数多,上下文切换开销大。
// 伪代码:每路独立线程
class DecoderThread extends Thread {
private MediaCodec codec;
public void run() {
while (isRunning) {
// 送输入
int inputIndex = codec.dequeueInputBuffer(TIMEOUT);
// 取输出
int outputIndex = codec.dequeueOutputBuffer(info, TIMEOUT);
// 渲染或回调
}
}
}
方案二:统一调度线程池
用一个线程池管理所有解码任务。每个解码器实例注册到调度器,由调度器统一分配时间片。优点是线程数可控,缺点是调度逻辑复杂。
我个人习惯用方案一,但会限制最大线程数。比如 4 路视频,就开 4 个线程,每个线程绑定一个解码器。超过 4 路,就排队等待。
我的建议:线程数不要超过 CPU 核心数的 2 倍。否则上下文切换的开销,会吃掉所有性能收益。
13.4 性能监控:怎么知道哪里卡了?
多实例解码,性能监控比单路复杂得多。你不能只看总帧率,得知道每一路的情况。
监控指标:
| 指标 | 说明 | 正常范围 |
|---|---|---|
| 每路解码帧率 | 每秒钟解码输出的帧数 | ≥ 目标帧率的 90% |
| 解码延迟 | 从输入到输出的时间差 | < 33ms (30fps) |
| 内存占用 | 每路解码器的内存使用量 | 每路 < 50MB |
| CPU 使用率 | 解码线程的 CPU 占用 | 总 CPU < 60% |
| 丢帧率 | 被丢弃的帧数占比 | < 1% |
我曾经在监控项目里加了一个「解码健康度」指标:如果某路连续 3 秒帧率低于 15fps,就自动降低该路的分辨率。嗯,效果还不错。
13.5 核心逻辑:多实例解码架构图
下面这张图,是我总结的多实例解码核心架构。你看一眼就能明白整体流程:
从图里你能看到:每个输入源对应一个独立的解码器实例,解码器实例由线程调度器统一管理,最终输出到渲染层。这就是多实例解码的标准架构。
13.6 避坑指南:我踩过的那些坑
最后,分享几个我实际项目中踩过的坑:
- 坑一:MediaCodec 的 reset 不是万能的。我曾经在码流异常时调用 reset 恢复,结果发现有些芯片的硬件状态没清干净。后来改成 release 再 create,彻底解决。
- 坑二:Surface 的引用计数。多路解码共用 Surface 时,一定要小心引用计数。我遇到过一路释放了 Surface,其他路直接崩溃的情况。
- 坑三:内存泄漏。每路解码器都会分配大量内存,如果忘记释放,内存会迅速涨满。建议用 LeakCanary 或手动监控。
总结一句话:多实例解码,核心是资源隔离和线程调度。做好这两点,剩下的就是细节了。