15、关键帧请求(PLI/FIR):PLI 与 FIR 的区别、关键帧请求的触发条件、如何避免关键帧风暴
关键帧请求,说白了就是接收端在视频流卡住时,向发送端喊一声:“我啥也看不到了,赶紧给我一个完整的画面!”
这个机制在实时通信里极其重要。没有它,一旦丢包或者解码出错,画面就会一直花屏下去。我早期做WebRTC项目时,就吃过这个亏——当时没处理好关键帧请求,用户反馈“画面卡成PPT”,排查了半天才发现是关键帧请求策略太保守了。
PLI 与 FIR 的区别
这两个都是关键帧请求,但出身不同,行为也不同。
| 特性 | PLI (Picture Loss Indication) | FIR (Full Intra Request) |
|---|---|---|
| 协议来源 | RTCP 反馈(RFC 4585) | RTCP 反馈(RFC 5104) |
| 请求语义 | “我可能丢了数据,你看着办” | “立刻给我一个关键帧,别废话” |
| 发送端响应 | 建议性,编码器可以忽略 | 强制性,必须生成关键帧 |
| 适用场景 | 普通丢包恢复 | 新加入会话、长期花屏 |
| WebRTC 使用 | 默认启用 | 仅用于 SFU 场景 |
PLI 是“软请求”。它告诉发送端:“我这边可能丢了一些帧,如果你方便的话,给我一个关键帧吧。”编码器收到 PLI 后,可以选择在下一个 GOP 边界生成关键帧,也可以忽略——如果它觉得当前码率不允许。
FIR 是“硬请求”。它直接命令发送端:“现在、立刻、马上给我一个关键帧!”发送端收到 FIR 后,必须打断当前编码流程,立即生成一个完整的 I 帧。这在 SFU 场景下特别有用——比如一个新用户加入会议,SFU 会向所有发送端发 FIR,让新用户尽快看到画面。
核心区别一句话:PLI 是建议,FIR 是命令。
关键帧请求的触发条件
什么时候会触发关键帧请求?我总结下来,主要有这几种情况:
- 解码失败——这是最常见的触发条件。当接收端发现参考帧丢失,无法解码当前帧时,就会发 PLI。比如你丢了 P 帧依赖的那个 I 帧,那后续所有帧都解不出来,只能请求关键帧。
- 长期花屏——如果画面已经花了一段时间(比如超过 1 秒),接收端会主动请求关键帧来“刷新”画面。我记得有一次线上问题,用户说画面偶尔闪一下绿屏,排查后发现是某个 B 帧的参考帧丢了,但解码器没及时触发 PLI,导致花屏持续了 3 秒多。
- 新加入会话——用户中途加入一个正在进行的会议,接收端没有任何参考帧,必须请求一个关键帧才能开始解码。这时候通常用 FIR,因为要尽快出图。
- 切换分辨率——当发送端切换编码分辨率时,所有旧的参考帧都失效了,接收端需要一个新的关键帧来重新开始解码。
- 丢包率过高——如果丢包率超过某个阈值(比如 20%),接收端会认为当前流已经不可靠,主动请求关键帧来重置解码状态。
我的经验:不要等到解码失败才发 PLI。如果连续 3 帧都检测到丢包,提前发一个 PLI 往往能避免后续的花屏。这个阈值我一般设成 3,太低了容易触发风暴,太高了又反应太慢。
如何避免关键帧风暴
关键帧风暴,说白了就是接收端疯狂发 PLI/FIR,发送端疯狂生成关键帧,导致码率飙升、网络拥塞、丢包加剧,然后接收端又发更多 PLI……这是个典型的正反馈死循环。
我曾经在一个大并发直播项目中遇到过这个问题。当时 500 个观众同时加入,每个观众都发 FIR,发送端瞬间要生成 500 个关键帧,直接把编码器打爆了。嗯,那次教训挺深刻的。
避免关键帧风暴,有这几个常用手段:
- 限速请求——接收端不能无限制地发 PLI。WebRTC 默认的限速策略是:每秒最多发 1 个 PLI,如果连续丢包,可以放宽到每秒 2-3 个。超过这个频率的请求会被直接丢弃。
- 去重合并——在 SFU 场景下,多个接收端可能同时请求关键帧。SFU 应该合并这些请求,只向发送端发一个 FIR,而不是转发所有请求。我见过有些实现直接把所有 PLI 透传,结果发送端收到几十个重复请求,每个都触发一次关键帧生成,码率直接翻倍。
- 自适应 GOP 大小——如果网络状况不好,发送端可以主动缩短 GOP 长度,比如从 2 秒缩短到 0.5 秒。这样即使接收端丢了关键帧,也能很快等到下一个。但要注意,GOP 太短会降低编码效率,一般不建议低于 0.5 秒。
- 使用 NACK 代替 PLI——如果只是丢了几个包,用 NACK 重传就能恢复,没必要发 PLI。很多新手一看到花屏就发 PLI,其实 NACK 就能解决问题。我建议的策略是:先尝试 NACK 重传,如果重传后还是解不出来,再发 PLI。
避坑指南:我曾经遇到过一个 case,接收端每帧都发 PLI,因为它的解码器有个 bug——每次解码失败都会触发 PLI,但失败的原因其实是参考帧还没到(乱序到达)。结果发送端每帧都生成关键帧,码率从 2Mbps 飙升到 20Mbps。解决方案很简单:在接收端加一个 50ms 的去抖窗口,PLI 请求在窗口内只发一次。
关键帧请求的完整流程
我用一张图来总结整个流程:
这个流程里,最关键的就是第三步的限速检查和风暴检测。我见过很多实现只做了限速,没做去重,结果还是出了风暴。两个都要做,缺一不可。
实际调优建议
最后,分享几个我在项目中实际用过的调优参数:
- PLI 发送间隔: 1 秒。如果连续丢包超过 3 秒,可以缩短到 500ms,但不能再短了。
- FIR 发送间隔: 2 秒。FIR 是命令,不能发太频繁,否则编码器会崩溃。
- GOP 长度: 2 秒(30fps 下约 60 帧)。网络差时可以动态调整到 1 秒。
- 去抖窗口: 50ms。同一个 PLI 请求在 50ms 内只发一次。
- NACK 重传次数: 3 次。超过 3 次还没收到,就发 PLI。
一句话总结:PLI 是建议,FIR 是命令。关键帧请求要限速、要去重、要配合 NACK 使用。别让风暴毁了你的视频质量。