//! viewer-core 的桌面 headless 验证工具(阶段 1 验收用,不依赖 Android)。 //! //! 证明三件事,逐条打印: //! 1. viewer-core 能完成 QUIC 连接 + 协议 v2 认证握手; //! 2. 帧流按契约交付(seq 单调递增、keyframe 标志与码流实际 NAL 一致); //! 3. 收到的码流能被 openh264 解出画面(即:Android 侧 MediaCodec 将拿到同样 //! 合法的 Annex-B——解码器不同,但码流合法性在这里就证明了)。 //! //! 用法: //! 终端1: target\release\agent.exe --port 47900 --code t //! 终端2: cargo run -p viewer-core --bin headless_check -- --port 47900 --code t --frames 60 use anyhow::Result; use std::time::Duration; use viewer_core::{ConnectConfig, Session}; fn arg(name: &str) -> Option { let a: Vec = std::env::args().collect(); a.iter() .position(|x| x == name) .and_then(|i| a.get(i + 1)) .cloned() } /// 与 dump_stream 同款:扫 Annex-B 起始码拿 NAL 类型,交叉校验 keyframe 标志 fn has_idr(payload: &[u8]) -> bool { let mut i = 0usize; while i + 3 < payload.len() { let sc = if payload[i..].starts_with(&[0, 0, 0, 1]) { 4 } else if payload[i..].starts_with(&[0, 0, 1]) { 3 } else { i += 1; continue; }; match payload.get(i + sc) { Some(&b) if b & 0x1F == 5 => return true, Some(_) => i += sc + 1, None => break, } } false } #[tokio::main] async fn main() -> Result<()> { let port: u16 = arg("--port") .map(|s| s.parse()) .transpose()? .unwrap_or(47800); let code = arg("--code").unwrap_or_else(|| "123456".into()); // --host:默认 127.0.0.1;填局域网 IP 可顺带验证防火墙入站路径 let host = arg("--host").unwrap_or_else(|| "127.0.0.1".into()); let frames_wanted: u64 = arg("--frames") .map(|s| s.parse()) .transpose()? .unwrap_or(60); let mut session = Session::connect(ConnectConfig::direct(&host, port, code)).await?; println!("[check] 连接成功 host={host} peer={}", session.peer_addr()); let mut decoder = openh264::decoder::Decoder::new()?; let mut last_seq: Option = None; let mut got = 0u64; let mut seq_violations = 0u64; let mut kf_mismatches = 0u64; let mut decoded = 0u64; let mut decode_failed_before_idr = 0u64; let mut awaiting_idr = true; // 与 Android 侧相同的状态机 let mut rtt_shown = false; let deadline = std::time::Instant::now() + Duration::from_secs(30); while got < frames_wanted && std::time::Instant::now() < deadline { let Some(f) = tokio::time::timeout(Duration::from_secs(10), session.next_frame()) .await .ok() .flatten() else { break; }; got += 1; // 契约 2a:seq 单调递增 if let Some(prev) = last_seq { if f.seq <= prev { seq_violations += 1; println!("[check] ⚠️ seq 回退: {prev} -> {}", f.seq); } } last_seq = Some(f.seq); // 契约 2b:keyframe 标志与码流一致 let real_kf = has_idr(&f.payload); if f.keyframe != real_kf { kf_mismatches += 1; println!( "[check] ⚠️ 帧头 keyframe={} 但码流实际 IDR={}(seq={})", f.keyframe, real_kf, f.seq ); } // 契约 3:与 Android 相同的状态机下解码必须成功 if awaiting_idr && !f.keyframe { continue; // 等 IDR,P 帧不喂(这是 R-3 教训的实现) } if f.keyframe { awaiting_idr = false; } match decoder.decode(&f.payload) { Ok(_) => decoded += 1, Err(e) => { decode_failed_before_idr += 1; println!("[check] ❌ seq={} 解码失败: {e}", f.seq); awaiting_idr = true; session.request_keyframe(); } } // RTT 出现即记录一次 if !rtt_shown { let s = session.stats(); if s.rtt_ms > 0 { println!("[check] RTT={}ms frames_in={} bytes_in={}", s.rtt_ms, s.frames_in, s.bytes_in); rtt_shown = true; } } } let s = session.stats(); println!("\n========== 阶段 1 headless 验收 =========="); println!("收到帧: {got}(目标 {frames_wanted})"); println!("seq 违例: {seq_violations}"); println!("keyframe 标志不符: {kf_mismatches}"); println!("解码成功: {decoded} / 解码失败: {decode_failed_before_idr}"); println!("统计: rtt={}ms frames_in={} bytes_in={}", s.rtt_ms, s.frames_in, s.bytes_in); let ok = got >= frames_wanted.min(30) && seq_violations == 0 && kf_mismatches == 0 && decoded > 0 && decode_failed_before_idr == 0; println!( "结论: {}", if ok { "✅ viewer-core 收帧链路通过" } else { "❌ 存在未满足项" } ); session.close().await; Ok(()) }