From 8dd18a06a1a6054f9fe904043d5d7a76e359655c Mon Sep 17 00:00:00 2001 From: dev-team Date: Fri, 23 Oct 2026 13:27:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20E2E=20=E7=A7=AF=E5=8E=8B=20-=20?= =?UTF-8?q?=E9=99=88=E6=97=A7=E5=B8=A7=E8=B7=B3=E8=BF=87=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E6=8E=92=E7=A9=BA=20QUIC=20=E9=98=9F=E5=88=97=20+=20rgb=20?= =?UTF-8?q?=E7=BC=93=E5=86=B2=E5=A4=8D=E7=94=A8=20+=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E5=8D=87=20v0.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- viewer/src/main.rs | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/viewer/src/main.rs b/viewer/src/main.rs index 48873a0..19eaac8 100644 --- a/viewer/src/main.rs +++ b/viewer/src/main.rs @@ -253,6 +253,9 @@ fn session( let latest = latest.clone(); let mut decoder = openh264::decoder::Decoder::new() .map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?; + let mut rgb_buf: Vec = Vec::new(); + let mut last_kfreq = Instant::now(); + let mut skip_stale = 0u64; tokio::spawn(async move { let mut size_logged = false; loop { @@ -262,11 +265,16 @@ fn session( stats.frames.fetch_add(1, Ordering::Relaxed); // 端到端延迟 = 接收时刻 - 时钟偏移 - 采集时刻(EMA 平滑) let mut est = None; + let mut stale = false; if stats.offset_valid.load(Ordering::Relaxed) { let e = now_ms() as i64 - stats.offset.load(Ordering::Relaxed) - hdr.ts_ms as i64; if (0..5000).contains(&e) { + // 积压保护:QUIC 视频流可靠有序,积压必须逐帧读出, + // 但陈旧帧(>250ms)可以不解码——跳过解码让积压以 + // 网络速度排空,否则消化速率≈到达速率永远追不回来 + stale = e > 250; let old = stats.e2e.load(Ordering::Relaxed) as i64; let sm = if stats.e2e_valid.load(Ordering::Relaxed) { (old * 4 + e) / 5 @@ -279,6 +287,22 @@ fn session( } } stats.on_frame_arrival(est); + if stale { + skip_stale += 1; + if skip_stale % 120 == 1 { + eprintln!( + "[viewer] backlog: stale frame skipped (e2e>250ms, total {skip_stale})" + ); + } + // 限频请求关键帧:积压排空后从 IDR 重新衔接画面 + 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; + } match decoder.decode(&payload) { Ok(Some(yuv)) => { let (dw, dh) = yuv.dimensions(); @@ -290,11 +314,12 @@ fn session( hdr.width, hdr.height ); } - let mut rgb = vec![0u8; dw as usize * dh as usize * 3]; - yuv.write_rgb8(&mut rgb); + rgb_buf.clear(); + rgb_buf.resize(dw as usize * dh as usize * 3, 0); + yuv.write_rgb8(&mut rgb_buf); // latest-wins:直接覆盖旧帧,无积压 if let Ok(mut slot) = latest.lock() { - *slot = Some((dw, dh, rgb)); + *slot = Some((dw, dh, rgb_buf.clone())); } } Ok(None) => {} @@ -340,7 +365,10 @@ fn session( stats.offset_valid.store(true, Ordering::Relaxed); } Ok(_) => {} - Err(_) => break, + Err(e) => { + eprintln!("[viewer] control stream closed: {e} (RTT/LOSS 将停止更新)"); + break; + } } } }); @@ -364,7 +392,7 @@ fn session( push(&msg_tx, ControlMsg::KeyFrameReq); let mut window = Window::new( - "mvp-remote v0.7-LowLatency (Esc 退出)", + "mvp-remote v0.8-CatchUp (Esc 退出)", w, h, WindowOptions { From ad022ffcf4b6c45f8bf16502f6dabae6cde55836 Mon Sep 17 00:00:00 2001 From: dev-team Date: Fri, 23 Oct 2026 13:36:21 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20v0.8.1=20=E9=9D=99=E6=AD=A2=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E9=BB=91=E5=B1=8F=20-=20=E7=A7=AF=E5=8E=8B=E8=B7=B3?= =?UTF-8?q?=E5=B8=A7=E9=99=90=E5=AE=9A=E7=AA=81=E5=8F=91=E5=88=B0=E8=BE=BE?= =?UTF-8?q?,=20=E7=A9=BA=E9=97=B2=E9=87=8D=E5=8F=91=E5=B8=A7=E4=B8=8D?= =?UTF-8?q?=E5=88=A4=E9=99=88=E6=97=A7=E4=B8=8D=E8=AE=A1E2E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- viewer/src/main.rs | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/viewer/src/main.rs b/viewer/src/main.rs index 19eaac8..8521538 100644 --- a/viewer/src/main.rs +++ b/viewer/src/main.rs @@ -256,6 +256,7 @@ fn session( let mut rgb_buf: Vec = Vec::new(); let mut last_kfreq = Instant::now(); let mut skip_stale = 0u64; + let mut last_arr: Option = None; tokio::spawn(async move { let mut size_logged = false; loop { @@ -263,6 +264,12 @@ fn session( Ok((hdr, payload)) => { stats.bytes.fetch_add(payload.len() as u64, Ordering::Relaxed); stats.frames.fetch_add(1, Ordering::Relaxed); + // 与上一帧的到达间隔:≤100ms 才算"突发到达"。 + // 桌面静止时 agent 以 2Hz 空闲重发,帧龄天然 ≥500ms, + // 不能按陈旧帧丢弃——否则静止桌面所有帧被跳过 → + // 永久黑屏 + KeyFrameReq/IDR 死循环(v0.8 实测教训) + let burst = matches!(last_arr, Some(t) if t.elapsed() <= Duration::from_millis(100)); + last_arr = Some(Instant::now()); // 端到端延迟 = 接收时刻 - 时钟偏移 - 采集时刻(EMA 平滑) let mut est = None; let mut stale = false; @@ -271,27 +278,30 @@ fn session( now_ms() as i64 - stats.offset.load(Ordering::Relaxed) - hdr.ts_ms as i64; if (0..5000).contains(&e) { - // 积压保护:QUIC 视频流可靠有序,积压必须逐帧读出, - // 但陈旧帧(>250ms)可以不解码——跳过解码让积压以 - // 网络速度排空,否则消化速率≈到达速率永远追不回来 - stale = e > 250; - let old = stats.e2e.load(Ordering::Relaxed) as i64; - let sm = if stats.e2e_valid.load(Ordering::Relaxed) { - (old * 4 + e) / 5 - } else { - e - }; - stats.e2e.store(sm as u64, Ordering::Relaxed); - stats.e2e_valid.store(true, Ordering::Relaxed); - est = Some(sm); + // 积压判定必须同时满足:突发到达(背靠背)+ 帧龄>250ms。 + // QUIC 可靠有序,积压须逐帧读出但可不解码——跳过解码 + // 让队列以网络速度排空 + stale = burst && e > 250; + // 空闲重发帧的帧龄不代表显示延迟,不计入 EMA + if burst || e < 250 { + let old = stats.e2e.load(Ordering::Relaxed) as i64; + let sm = if stats.e2e_valid.load(Ordering::Relaxed) { + (old * 4 + e) / 5 + } else { + e + }; + stats.e2e.store(sm as u64, Ordering::Relaxed); + stats.e2e_valid.store(true, Ordering::Relaxed); + est = Some(sm); + } } } stats.on_frame_arrival(est); if stale { skip_stale += 1; - if skip_stale % 120 == 1 { + if skip_stale % 60 == 1 { eprintln!( - "[viewer] backlog: stale frame skipped (e2e>250ms, total {skip_stale})" + "[viewer] backlog: stale burst frame skipped (total {skip_stale})" ); } // 限频请求关键帧:积压排空后从 IDR 重新衔接画面 @@ -392,7 +402,7 @@ fn session( push(&msg_tx, ControlMsg::KeyFrameReq); let mut window = Window::new( - "mvp-remote v0.8-CatchUp (Esc 退出)", + "mvp-remote v0.8.1-CatchUp (Esc 退出)", w, h, WindowOptions {