diff --git a/viewer/src/main.rs b/viewer/src/main.rs index d81a04e..a7c6ef8 100644 --- a/viewer/src/main.rs +++ b/viewer/src/main.rs @@ -110,6 +110,8 @@ struct Stats { agent_sent: AtomicU64, /// H.264 解码失败计数(关键帧自愈验证指标) dec_fail: AtomicU64, + /// 当前是否处于「参考链断裂、等待 IDR」状态(HUD 显示 RECOVER) + awaiting_idr: AtomicBool, /// 帧到达间隔环形缓冲 gaps: std::sync::Mutex, } @@ -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 = 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), ],