在修复 #19 后的验证会话中(54.1s,KWin Portal/PipeWire,WebRTC viewer 连接),观察到三类异常叠加,最终导致秒级端到端延迟,用户被迫主动断开。
11:53:22.505 BWE=5_000_000 bps ← 连接初始估计 11:53:26.323 BWE=5_512_893 bps 11:53:27.569 BWE=6_078_121 bps 11:53:28.283 BWE=6_701_719 bps 11:53:29.361 BWE=7_389_432 bps 11:53:30.535 BWE=8_147_630 bps 11:53:32.148 BWE=8_985_074 bps 11:53:34.486 BWE=9_906_751 bps ← 接近翻倍
每条 updating encoder bitrate from BWE feedback 都被即时应用(avhw.rs 的 bit_rate = target_bps as i64)。
updating encoder bitrate from BWE feedback
avhw.rs
bit_rate = target_bps as i64
11:53:39.606 received keyframe request from viewer → IDR 336_232 bytes 11:53:39.810 received keyframe request from viewer → IDR 280_954 bytes ← +204ms 11:53:40.028 received keyframe request from viewer → IDR 309_335 bytes ← +218ms 11:53:40.247 received keyframe request from viewer → IDR 304_086 bytes ← +219ms 11:53:40.450 received keyframe request from viewer → IDR 251_964 bytes ← +203ms
对比首个连接 IDR(11:53:22.515)只有 67_357 字节——风暴中的 IDR 比首 IDR 大 4-5 倍。
观察者主观体验"至少好几秒"延迟,在 11:53:42.8 主动断开。
src/webrtc.rs
take_force_keyframe()
set_need_keyframe()
src/state_portal.rs:725-736
src/avhw.rs:1130-1138
5 * W * H * fps / 100
state_portal.rs:202-204
#16 修复让 keyframe 响应从 2.1s 降到 10ms(forced-idr=1 + pict_type=IDR)。这是好事,但副作用是:客户端可以以任意频率请求 keyframe,服务端都会立即响应。
forced-idr=1
pict_type=IDR
当客户端遇到以下任一情况就会发 PLI:
在 BWE 一路攀升到 9.9 Mbps 后,瞬时突发开始超过实际可持续吞吐,客户端解码陷入困境 → 发 PLI → 收到 300KB IDR → 解码更慢 → 再发 PLI。死循环。
webrtc_thread_loop(state_portal.rs:725-736)每收到一个更大的 BWE 就即时应用到编码器。但 BWE 是估计值,不是可持续吞吐,且:
webrtc_thread_loop
state_portal.rs:725-736
结果:12 秒内码率翻倍,瞬时突发开始冲击链路。
首个连接 IDR 在 5 Mbps 时只有 67KB。但码率升到 9.9 Mbps 后,IDR 暴涨到 300KB。这暗示 VBV buffer 配置(avhw.rs 中 libx264 初始化)允许 IDR 占用过多瞬时带宽。
按 WebRTC H.264 实践,单个 IDR 应该被限制在 ~50-150KB(取决于分辨率/帧率),避免瞬时 burst。
// webrtc.rs 中,新增 last_pli_time: Option<Instant> const PLI_MIN_INTERVAL: Duration = Duration::from_millis(500); pub fn take_force_keyframe(&mut self) -> bool { let now = Instant::now(); if let Some(last) = self.last_pli_time { if now.duration_since(last) < PLI_MIN_INTERVAL { return false; // 限流:丢弃过频的 PLI } } self.last_pli_time = Some(now); self.force_keyframe.take().unwrap_or(false) }
500ms 限流意味着最多 2 keyframe/秒,对 H.264 WebRTC 而言足够安全。
// state_portal.rs webrtc_thread_loop 中,应用 BWE 前先 clamp const WEBRTC_MAX_BITRATE: u64 = 8_000_000; // 8 Mbps 上限 let clamped_bwe = bwe.min(WEBRTC_MAX_BITRATE); // ... 后续应用 clamped_bwe
或者做成 CLI flag:--max-bitrate 8000000。
--max-bitrate 8000000
在 avhw.rs libx264 初始化时设置更紧的 VBV:
rc_buffer_size = bitrate / fps
rc_max_rate = bitrate * 1.2
让 IDR 不能用掉整个 VBV buffer,强制 x264 用更大的 QP 压缩 IDR。
wl-webrtc --port 56666 -v > /tmp/wl-webrtc-test.log 2>&1 # 浏览器连接 WebRTC viewer(H.264) # 持续观看 30+ 秒,观察日志中 BWE 攀升 # 在 ~15-20 秒后,注意日志中 "received keyframe request from viewer" 频率 # 客户端体验:从可接受 → 卡顿 → 秒级延迟
期望日志模式:
grep "updating encoder bitrate" /tmp/wl-webrtc-test.log # 看攀升 grep "received keyframe request" /tmp/wl-webrtc-test.log # 看风暴 grep "IDR keyframe" /tmp/wl-webrtc-test.log # 看 IDR 大小
完整会话日志(54.1s,34s idle + 20s connected):
updating encoder bitrate
received keyframe request
got IDR keyframe
Total: 983 frames in 54.1s, avg 18.2fps
kb/s:5616.11
Critical —— 直接破坏可用性,用户被迫断开。建议优先于 #15、#18、#20、#21 处理。
在设计修复方案时,通过 Oracle 审核和 x264 源码 验证,发现了 issue 描述中未提到的更深根因:
原 issue 三大根因(A: PLI 无限流 / B: 码率无上限 / C: VBV 不约束 IDR)中,根因 C 比 issue 描述的严重得多 —— VBV 实际完全失效了。
encoder/ratecontrol.c:658-661:
encoder/ratecontrol.c:658-661
int kilobit_size = h->param.i_avcintra_class ? 1024 : 1000; int vbv_buffer_size = h->param.rc.i_vbv_buffer_size * kilobit_size; int vbv_max_bitrate = h->param.rc.i_vbv_max_bitrate * kilobit_size;
x264.c:730-731 CLI 帮助:
x264.c:730-731
--vbv-maxrate <integer> Max local bitrate (kbit/s) --vbv-bufsize <integer> Set size of the VBV buffer (kbit)
结论:vbv-maxrate 和 vbv-bufsize 的单位是 kbit/s 和 kbit(除以 1000),不是 bps。
vbv-maxrate
vbv-bufsize
src/avhw.rs:1842-1843:
src/avhw.rs:1842-1843
let vbv_maxrate = bitrate; // 5_529_600 ❌ let vbv_bufsize = bitrate / 4; // 1_382_400 ❌
5529600
5529
1382400
1382
VBV 等于完全关闭 —— 173 MB buffer,任何现实帧都不会触发 VBV 限流。
如果 VBV 真在工作,5.5 Mbps 配置下单帧最大 ≈ 172KB,336KB 不可能出现。
src/avhw.rs:1972-1982 的 vbv_x264opts_format 测试断言了错误的值:
src/avhw.rs:1972-1982
vbv_x264opts_format
let vbv_maxrate = bitrate; // 错:5_000_000 assert!(opts.contains("vbv-maxrate=5000000")); // 错:应该是 5000
测试通过,但行为是错的。典型的"测试覆盖了错误的实现"。
src/avhw.rs:1842-1843 改 2 行:
// x264 的 vbv-maxrate / vbv-bufsize 单位是 kbit/s 和 kbit,不是 bps // 参考 x264 源码 encoder/ratecontrol.c:658 的 *1000 转换 let vbv_maxrate = bitrate / 1000; let vbv_bufsize = (bitrate / 4) / 1000;
原 issue 的三大根因优先级需要调整:
三个 fix 必须一起做,因为它们互相强化:
三者叠加 = 链路带宽永远不会被突发压垮。
No dependencies set.
The note is not visible to the blocked user.
现象
在修复 #19 后的验证会话中(54.1s,KWin Portal/PipeWire,WebRTC viewer 连接),观察到三类异常叠加,最终导致秒级端到端延迟,用户被迫主动断开。
1. 码率从 5 Mbps 攀升到 9.9 Mbps(12 秒内翻倍)
每条
updating encoder bitrate from BWE feedback都被即时应用(avhw.rs的bit_rate = target_bps as i64)。2. Keyframe 风暴:845ms 内 5 个 PLI 触发 5 个 IDR(共 ~1.48 MB 突发)
对比首个连接 IDR(11:53:22.515)只有 67_357 字节——风暴中的 IDR 比首 IDR 大 4-5 倍。
3. 用户感知:秒级延迟
观察者主观体验"至少好几秒"延迟,在 11:53:42.8 主动断开。
触发位置
src/webrtc.rs(take_force_keyframe()+set_need_keyframe())—— 当前对客户端 PLI 没有任何限流src/state_portal.rs:725-736(webrtc_thread_loop 中 BWE → bitrate_tx)+src/avhw.rs:1130-1138(应用 bitrate)5 * W * H * fps / 100公式(state_portal.rs:202-204)只用于初始默认值;BWE 反馈没有上限校验根因分析
根因 A:PLI 无限流(核心)
#16 修复让 keyframe 响应从 2.1s 降到 10ms(
forced-idr=1+pict_type=IDR)。这是好事,但副作用是:客户端可以以任意频率请求 keyframe,服务端都会立即响应。当客户端遇到以下任一情况就会发 PLI:
在 BWE 一路攀升到 9.9 Mbps 后,瞬时突发开始超过实际可持续吞吐,客户端解码陷入困境 → 发 PLI → 收到 300KB IDR → 解码更慢 → 再发 PLI。死循环。
根因 B:码率跟随 BWE 一路攀升,无上限
webrtc_thread_loop(state_portal.rs:725-736)每收到一个更大的 BWE 就即时应用到编码器。但 BWE 是估计值,不是可持续吞吐,且:结果:12 秒内码率翻倍,瞬时突发开始冲击链路。
根因 C:Keyframe 大小不受 VBV 严格约束
首个连接 IDR 在 5 Mbps 时只有 67KB。但码率升到 9.9 Mbps 后,IDR 暴涨到 300KB。这暗示 VBV buffer 配置(
avhw.rs中 libx264 初始化)允许 IDR 占用过多瞬时带宽。按 WebRTC H.264 实践,单个 IDR 应该被限制在 ~50-150KB(取决于分辨率/帧率),避免瞬时 burst。
影响
期望方案(择一/组合)
方案 1:PLI 限流(必做,最小代价)
500ms 限流意味着最多 2 keyframe/秒,对 H.264 WebRTC 而言足够安全。
方案 2:码率硬性上限(强烈建议)
或者做成 CLI flag:
--max-bitrate 8000000。方案 3:VBV 约束 IDR 大小(可选优化)
在
avhw.rslibx264 初始化时设置更紧的 VBV:rc_buffer_size = bitrate / fps(1 帧大小的 buffer)rc_max_rate = bitrate * 1.2(瞬时上限 20%)让 IDR 不能用掉整个 VBV buffer,强制 x264 用更大的 QP 压缩 IDR。
复现
期望日志模式:
日志证据
完整会话日志(54.1s,34s idle + 20s connected):
updating encoder bitrate,从 5 Mbps 到 9.9 Mbpsreceived keyframe request+ 5 次got IDR keyframe,集中在 845ms 内Total: 983 frames in 54.1s, avg 18.2fps(目标 30fps,损失 40%)kb/s:5616.11(接近 BWE 中位值)关联
严重度
Critical —— 直接破坏可用性,用户被迫断开。建议优先于 #15、#18、#20、#21 处理。
🔍 更新:发现更根本的根因 —— VBV 单位 bug
在设计修复方案时,通过 Oracle 审核和 x264 源码 验证,发现了 issue 描述中未提到的更深根因:
现象再分析
原 issue 三大根因(A: PLI 无限流 / B: 码率无上限 / C: VBV 不约束 IDR)中,根因 C 比 issue 描述的严重得多 —— VBV 实际完全失效了。
x264 VBV 单位约定(源码实锤)
encoder/ratecontrol.c:658-661:x264.c:730-731CLI 帮助:结论:
vbv-maxrate和vbv-bufsize的单位是 kbit/s 和 kbit(除以 1000),不是 bps。当前代码 bug
src/avhw.rs:1842-1843:实际效果(5.5 Mbps 初始码率下)
vbv-maxrate传入55296005529vbv-bufsize传入13824001382VBV 等于完全关闭 —— 173 MB buffer,任何现实帧都不会触发 VBV 限流。
为什么首 IDR 只有 67KB,后续 IDR 却能到 336KB
如果 VBV 真在工作,5.5 Mbps 配置下单帧最大 ≈ 172KB,336KB 不可能出现。
已有测试也错了
src/avhw.rs:1972-1982的vbv_x264opts_format测试断言了错误的值:测试通过,但行为是错的。典型的"测试覆盖了错误的实现"。
修复
src/avhw.rs:1842-1843改 2 行:这改变了修复优先级
原 issue 的三大根因优先级需要调整:
三个 fix 必须一起做,因为它们互相强化:
三者叠加 = 链路带宽永远不会被突发压垮。
参考