From 8eed1e20e15ac1983ddb1f1032f80b89611d4849 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:39:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(viewer-core,android-ffi):=20v5=20=E5=85=89?= =?UTF-8?q?=E6=A0=87=E5=AD=97=E6=AE=B5=E9=80=8F=E4=BC=A0=E5=88=B0=20FFI?= =?UTF-8?q?=EF=BC=88=E5=B8=A7=E5=A4=B4=2025=E2=86=9230=20=E5=AD=97?= =?UTF-8?q?=E8=8A=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主控链路把协议帧头的光标坐标交给 Kotlin: - viewer-core::Frame 增 cursor: Option<(i16,i16)>,From 实现透传; 契约测试改为 Some((640,480)) 并断言透传——丢它=标记永不显示且丢法是静默的 - android-ffi 定长头 25→30:尾部追加 [has_cursor 1][x i16 LE 2][y i16 LE 2], 放末尾保证 Kotlin 既有偏移量(0/1/9/17/21)全部不移动 - None → has=0 且坐标字节恒 0(无意义);Some 分支显式写两坐标, 杜绝「Some 但忘写坐标」的编辑事故(文件头注释同步 30 字节布局) - R-5 同源检查:RustBridge.kt 的 FRAME_HEADER_LEN=25 与拆头偏移是本次协议变更的 同步改动点,Kotlin 侧在下一提交完成(同一循环、相邻提交) --- android-ffi/src/lib.rs | 26 +++++++++++++++++++++----- viewer-core/src/lib.rs | 9 ++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/android-ffi/src/lib.rs b/android-ffi/src/lib.rs index 3cce6e8..551cd2a 100644 --- a/android-ffi/src/lib.rs +++ b/android-ffi/src/lib.rs @@ -15,9 +15,13 @@ //! 句柄约定:`nativeConnect` 返回 Box 裸指针(永不为 0)作为 jlong; //! `nativeDestroy` 释放。句柄释放后不得再使用,Kotlin 侧用置 0 保护。 //! -//! 帧返回约定:byte[],布局 = [1 标志][8 seq LE][8 ts LE][4 w LE][4 h LE][payload]。 -//! 定长头 25 字节;Kotlin 读 buf[0] 判断 keyframe(标志位复用为:0=P 帧, 1=IDR 帧, -//! 会话结束则整个返回 null)。 +//! 帧返回约定:byte[],布局 = [1 标志][8 seq LE][8 ts LE][4 w LE][4 h LE] +//! [1 has_cursor][2 cursorX i16 LE][2 cursorY i16 LE][payload]。 +//! 定长头 30 字节(v5 起;此前 25);Kotlin 读 buf[0] 判断 keyframe(标志位复用为: +//! 0=P 帧, 1=IDR 帧,会话结束则整个返回 null)。 +//! has_cursor=0 时后 4 字节恒为 0 且**无意义**(指针隐藏/离屏/后端未实现, +//! 语义单一来源在被控端——见 common::VideoFrameHeader::cursor);=1 时坐标为 +//! 采集画面内物理像素(0 ≤ x < w、0 ≤ y < h,被控端已折叠越界值)。 use common::InputEvent; use jni::objects::{JCharArray, JClass, JObject, JString}; @@ -26,8 +30,9 @@ use jni::JNIEnv; use std::sync::{Arc, Mutex}; use viewer_core::{ConnectConfig, Session}; -/// 帧头长度:keyframe 标志(1) + seq(8) + ts(8) + w(4) + h(4) -const FRAME_HEADER_LEN: usize = 25; +/// 帧头长度:keyframe 标志(1) + seq(8) + ts(8) + w(4) + h(4) + has_cursor(1) + x(2) + y(2) +/// (v5:25→30,尾部三字段是光标;放末尾保证 Kotlin 旧偏移量全部不移动) +const FRAME_HEADER_LEN: usize = 30; struct SessionBox { session: Arc, @@ -168,6 +173,17 @@ pub extern "system" fn Java_com_verify_remote_RustBridge_nativeNextFrame<'local> buf[9..17].copy_from_slice(&f.ts_ms.to_le_bytes()); buf[17..21].copy_from_slice(&f.width.to_le_bytes()); buf[21..25].copy_from_slice(&f.height.to_le_bytes()); + // v5 光标三字段(偏移 25/26/28):None → has=0 且坐标字节保持 0(无意义)。 + // Option 展开成显式分支而不是 f.cursor.is_some() as u8——两处读同一个 + // Option,任何「Some 但忘写坐标」的编辑错误都会在这里编译期暴露。 + match f.cursor { + Some((x, y)) => { + buf[25] = 1; + buf[26..28].copy_from_slice(&x.to_le_bytes()); + buf[28..30].copy_from_slice(&y.to_le_bytes()); + } + None => buf[25] = 0, + } buf[FRAME_HEADER_LEN..].copy_from_slice(&f.payload); match env.byte_array_from_slice(&buf) { Ok(a) => a.into_raw(), diff --git a/viewer-core/src/lib.rs b/viewer-core/src/lib.rs index c61feec..58fb732 100644 --- a/viewer-core/src/lib.rs +++ b/viewer-core/src/lib.rs @@ -198,6 +198,10 @@ pub struct Frame { pub ts_ms: u64, pub seq: u64, pub keyframe: bool, + /// v5:帧发送时刻的远端光标位置(采集画面内物理像素)。 + /// `None` = 隐藏/离屏/后端未实现(语义单一来源在被控端,见 `VideoFrameHeader::cursor`)。 + /// 桌面 viewer 不消费该字段(看着自己的鼠标),安卓据它画标记。 + pub cursor: Option<(i16, i16)>, pub payload: Vec, } @@ -209,6 +213,7 @@ impl From<(VideoFrameHeader, Vec)> for Frame { ts_ms: h.ts_ms, seq: h.seq, keyframe: h.keyframe, + cursor: h.cursor, payload, } } @@ -678,13 +683,15 @@ mod tests { ts_ms: 42, seq: 7, keyframe: true, - cursor: None, + cursor: Some((640, 480)), }; let payload = vec![0u8; 16]; let f: Frame = (h.clone(), payload.clone()).into(); assert_eq!((f.width, f.height), (1920, 1080)); assert_eq!(f.seq, 7); assert!(f.keyframe); + // v5:光标字段必须透传,丢它=标记永远不显示,且丢法是静默的 + assert_eq!(f.cursor, Some((640, 480))); assert_eq!(f.payload, payload); }