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 是命令。

关键帧请求的触发条件

什么时候会触发关键帧请求?我总结下来,主要有这几种情况:

  1. 解码失败——这是最常见的触发条件。当接收端发现参考帧丢失,无法解码当前帧时,就会发 PLI。比如你丢了 P 帧依赖的那个 I 帧,那后续所有帧都解不出来,只能请求关键帧。
  2. 长期花屏——如果画面已经花了一段时间(比如超过 1 秒),接收端会主动请求关键帧来“刷新”画面。我记得有一次线上问题,用户说画面偶尔闪一下绿屏,排查后发现是某个 B 帧的参考帧丢了,但解码器没及时触发 PLI,导致花屏持续了 3 秒多。
  3. 新加入会话——用户中途加入一个正在进行的会议,接收端没有任何参考帧,必须请求一个关键帧才能开始解码。这时候通常用 FIR,因为要尽快出图。
  4. 切换分辨率——当发送端切换编码分辨率时,所有旧的参考帧都失效了,接收端需要一个新的关键帧来重新开始解码。
  5. 丢包率过高——如果丢包率超过某个阈值(比如 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 请求在窗口内只发一次。

关键帧请求的完整流程

我用一张图来总结整个流程:

关键帧请求流程 接收端 发送端 ① 解码失败 / 丢包检测 ② 触发条件判断 ③ 限速检查(≤1个/秒) ④ 发送 PLI / FIR RTCP A. 收到 PLI / FIR B. 判断请求类型 C. 生成关键帧(I帧) D. 发送关键帧 RTP 关键帧 ⚠ 风暴检测:去重 + 限速 ⚠ 风暴检测:合并请求

这个流程里,最关键的就是第三步的限速检查和风暴检测。我见过很多实现只做了限速,没做去重,结果还是出了风暴。两个都要做,缺一不可。

实际调优建议

最后,分享几个我在项目中实际用过的调优参数:

  • PLI 发送间隔: 1 秒。如果连续丢包超过 3 秒,可以缩短到 500ms,但不能再短了。
  • FIR 发送间隔: 2 秒。FIR 是命令,不能发太频繁,否则编码器会崩溃。
  • GOP 长度: 2 秒(30fps 下约 60 帧)。网络差时可以动态调整到 1 秒。
  • 去抖窗口: 50ms。同一个 PLI 请求在 50ms 内只发一次。
  • NACK 重传次数: 3 次。超过 3 次还没收到,就发 PLI。

一句话总结:PLI 是建议,FIR 是命令。关键帧请求要限速、要去重、要配合 NACK 使用。别让风暴毁了你的视频质量。

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