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:
1 parent
703eb64fc0
commit
9e50bc6237
6 files changed
+560
-55
No files matched your search
+96
-17
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user