feat(common): 协议 v5 帧头新增光标字段 cursor
4b 光标覆盖层的协议地基:VideoFrameHeader 尾部追加 Option<(i16,i16)>, 携带本帧发送时刻的远端光标物理像素坐标(None=隐藏/离屏/未实现三态折叠)。 解决「看不见远端鼠标」——相对模式下用户不知道指针在哪、轻点会落在哪。 - 字段追加改变 bincode 线格式,PROTO_VERSION 4→5,v4 旧端由握手显式拒绝(既有机制) - v4 守卫按 v3 先例降为下界锚点,最新一代用精确等号守卫 - 新增 3 测试:Option 往返(含 i16 边界与负值)、尾部追加不扰动既有字段、 None 紧凑性(Some 比 None 恰多 4 字节,证明光标未被固定宽度白占) - agent/agent-core/viewer-core 构造点补占位 cursor: None(真实采样下一步接入), 保证本提交全 workspace 可编译
This commit is contained in:
parent
e96502f888
commit
98b4f03cff
@ -189,6 +189,8 @@ fn encode_send(
|
||||
ts_ms,
|
||||
seq,
|
||||
keyframe: h264_contains_idr(&payload),
|
||||
// v5 光标字段:占位 None,本 crate 的真实采样在下一步接入
|
||||
cursor: None,
|
||||
};
|
||||
let frame = encode_video_frame(&header, &payload);
|
||||
if tx.send(Some(frame.clone())).is_err() {
|
||||
|
||||
@ -372,6 +372,8 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
ts_ms: start.elapsed().as_millis() as u64,
|
||||
seq,
|
||||
keyframe: $kf,
|
||||
// v5 光标字段:占位 None,真实采样(GetCursorInfo)在下一步接入
|
||||
cursor: None,
|
||||
};
|
||||
let payload_vec: Vec<u8> = $payload;
|
||||
let frame = encode_video_frame(&header, &payload_vec);
|
||||
|
||||
@ -22,7 +22,10 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
/// - v4:新增 `InputEvent::MouseMoveRel`(相对鼠标,单位=被控端桌面物理像素)。
|
||||
/// bincode 按位置编码枚举判别序号,新增变体会改变其后变体的编号,
|
||||
/// 故同样必须三端同步;旧端由 `Auth` 握手显式拒绝。
|
||||
pub const PROTO_VERSION: u16 = 4;
|
||||
/// - v5:`VideoFrameHeader` 尾部新增 `cursor`(本帧发送时刻的远端光标位置)。
|
||||
/// 字段追加同样改变线格式(旧端会在反序列化时少读/多读而错位),故必须三端同步;
|
||||
/// 旧端由 `Auth` 握手显式拒绝。语义见 [`VideoFrameHeader::cursor`]。
|
||||
pub const PROTO_VERSION: u16 = 5;
|
||||
|
||||
/// 控制通道消息(双向)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -132,6 +135,15 @@ pub fn clamp_text_units(units: Vec<u16>) -> (Vec<u16>, bool) {
|
||||
/// 用于丢帧检测与弱网统计;注意它统计的是"发送序",不是"显示序"
|
||||
/// - `keyframe`:本帧是否 IDR。解码端(尤其 MediaCodec)需要它决定是否重置解码器,
|
||||
/// 不必再自己扫 NAL 起始码判断
|
||||
/// - `cursor`(v5):本帧**发送时刻**的远端光标位置,被采集显示器内的**物理像素**
|
||||
/// ((0,0)=采集画面左上角,与 `width`/`height` 同尺度同原点)。
|
||||
/// `None` = 不可用,含三种情况:指针被隐藏(系统未显示光标)、指针在采集显示器
|
||||
/// **之外**(多显示器负坐标区等——采集画面里没有它,主控端画了也是错的)、
|
||||
/// 或被控端平台未实现光标采样。
|
||||
/// 用途:主控端据此在画面上标记指针位置(「看不见远端鼠标」修复);它与 `ts_ms`
|
||||
/// 同刻、同帧到达,不存在标记与画面的相对错位。
|
||||
/// 为什么 `i16`:采集原点固定 (0,0),现实桌面尺寸远小于 32767;被控端已把
|
||||
/// 越界值折叠为 `None`,到达值恒在 `[0, width)×[0, height)` 内。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoFrameHeader {
|
||||
pub width: u32,
|
||||
@ -139,6 +151,7 @@ pub struct VideoFrameHeader {
|
||||
pub ts_ms: u64,
|
||||
pub seq: u64,
|
||||
pub keyframe: bool,
|
||||
pub cursor: Option<(i16, i16)>,
|
||||
}
|
||||
|
||||
async fn read_u32le<R: AsyncReadExt + Unpin>(r: &mut R) -> Result<u32> {
|
||||
@ -279,6 +292,7 @@ mod tests {
|
||||
ts_ms: 12345,
|
||||
seq,
|
||||
keyframe,
|
||||
cursor: Some((640, 480)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,15 +361,63 @@ mod tests {
|
||||
|
||||
// ---- v4 相对鼠标 ----
|
||||
|
||||
/// 版本守卫:v4 是相对鼠标的登记点。
|
||||
/// 版本守卫:v4 是相对鼠标的登记点。v5 之后此断言随版本推进为下界,
|
||||
/// 与 v3 守卫同规(保留它是「任何一次版本变更都必须显式经过这里」的锚点)。
|
||||
#[test]
|
||||
fn proto_version_is_four_after_rel_move() {
|
||||
assert_eq!(
|
||||
PROTO_VERSION, 4,
|
||||
"新增 InputEvent::MouseMoveRel 后必须递增到 4"
|
||||
assert!(
|
||||
PROTO_VERSION >= 4,
|
||||
"MouseMoveRel 自 v4 起存在,版本不得回退(当前 {PROTO_VERSION})"
|
||||
);
|
||||
}
|
||||
|
||||
/// 版本守卫:v5 是光标字段的登记点(最新一代用精确等号)。
|
||||
#[test]
|
||||
fn proto_version_is_five_after_cursor_field() {
|
||||
assert_eq!(
|
||||
PROTO_VERSION, 5,
|
||||
"新增 VideoFrameHeader::cursor 后必须递增到 5"
|
||||
);
|
||||
}
|
||||
|
||||
/// 光标字段必须原样往返,含 `None`(隐藏/离屏/未实现三态都折叠为它)与负值边界。
|
||||
/// `None` 是本变体的核心语义:缺它主控端就无法区分「指针在 (0,0)」与「没有指针」。
|
||||
#[test]
|
||||
fn cursor_field_roundtrips_option_and_i16() {
|
||||
for c in [
|
||||
None,
|
||||
Some((0i16, 0i16)),
|
||||
Some((1919, 1079)),
|
||||
Some((-32768, 32767)),
|
||||
Some((i16::MIN, i16::MAX)),
|
||||
] {
|
||||
let mut h = hdr(1, false);
|
||||
h.cursor = c;
|
||||
let payload = b"\x00\x00\x00\x01\x65x";
|
||||
let bytes = encode_video_frame(&h, payload);
|
||||
let hlen = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||||
let back: VideoFrameHeader = bincode::deserialize(&bytes[8..8 + hlen]).unwrap();
|
||||
assert_eq!(back.cursor, c, "光标字段往返失真: {c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// cursor 是**追加在尾部**的字段:既有字段(width/height/ts/seq/keyframe)的
|
||||
/// 解码不受影响,且 `None` 变体让帧头显著变短(证明它没被塞进固定宽度里白占字节)。
|
||||
#[test]
|
||||
fn cursor_is_trailing_field_and_none_is_compact() {
|
||||
let mut with = hdr(9, true);
|
||||
with.cursor = Some((10, 20));
|
||||
let mut without = hdr(9, true);
|
||||
without.cursor = None;
|
||||
let wb = bincode::serialize(&with).unwrap();
|
||||
let wob = bincode::serialize(&without).unwrap();
|
||||
// Some((i16,i16)) 比 None 多 4 字节(两个 i16),1 字节 tag 两边都有
|
||||
assert_eq!(wb.len(), wob.len() + 4, "光标坐标应为 2×i16 定长附加");
|
||||
// 前缀不变:去掉尾部光标字段,其余字节的解码结果完全一致(追加语义)
|
||||
let h: VideoFrameHeader = bincode::deserialize(&wb).unwrap();
|
||||
assert_eq!((h.width, h.height, h.seq, h.keyframe), (1920, 1080, 9, true));
|
||||
}
|
||||
|
||||
/// 相对位移必须原样往返,**包含负值**(向左/向上移动)。
|
||||
/// 负值是本变体的核心语义:绝对坐标是无符号的,相对增量必须有符号。
|
||||
#[test]
|
||||
|
||||
@ -678,6 +678,7 @@ mod tests {
|
||||
ts_ms: 42,
|
||||
seq: 7,
|
||||
keyframe: true,
|
||||
cursor: None,
|
||||
};
|
||||
let payload = vec![0u8; 16];
|
||||
let f: Frame = (h.clone(), payload.clone()).into();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user