From 1837658c8512b5a20c596b2a9301762d7a40d3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Thu, 29 Oct 2026 16:13:58 +0800 Subject: [PATCH] =?UTF-8?q?test(agent):=20dump=5Fstream=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20--save=EF=BC=8C=E5=8F=AF=E5=AF=BC=E5=87=BA=E8=A3=B8?= =?UTF-8?q?=20H.264=20=E7=A0=81=E6=B5=81=E4=BE=9B=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E5=99=A8=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目的 安卓主控方案的唯一高风险未知项是「agent 发出的码流能否被 MediaCodec 解码」。 要验证它,需要一份脱离 QUIC/自定义帧头的**裸 Annex-B 码流文件**,供手机端 或任何标准解码器直接读取。 改动 - 新增 --save <路径>:把各帧载荷按到达顺序拼接成 Annex-B 文件 - 存的是**载荷**而非完整帧:外部解码器只认标准 Annex-B,不认识本项目的 长度前缀与 bincode 帧头,故不能存 encode_video_frame 的输出 - 保存时打印字节数与 IDR 数量,便于快速判断样本是否可用 不做的事 不引入独立的码流解析库,IDR 统计复用文件内已有的起始码扫描逻辑, 避免为一次性诊断工具增加依赖。 验证 cargo build --workspace 退出码 0;cargo test --workspace --exclude linux-agent 全绿。 实际使用:导出 90 帧、437823 字节、含 2 个 IDR,该文件已用于安卓 MediaCodec 兼容性验证(vivo S6 / Exynos 硬解,87/89 帧解出)。 --- agent/src/bin/dump_stream.rs | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/agent/src/bin/dump_stream.rs b/agent/src/bin/dump_stream.rs index 9d61853..0101a0b 100644 --- a/agent/src/bin/dump_stream.rs +++ b/agent/src/bin/dump_stream.rs @@ -130,6 +130,10 @@ async fn main() -> Result<()> { .map(|s| s.parse()) .transpose()? .unwrap_or(30); + // --save <路径>:把**裸 H.264 码流**(各帧载荷按到达顺序直接拼接) + // 写成 Annex-B 文件,供安卓 MediaCodec 等外部解码器验证兼容性。 + // 注意存的是载荷而非完整帧(不含自定义帧头),因为外部解码器只认标准 Annex-B。 + let save_path = arg("--save"); let tls = rustls::ClientConfig::builder() .dangerous() @@ -177,6 +181,8 @@ async fn main() -> Result<()> { let mut decoder = Decoder::new()?; let mut sent_kfreq = false; + // 累积裸码流,用于 --save + let mut saved: Vec = Vec::new(); println!("\n序号 seq kf 载荷字节 NAL 结构 解码结果"); println!("{}", "-".repeat(72)); @@ -198,6 +204,11 @@ async fn main() -> Result<()> { let types = nal_types(&payload); let is_idr = types.contains(&5); + // 需要在 save 里也保留这一帧时,先记录到缓冲 + if save_path.is_some() { + saved.extend_from_slice(&payload); + } + // 交叉校验:帧头 keyframe 标志必须与码流实际 NAL 类型一致。 // 这条不一致意味着时间戳/序号契约或编码器标志有 bug,比画面花屏更难排查。 if hdr.keyframe != is_idr { @@ -267,6 +278,39 @@ async fn main() -> Result<()> { } } + // 导出裸码流文件(Annex-B 拼接),供外部解码器验证 + if let Some(p) = save_path.as_ref() { + std::fs::write(p, &saved)?; + let idr_cnt = { + // 粗略统计 IDR 数量:扫起始码后的 NAL 类型 + let mut n = 0; + let mut i = 0usize; + while i + 3 < saved.len() { + let sc = if saved[i..].starts_with(&[0, 0, 0, 1]) { + 4 + } else if saved[i..].starts_with(&[0, 0, 1]) { + 3 + } else { + i += 1; + continue; + }; + match saved.get(i + sc) { + Some(&b) if b & 0x1F == 5 => { + n += 1; + i += sc + 1; + } + Some(_) => i += sc + 1, + None => break, + } + } + n + }; + println!( + "\n[dump] 已保存裸码流: {p}({} 字节,含 {idr_cnt} 个 IDR)", + saved.len() + ); + } + let _ = conn.close(0u32.into(), b"done"); Ok(()) }