From 98b4f03cffe3f549784d7acc869c6c02be01efef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Mon, 2 Nov 2026 10:11:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(common):=20=E5=8D=8F=E8=AE=AE=20v5=20?= =?UTF-8?q?=E5=B8=A7=E5=A4=B4=E6=96=B0=E5=A2=9E=E5=85=89=E6=A0=87=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=20cursor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 可编译 --- agent-core/src/lib.rs | 2 ++ agent/src/capture.rs | 2 ++ common/src/lib.rs | 72 +++++++++++++++++++++++++++++++++++++++--- viewer-core/src/lib.rs | 1 + 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/agent-core/src/lib.rs b/agent-core/src/lib.rs index adf6056..ac9551a 100644 --- a/agent-core/src/lib.rs +++ b/agent-core/src/lib.rs @@ -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() { diff --git a/agent/src/capture.rs b/agent/src/capture.rs index a96d591..65439b4 100644 --- a/agent/src/capture.rs +++ b/agent/src/capture.rs @@ -372,6 +372,8 @@ pub fn run(tx: Sender>>, keyreq: Arc, congestion: Arc ts_ms: start.elapsed().as_millis() as u64, seq, keyframe: $kf, + // v5 光标字段:占位 None,真实采样(GetCursorInfo)在下一步接入 + cursor: None, }; let payload_vec: Vec = $payload; let frame = encode_video_frame(&header, &payload_vec); diff --git a/common/src/lib.rs b/common/src/lib.rs index 55435df..5a2a463 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -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) -> (Vec, 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: &mut R) -> Result { @@ -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] diff --git a/viewer-core/src/lib.rs b/viewer-core/src/lib.rs index 349d88f..c61feec 100644 --- a/viewer-core/src/lib.rs +++ b/viewer-core/src/lib.rs @@ -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();