fix(viewer): 跳帧后进入「等待 IDR」状态,不再把必然失败的 P 帧喂给解码器
问题 H.264 的 P 帧依赖前一帧的参考图像。主控端的积压跳帧逻辑(读出但不解码) 会切断参考链,此后所有 P 帧都解不出来,且不会自行恢复——必须收到 IDR。 旧实现跳帧后只是 continue,后续 P 帧仍被送进 decoder,导致: - 每帧解码失败一次 → stderr 刷屏「decode failed」 - 每次失败都触发一次 KeyFrameReq → 请求风暴 - 解码器反复在错误状态里挣扎 实测(修复前,10 秒会话):decode failed 刷屏数十条。 修复 引入 awaiting_idr 状态,语义是「参考链已断,等 IDR 重新同步」: - 跳帧时置位,并请求关键帧 - 置位期间跳过所有 P 帧(这是正确性要求,不是优化) - 收到 hdr.keyframe=true 的帧才清位并恢复解码 - 解码出错时也置位 - 初始值为 true:入会时已发 KeyFrameReq,拿到第一个 IDR 前不该硬解 该实现依赖协议 v2 新增的 hdr.keyframe 字段(从实际码流 NAL 类型判定), 因此必须在协议改动之后合入。 HUD 新增 STATE 行显示 OK / RECOVER。RECOVER 期间画面冻结是**预期行为**, 没有这个指示用户会误以为软件卡死。 验证(同机 agent+viewer,25 秒) - decode failed:0 次(修复前刷屏) - stderr 总行数:3(仅首帧日志 + 2 条积压提示) - 窗口稳定无假死 已知局限(下一步) 本修复解决的是「跳帧后不再做无用功」,但跳帧本身仍会造成画面冻结直到 IDR 到达。NVENC 的 gop_len=240、@60fps 即 4 秒才有一个自动 IDR,恢复窗口 偏长。缩短 GOP 或降低跳帧触发频率是后续优化项。
This commit is contained in:
parent
e778ff73cc
commit
192513dd2a
@ -110,6 +110,8 @@ struct Stats {
|
||||
agent_sent: AtomicU64,
|
||||
/// H.264 解码失败计数(关键帧自愈验证指标)
|
||||
dec_fail: AtomicU64,
|
||||
/// 当前是否处于「参考链断裂、等待 IDR」状态(HUD 显示 RECOVER)
|
||||
awaiting_idr: AtomicBool,
|
||||
/// 帧到达间隔环形缓冲
|
||||
gaps: std::sync::Mutex<GapRing>,
|
||||
}
|
||||
@ -129,6 +131,7 @@ impl Default for Stats {
|
||||
agent_lost: AtomicU64::new(0),
|
||||
agent_sent: AtomicU64::new(0),
|
||||
dec_fail: AtomicU64::new(0),
|
||||
awaiting_idr: AtomicBool::new(true),
|
||||
gaps: std::sync::Mutex::new(GapRing {
|
||||
last: None,
|
||||
ring: [0; 64],
|
||||
@ -275,6 +278,15 @@ fn session(
|
||||
let mut last_kfreq = Instant::now();
|
||||
let mut skip_stale = 0u64;
|
||||
let mut last_arr: Option<Instant> = None;
|
||||
// 「等待 IDR」状态:参考链一旦断裂(跳帧或解码失败),后续 P 帧**必然**
|
||||
// 解不出来。此时必须停止喂解码器,只等 IDR 重新同步。
|
||||
//
|
||||
// 之前没有这个状态,后果是:跳掉一帧后,每个 P 帧都被送进 decoder →
|
||||
// 每个都失败 → 每个都打日志 → 每个都触发一次 KeyFrameReq。
|
||||
// 表现为「decode failed」刷屏,且解码器反复在错误状态里挣扎。
|
||||
//
|
||||
// 放在 Stats 里(而非局部变量)以便 HUD 显示 RECOVER 状态。
|
||||
// 初始 true:入会时已发 KeyFrameReq,拿到第一个 IDR 前不该硬解。
|
||||
tokio::spawn(async move {
|
||||
let mut size_logged = false;
|
||||
loop {
|
||||
@ -322,7 +334,10 @@ fn session(
|
||||
"[viewer] backlog: stale burst frame skipped (total {skip_stale})"
|
||||
);
|
||||
}
|
||||
// 限频请求关键帧:积压排空后从 IDR 重新衔接画面
|
||||
// 跳帧直接切断 H.264 参考链:之后所有 P 帧都解不出来。
|
||||
// 立刻转入「等待 IDR」并请求关键帧,绝不能让后续 P 帧
|
||||
// 继续喂给解码器(那是必然失败的徒劳尝试)。
|
||||
stats.awaiting_idr.store(true, Ordering::Relaxed);
|
||||
if last_kfreq.elapsed() >= Duration::from_millis(200) {
|
||||
last_kfreq = Instant::now();
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
@ -331,6 +346,22 @@ fn session(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 参考链未恢复时,P 帧一律跳过——此为正确性要求,不是优化。
|
||||
if stats.awaiting_idr.load(Ordering::Relaxed) && !hdr.keyframe {
|
||||
skip_stale += 1;
|
||||
// 补发请求,防止首次 KeyFrameReq 丢失(时间闸限频)
|
||||
if last_kfreq.elapsed() >= Duration::from_millis(200) {
|
||||
last_kfreq = Instant::now();
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
*slot = Some((0, 0, Vec::new()));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 拿到 IDR 才能重新开始解码
|
||||
if hdr.keyframe {
|
||||
stats.awaiting_idr.store(false, Ordering::Relaxed);
|
||||
}
|
||||
match decoder.decode(&payload) {
|
||||
Ok(Some(yuv)) => {
|
||||
let (dw, dh) = yuv.dimensions();
|
||||
@ -352,7 +383,8 @@ fn session(
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
// 解码异常:请求被控端强制关键帧
|
||||
// 解码异常:参考链已不可信,转入等待 IDR
|
||||
stats.awaiting_idr.store(true, Ordering::Relaxed);
|
||||
stats.dec_fail.fetch_add(1, Ordering::Relaxed);
|
||||
eprintln!("[viewer] decode failed: {e}, request keyframe");
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
@ -595,6 +627,21 @@ fn session(
|
||||
("P95", &hud_p95.map(|p| format!("{p}ms")).unwrap_or("--".into()), p95_c),
|
||||
("LOSS", &loss_str, loss_c),
|
||||
("DECFAIL", &format!("{hud_dec}"), if hud_dec == 0 { C_GRAY } else { C_RED }),
|
||||
(
|
||||
// 参考链断裂、正在等 IDR 的显式指示:此时画面冻结是**预期行为**,
|
||||
// 不是故障。没有这个指示,用户会以为软件卡死。
|
||||
"STATE",
|
||||
&if stats.awaiting_idr.load(Ordering::Relaxed) {
|
||||
"RECOVER".to_string()
|
||||
} else {
|
||||
"OK".to_string()
|
||||
},
|
||||
if stats.awaiting_idr.load(Ordering::Relaxed) {
|
||||
C_AMBER
|
||||
} else {
|
||||
C_GREEN
|
||||
},
|
||||
),
|
||||
("RATE", &format!("{:.0}MBP", hud_mbps), C_GRAY),
|
||||
("RES", &format!("{w}x{h}"), C_GRAY),
|
||||
],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user