[webrtc] Portal 路径 PTS 用顺序计数器,客户端 jitter buffer 累积到 2-3 秒操作延迟 #24

Closed
opened 2026-06-20 21:27:30 +08:00 by dailz · 0 comments
Owner

现象

在 #19、#23、#15、#18 全部修复后,WebRTC 客户端仍有 2-3 秒"操作后延迟":

  • 用户移动鼠标 / 操作画面
  • 服务端 5-10ms 内编码完成并发出
  • 但客户端显示这个帧要等 2-3 秒

测试环境:

  • 浏览器 WebRTC 客户端(Chrome/Edge/Firefox)
  • 局域网(无网络延迟/丢包)
  • 服务端 cargo build --release 最新版(test4 日志)

触发位置

src/state_portal.rs:455 (Portal/PipeWire 路径):

let pts = self.frames_encoded as i64;  // ← 顺序计数器,0/1/2/3...

对比 src/state.rs:606-607 (wlr-screencopy 路径,正确的实现):

let pts = (tv_sec as i64) * fps + (tv_usec as i64) * fps / 1_000_000;
// 用真实捕获时间

根因分析

核心错位:PTS 单位 vs 真实时间

Portal 路径用帧序号作为 PTS,假设帧率恒定。但 KWin 在 Wayland 下是损伤驱动(damage-driven),帧率从 1.6 fps(静态画面)到 57 fps(活跃画面)剧烈波动。

客户端 jitter buffer 的误判

WebRTC 客户端用 RTP 时间戳(90kHz 时钟,从 PTS 转换)来判断:

  • 帧应该何时显示
  • 网络抖动有多少
  • jitter buffer 应该多大

静态期间(每 600ms 一帧):

真实时间 第 N 帧 当前 PTS RTP 时间戳(90kHz) 客户端推算"应显示于" 实际到达 客户端算出的"网络抖动"
T=0 帧 0 0 0 0ms T=0 0
T=600ms 帧 1 1 3000 33ms T=600ms +567ms
T=1200ms 帧 2 2 6000 66ms T=1200ms +1134ms
T=5000ms 帧 8 8 24000 266ms T=5000ms +4734ms

客户端算法推断:"帧到达间隔 600ms,但 RTP 时间戳说应该 33ms 间隔,所以网络抖动 567ms,扩大 jitter buffer 到 567ms"。

几个周期后,jitter buffer 累积到 2-3 秒,且不自动缩小(浏览器保守策略)。

为什么用户感觉"操作后延迟"

  1. 用户移动鼠标 → KWin 立即送新帧 → 服务端 5-10ms 编码 → 立即发出
  2. 客户端收到新帧,但 jitter buffer 里还有 2-3 秒的旧帧等播放
  3. 新帧排到队尾,要等 2-3 秒才显示
  4. 用户感知:操作后 2-3 秒才看到反馈

验证证据(test4 日志)

1. 帧发送间隔呈"哑铃型"分布

T12:58:36.442 → 36.910 (+468ms)
T12:58:36.930 → 37.027 (+97ms)
T12:58:37.626 → 38.227 (+599ms)   ← 进入 600ms 周期(静态)
T12:58:38.825 → 39.425 (+600ms)
T12:58:40.025 → 40.624 (+600ms)
T12:58:41.225 → 41.824 (+599ms)
T12:58:41.915 → 41.932 (+17ms)    ← 用户操作,burst 开始
T12:58:42.426 → 43.026 (+600ms)   ← 又回到 600ms 周期

每 600ms 一帧 = 1.6 fps(光标闪烁或 widget 刷新触发的损伤)。

2. 服务端处理无延迟

