diff --git a/agent/src/capture.rs b/agent/src/capture.rs index 48c2858..d6e141e 100644 --- a/agent/src/capture.rs +++ b/agent/src/capture.rs @@ -201,7 +201,8 @@ impl NvencEncoder { // CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps) config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR; config.preset_cfg.rc_params.average_bit_rate = bitrate_bps; - config.preset_cfg.gop_len = 0xffff_ffff; // 无周期 IDR,仅按需(KeyFrameReq) + config.preset_cfg.gop_len = 240; // 周期 IDR(60fps 下 4s):网络异常后的自愈恢复点, + // 配合 KeyFrameReq 即时恢复,避免长时间冻结 config.preset_cfg.frame_interval_p = 1; // 无 B 帧 let init = InitParams { diff --git a/viewer/src/main.rs b/viewer/src/main.rs index 1ef5d90..2a1103e 100644 --- a/viewer/src/main.rs +++ b/viewer/src/main.rs @@ -241,13 +241,20 @@ fn session( let (w, h) = (width as usize, height as usize); let stats = Arc::new(Stats::default()); - // 读任务(流 1):H.264 解码 → RGB → mpsc;同时统计端到端延迟与流量 - let (frame_tx, frame_rx) = std::sync::mpsc::channel::>(); + // 读任务(流 1):H.264 解码 → RGB → 单槽 latest-wins;同时统计端到端延迟与流量 + // 帧格式 (src_w, src_h, rgb);(0,0,_) = 解码失败占位,触发 KeyFrameReq + // 单槽而非无界队列:渲染跟不上 60fps 到达率时旧帧直接被覆盖,绝不积压 + //(无界 mpsc 在 NVENC 60fps 下会无限积压 → 延迟线性增长 + 渲染 drain 时 + // 长时间阻塞窗口消息泵 → 窗口假死、HUD 停刷新) + let latest: Arc)>>> = + Arc::new(std::sync::Mutex::new(None)); { let stats = stats.clone(); + let latest = latest.clone(); let mut decoder = openh264::decoder::Decoder::new() .map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?; tokio::spawn(async move { + let mut size_logged = false; loop { match common::read_video_frame(&mut recv).await { Ok((hdr, payload)) => { @@ -275,10 +282,19 @@ fn session( match decoder.decode(&payload) { Ok(Some(yuv)) => { let (dw, dh) = yuv.dimensions(); - let mut rgb = vec![0u8; dw * dh * 3]; + let (dw, dh) = (dw as u32, dh as u32); + if !size_logged { + size_logged = true; + eprintln!( + "[viewer] decoded frame {dw}x{dh} (window {}x{})", + hdr.width, hdr.height + ); + } + let mut rgb = vec![0u8; dw as usize * dh as usize * 3]; yuv.write_rgb8(&mut rgb); - if frame_tx.send(rgb).is_err() { - break; + // latest-wins:直接覆盖旧帧,无积压 + if let Ok(mut slot) = latest.lock() { + *slot = Some((dw, dh, rgb)); } } Ok(None) => {} @@ -286,7 +302,9 @@ fn session( // 解码异常:请求被控端强制关键帧 stats.dec_fail.fetch_add(1, Ordering::Relaxed); eprintln!("[viewer] decode failed: {e}, request keyframe"); - let _ = frame_tx.send(Vec::new()); // 空帧占位唤醒渲染循环 + if let Ok(mut slot) = latest.lock() { + *slot = Some((0, 0, Vec::new())); + } } } } @@ -379,18 +397,15 @@ fn session( push(&msg_tx, ControlMsg::Ping(now_ms())); } - // 1) 渲染最新帧(只保留最新,丢弃积压);空帧=解码异常,请求关键帧 - loop { - match frame_rx.try_recv() { - Ok(raw) => { - if raw.is_empty() { - push(&msg_tx, ControlMsg::KeyFrameReq); - } else if raw.len() == w * h * 3 { - blit(&mut buffer, &raw); - dirty = true; - } + // 1) 渲染最新帧(单槽 latest-wins:每迭代只取最新一帧,天然防积压) + if let Ok(mut slot) = latest.lock() { + if let Some((sw, sh, raw)) = slot.take() { + if raw.is_empty() { + push(&msg_tx, ControlMsg::KeyFrameReq); + } else if sw > 0 && sh > 0 { + blit(&mut buffer, w, h, &raw, sw as usize, sh as usize); + dirty = true; } - Err(_) => break, } } @@ -675,9 +690,17 @@ fn draw_hud(buffer: &mut [u32], w: usize, lines: &[(&str, &str, u32)]) { } } -/// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲 -fn blit(buffer: &mut [u32], raw: &[u8]) { - for (px, chunk) in buffer.iter_mut().zip(raw.chunks_exact(3)) { - *px = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32; +/// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲。 +/// 支持源尺寸与窗口不同(解码器可能对齐到 16 倍数,如 1080→1088): +/// 逐行拷贝重叠区域,源小于窗口时右侧/下方留黑。 +fn blit(buffer: &mut [u32], w: usize, h: usize, raw: &[u8], dw: usize, dh: usize) { + let cw = w.min(dw); + let ch = h.min(dh); + for row in 0..ch { + let src = &raw[row * dw * 3..row * dw * 3 + cw * 3]; + let dst = &mut buffer[row * w..row * w + cw]; + for (px, chunk) in dst.iter_mut().zip(src.chunks_exact(3)) { + *px = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32; + } } }