feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验

为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在
common/src/lib.rs,三端适配在同一提交内完成以保证可编译。

协议版本(PROTO_VERSION=2)
- ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」:
  版本不一致时字段布局可能不同,验证码的解析结果不可信
- 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」

视频帧头(VideoFrameHeader)
- 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool
- keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定,
  不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源
- 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描

不可信输入防护
- read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4]
- 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS)
- 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入

顺带修复(与上述契约直接相关)
- 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头,
  否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约
- agent-core 与 agent 两端的静止重发路径均改用该函数

验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
This commit is contained in:
郑博文 committed 2026-10-28 16:05:41 +08:00
1 parent 703eb64fc0
commit 9e50bc6237
6 files changed
+560 -55

No files matched your search

+96 -17
View File
@@ -4,7 +4,7 @@
//! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。
use anyhow::{anyhow, Result};
use common::{encode_video_frame, VideoFrameHeader};
use common::{encode_video_frame, h264_contains_idr, VideoFrameHeader};
use nvenc::bitstream::BitStream;
use nvenc::encoder::RegisteredResource;
use nvenc::session::{InitParams, Session};
@@ -327,29 +327,41 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
}
};
let mut last_frame: Option<Vec<u8>> = None; // 软编静止重发
// 最近一帧的**裸载荷**与关键帧标志:静止重发时要用它们重建帧头,
// 不能直接重发整帧(那样 seq/ts_ms 会停在上次发送时刻)
let mut last_payload: Option<Vec<u8>> = None;
let mut last_kf = false;
let mut last_sent = Instant::now() - Duration::from_secs(10);
let min_frame = Duration::from_millis(16); // 60fps 上限
let mut scratch: Vec<u8> = Vec::new(); // 软编:行距不连续时的复用缓冲
// 帧序号:每发一帧 +1(含静止桌面的重复帧),供主控端做丢帧检测
let mut seq: u64 = 0;
// 编码耗时统计(每 120 帧输出一次均值,ms)
let mut stat_frames = 0u64;
let mut stat_convert_ms = 0u64;
let mut stat_encode_ms = 0u64;
let mut stat_skipped = 0u64; // 拥塞跳帧计数
// `$kf` 由调用点显式给出:NVENC 路径知道 `force_key`,软编路径同理。
// 不要在这里猜——首帧和 KeyFrameReq 都会产生 IDR,猜错会让解码端错过重同步点。
macro_rules! emit {
($payload:expr) => {{
($payload:expr, $kf:expr) => {{
let header = VideoFrameHeader {
width: dup.width,
height: dup.height,
ts_ms: start.elapsed().as_millis() as u64,
seq,
keyframe: $kf,
};
let frame = encode_video_frame(&header, &$payload);
let payload_vec: Vec<u8> = $payload;
let frame = encode_video_frame(&header, &payload_vec);
if tx.send(Some(frame.clone())).is_err() {
return; // 会话结束
}
last_frame = Some(frame);
last_payload = Some(payload_vec);
last_kf = $kf;
last_sent = Instant::now();
seq = seq.wrapping_add(1);
stat_frames += 1;
}};
}
@@ -358,10 +370,12 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
let iter_start = Instant::now();
match dup.acquire(80) {
Ok(true) => {
// 主控端请求关键帧(解码异常恢复)
let force_key = keyreq.swap(false, Ordering::Relaxed);
// 拥塞反馈:发送侧排队深 → 跳过本帧(NVENC 少输入一帧,参考链连续;
// 解码端收到的帧序 = 编码端实际输出序,无花屏风险)
//
// ⚠️ 必须放在消费 keyreq **之前**:先前顺序是「先 swap 再判断拥塞」,
// 一旦本帧因拥塞被跳过,已消费的 KeyFrameReq 就随 continue 一起丢失,
// 解码端等不到 IDR(与下方静止分支同源的缺陷)。
if congestion.swap(false, Ordering::Relaxed) {
stat_skipped += 1;
let el = iter_start.elapsed();
@@ -370,13 +384,19 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
}
continue;
}
// 主控端请求关键帧(解码异常恢复)。走到这里一定会编码,
// 因此可以安全地消费标志。
let force_key = keyreq.swap(false, Ordering::Relaxed);
match &mut encoder {
EncoderKind::Nvenc(enc) => {
let enc_start = Instant::now();
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
match r {
Ok(payload) => emit!(payload),
Ok(payload) => {
let kf = h264_contains_idr(&payload);
emit!(payload, kf)
}
Err(e) => eprintln!("[capture] nvenc encode failed: {e}"),
}
}
@@ -410,7 +430,11 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
let r = enc.encode(&yuv);
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
match r {
Ok(ebs) => emit!(ebs.to_vec()),
Ok(ebs) => {
let payload = ebs.to_vec();
let kf = h264_contains_idr(&payload);
emit!(payload, kf)
}
Err(e) => {
eprintln!("[capture] encode failed: {e}");
}
@@ -442,25 +466,80 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
}
Ok(false) => {
// 桌面静止:低频重发(丢包后恢复 + 会话保活)。
// 关键:KeyFrameReq 在桌面静止时也必须响应 —— 此前只在新帧路径
// 消费该标志,桌面完全静止时解码端请求 IDR 永远得不到响应 → 黑屏。
//
// ⚠️ 这里曾有三个缺陷,全部围绕「KeyFrameReq 被静默丢弃」:
// 1. `swap` 写在时间闸**外面**:请求若落在距上次发送 500ms 之内,
// 标志已被消费但分支不进入,该次请求直接丢失(手册 §6.1 第 3 条)。
// 2. 软编分支只重发旧码流、不看 force_key:永远吐不出 IDR(手册 §6.1 第 2 条)。
// 3. NVENC 分支虽然把 force_key 传给了编码器,但受第 1 条限制。
//
// 现在的语义:**关键帧请求绕过 500ms 节流**——它是错误恢复路径,
// 延迟一次 IDR 的代价是解码端持续黑屏,远高于多发一帧的带宽成本。
let force_key = keyreq.swap(false, Ordering::Relaxed);
if last_sent.elapsed() > Duration::from_millis(500) {
let due = force_key || last_sent.elapsed() > Duration::from_millis(500);
if due {
match &mut encoder {
EncoderKind::Nvenc(enc) => {
// 重编当前 GPU 帧(内容未变,输出极小;force_key 时为 IDR)
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
match r {
Ok(payload) => emit!(payload),
Ok(payload) => {
let kf = h264_contains_idr(&payload);
emit!(payload, kf)
}
Err(e) => {
eprintln!("[capture] nvenc idle encode failed: {e}")
}
}
}
EncoderKind::Soft(_) => {
// 软编:直接重发最后一帧码流副本
if let Some(f) = last_frame.as_ref() {
let _ = tx.send(Some(f.clone()));
EncoderKind::Soft(enc) => {
// 软编:静止桌面没有新像素可编,只能重发最近一帧。
// 但 force_key 时必须真的重编出 IDR —— 重发旧 P 帧对
// 已经解不动的解码端毫无帮助。
//
// 裸像素来源:staging 纹理里仍是上一帧内容(DXGI 只在
// 有新帧时覆盖),重新 Map 一次即可,不必每帧额外拷 14MB。
if force_key {
enc.force_intra_frame();
let dims = (dup.width as usize, dup.height as usize);
let conv = dup.with_mapped(|src, pitch| {
if pitch == dims.0 * 4 {
YUVBuffer::from_bgra8_source(BgraSliceU8::new(
src, dims,
))
} else {
scratch.resize(dims.0 * dims.1 * 4, 0);
for (row, dst) in
scratch.chunks_exact_mut(dims.0 * 4).enumerate()
{
dst.copy_from_slice(
&src[row * pitch..row * pitch + dims.0 * 4],
);
}
YUVBuffer::from_bgra8_source(BgraSliceU8::new(
&scratch, dims,
))
}
});
match conv {
Ok(yuv) => match enc.encode(&yuv) {
Ok(ebs) => {
let payload = ebs.to_vec();
let kf = h264_contains_idr(&payload);
emit!(payload, kf)
}
Err(e) => eprintln!(
"[capture] idle keyframe encode failed: {e}"
),
},
Err(e) => {
eprintln!("[capture] idle keyframe convert failed: {e}")
}
}
} else if let Some(p) = last_payload.as_ref() {
// 非关键帧的保活重发:复用载荷但重建帧头(seq 必须递增)
let kf = last_kf;
emit!(p.clone(), kf)
}
}
}