指标 实测 评价
total_p95(编码总耗时) 5-9ms 极快
send_wait_p95 0.0ms 无发送等待
UDP WouldBlock 0 次 内核 buffer 没满
enc_gap_max 1.0-1.4s 编码线程等输入(正常)
frame_age_p95 0.0ms ⚠️ 统计本身坏了(见 #20)

3. wlr-screencopy 路径不受影响

state.rs:606-607 已经正确使用真实时间。所以这个 bug 是 Portal 路径特有

修复方案

主修复:用 PipeWire 真实 PTS

src/state_portal.rs:455 修改:

// 旧:
let pts = self.frames_encoded as i64;

// 新:
// PipeWire 通过 SPA_META_Header 提供纳秒级真实捕获时间戳。
// 转换为 encoder time_base 单位(1/fps 秒)。
// 顺序计数器在 damage-driven 可变帧率下让客户端 jitter buffer 误判,
// 累积到 2-3 秒。参考 wlr-screencopy 路径(state.rs:606)。
let pts = if frame.pts > 0 {
    (frame.pts * self.args.fps as i64) / 1_000_000_000
} else {
    self.frames_encoded as i64  // 后备:PipeWire 未提供 PTS 时
};

PipeWire PTS 已经在 cap_portal.rs:46-47, 688-709 提取并放在 PwDmaBufFrame.pts 字段里,只是被 state_portal 忽略了

配套修复:打通 frame_age 统计(部分覆盖 #20)

src/stats.rs:151-167record_send 已经支持 capture_time 参数,但 WebRTC 路径的 record_send_from_thread 没有传递。把 frame.pts 转换为 Instant,通过 webrtc 线程传给 stats,可以让 frame_age_p95 反映真实延迟。

不在本次修复范围

  • wlr-screencopy 路径已经正确,不动
  • MP4 文件输出路径(create_software_h264_muxer)PTS 用顺序计数器是合理的(文件不需要实时播放)

复现

wl-webrtc --port 56666 -v --stats > /tmp/test.log 2>&1
# 浏览器连接 WebRTC viewer
# 让画面保持静态 30 秒(KWin 损伤驱动,光标闪烁触发 ~1.6 fps)
# 然后大幅移动鼠标
# 期望:客户端在 2-3 秒后才显示鼠标动作

辅助验证:

  • 打开 chrome://webrtc-internalsjitterBufferDelayjitterBufferEmittedCount
  • 期望看到 jitter buffer 持续在 2000-3000ms

关联

  • #19 (CLOSED):启动空转修复 —— 无关
  • #23 (CLOSED):PLI 风暴 + 码率 + VBV —— 修复了 PLI 风暴的延迟尖峰,但 PTS 错位让客户端 buffer 持续累积
  • #15 (CLOSED):stall 假报警 —— 无关
  • #18 (CLOSED):filler 浪费 —— 无关
  • #20 (OPEN):encoded_fps 准确性 —— frame_age 修复顺带覆盖部分

严重度

Critical —— 之前以为 #23 解决了延迟,但用户实测发现仍有 2-3 秒"操作后延迟"。这是产品可用性问题,影响所有 Portal 路径(KWin/KDE/GNOME)用户。

## 现象 在 #19、#23、#15、#18 全部修复后,WebRTC 客户端仍有 **2-3 秒"操作后延迟"**: - 用户移动鼠标 / 操作画面 - 服务端 5-10ms 内编码完成并发出 - 但客户端**显示这个帧要等 2-3 秒** 测试环境: - 浏览器 WebRTC 客户端(Chrome/Edge/Firefox) - 局域网(无网络延迟/丢包) - 服务端 `cargo build --release` 最新版(test4 日志) ## 触发位置 **`src/state_portal.rs:455`** (Portal/PipeWire 路径): ```rust let pts = self.frames_encoded as i64; // ← 顺序计数器,0/1/2/3... ``` 对比 **`src/state.rs:606-607`** (wlr-screencopy 路径,**正确的实现**): ```rust let pts = (tv_sec as i64) * fps + (tv_usec as i64) * fps / 1_000_000; // 用真实捕获时间 ``` ## 根因分析 ### 核心错位:PTS 单位 vs 真实时间 Portal 路径用**帧序号**作为 PTS,假设帧率恒定。但 KWin 在 Wayland 下是**损伤驱动**(damage-driven),帧率从 1.6 fps(静态画面)到 57 fps(活跃画面)剧烈波动。 ### 客户端 jitter buffer 的误判 WebRTC 客户端用 RTP 时间戳(90kHz 时钟,从 PTS 转换)来判断: - 帧应该何时显示 - 网络抖动有多少 - jitter buffer 应该多大 静态期间(每 600ms 一帧): | 真实时间 | 第 N 帧 | 当前 PTS | RTP 时间戳(90kHz) | 客户端推算"应显示于" | 实际到达 | 客户端算出的"网络抖动" | |---|---|---|---|---|---|---| | T=0 | 帧 0 | 0 | 0 | 0ms | T=0 | 0 | | T=600ms | 帧 1 | 1 | 3000 | **33ms** | T=600ms | **+567ms** | | T=1200ms | 帧 2 | 2 | 6000 | **66ms** | T=1200ms | **+1134ms** | | T=5000ms | 帧 8 | 8 | 24000 | **266ms** | T=5000ms | **+4734ms** | 客户端算法推断:"帧到达间隔 600ms,但 RTP 时间戳说应该 33ms 间隔,所以网络抖动 567ms,**扩大 jitter buffer** 到 567ms"。 几个周期后,jitter buffer **累积到 2-3 秒,且不自动缩小**(浏览器保守策略)。 ### 为什么用户感觉"操作后延迟" 1. 用户移动鼠标 → KWin 立即送新帧 → 服务端 5-10ms 编码 → 立即发出 2. 客户端收到新帧,但 jitter buffer 里**还有 2-3 秒的旧帧**等播放 3. 新帧排到队尾,要等 2-3 秒才显示 4. 用户感知:操作后 2-3 秒才看到反馈 ## 验证证据(test4 日志) ### 1. 帧发送间隔呈"哑铃型"分布 ``` T12:58:36.442 → 36.910 (+468ms) T12:58:36.930 → 37.027 (+97ms) T12:58:37.626 → 38.227 (+599ms) ← 进入 600ms 周期(静态) T12:58:38.825 → 39.425 (+600ms) T12:58:40.025 → 40.624 (+600ms) T12:58:41.225 → 41.824 (+599ms) T12:58:41.915 → 41.932 (+17ms) ← 用户操作,burst 开始 T12:58:42.426 → 43.026 (+600ms) ← 又回到 600ms 周期 ``` 每 600ms 一帧 = 1.6 fps(光标闪烁或 widget 刷新触发的损伤)。 ### 2. 服务端处理无延迟 | 指标 | 实测 | 评价 | |---|---|---| | `total_p95`(编码总耗时) | 5-9ms | 极快 | | `send_wait_p95` | 0.0ms | 无发送等待 | | UDP WouldBlock | 0 次 | 内核 buffer 没满 | | `enc_gap_max` | 1.0-1.4s | 编码线程等输入(正常) | | `frame_age_p95` | 0.0ms | ⚠️ **统计本身坏了**(见 #20) | ### 3. wlr-screencopy 路径不受影响 `state.rs:606-607` 已经正确使用真实时间。所以这个 bug 是 **Portal 路径特有**。 ## 修复方案 ### 主修复:用 PipeWire 真实 PTS `src/state_portal.rs:455` 修改: ```rust // 旧: let pts = self.frames_encoded as i64; // 新: // PipeWire 通过 SPA_META_Header 提供纳秒级真实捕获时间戳。 // 转换为 encoder time_base 单位(1/fps 秒)。 // 顺序计数器在 damage-driven 可变帧率下让客户端 jitter buffer 误判, // 累积到 2-3 秒。参考 wlr-screencopy 路径(state.rs:606)。 let pts = if frame.pts > 0 { (frame.pts * self.args.fps as i64) / 1_000_000_000 } else { self.frames_encoded as i64 // 后备:PipeWire 未提供 PTS 时 }; ``` PipeWire PTS 已经在 `cap_portal.rs:46-47, 688-709` 提取并放在 `PwDmaBufFrame.pts` 字段里,**只是被 state_portal 忽略了**。 ### 配套修复:打通 frame_age 统计(部分覆盖 #20) `src/stats.rs:151-167` 的 `record_send` 已经支持 `capture_time` 参数,但 WebRTC 路径的 `record_send_from_thread` 没有传递。把 `frame.pts` 转换为 `Instant`,通过 webrtc 线程传给 stats,可以让 `frame_age_p95` 反映真实延迟。 ### 不在本次修复范围 - wlr-screencopy 路径已经正确,不动 - MP4 文件输出路径(`create_software_h264_muxer`)PTS 用顺序计数器是合理的(文件不需要实时播放) ## 复现 ```bash wl-webrtc --port 56666 -v --stats > /tmp/test.log 2>&1 # 浏览器连接 WebRTC viewer # 让画面保持静态 30 秒(KWin 损伤驱动,光标闪烁触发 ~1.6 fps) # 然后大幅移动鼠标 # 期望:客户端在 2-3 秒后才显示鼠标动作 ``` 辅助验证: - 打开 `chrome://webrtc-internals` 看 `jitterBufferDelay` 和 `jitterBufferEmittedCount` - 期望看到 jitter buffer 持续在 2000-3000ms ## 关联 - **#19 (CLOSED)**:启动空转修复 —— 无关 - **#23 (CLOSED)**:PLI 风暴 + 码率 + VBV —— 修复了 PLI 风暴的**延迟尖峰**,但 PTS 错位让客户端 buffer 持续累积 - **#15 (CLOSED)**:stall 假报警 —— 无关 - **#18 (CLOSED)**:filler 浪费 —— 无关 - **#20 (OPEN)**:encoded_fps 准确性 —— frame_age 修复顺带覆盖部分 ## 严重度 **Critical** —— 之前以为 #23 解决了延迟,但用户实测发现仍有 2-3 秒"操作后延迟"。这是产品可用性问题,影响所有 Portal 路径(KWin/KDE/GNOME)用户。
dailz added the area/webrtcpriority/criticalarea/pipelinetype/bug labels 2026-06-20 21:27:30 +08:00
dailz closed this issue 2026-06-20 21:53:41 +08:00
Sign in to join this conversation.