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 核心逻辑:多实例解码架构图

下面这张图,是我总结的多实例解码核心架构。你看一眼就能明白整体流程:

多实例解码核心架构 输入源 1 输入源 2 输入源 N 解码器实例 1 解码器实例 2 解码器实例 N 线程调度器 / 线程池 输出渲染 / 回调 输入源 解码器实例 线程调度 输出渲染

从图里你能看到:每个输入源对应一个独立的解码器实例,解码器实例由线程调度器统一管理,最终输出到渲染层。这就是多实例解码的标准架构。

13.6 避坑指南:我踩过的那些坑

最后,分享几个我实际项目中踩过的坑:

  • 坑一:MediaCodec 的 reset 不是万能的。我曾经在码流异常时调用 reset 恢复,结果发现有些芯片的硬件状态没清干净。后来改成 release 再 create,彻底解决。
  • 坑二:Surface 的引用计数。多路解码共用 Surface 时,一定要小心引用计数。我遇到过一路释放了 Surface,其他路直接崩溃的情况。
  • 坑三:内存泄漏。每路解码器都会分配大量内存,如果忘记释放,内存会迅速涨满。建议用 LeakCanary 或手动监控。

总结一句话:多实例解码,核心是资源隔离和线程调度。做好这两点,剩下的就是细节了。

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