郑博文
|
e96502f888
|
test(tools): 4a 横屏验收脚本 + 既有 proof 适配自动隐藏与同源缺陷修复
新增 landscape_proof.ps1:V-C1~C7 判据(自动横屏 <=2s、旋转零中断、
动画桌面下 >=45fps、控制条自动隐藏、黑边唤出零远端泄漏、四按钮可点、
方向四态循环、表单竖屏)。方向判定只解析 Display: mDisplayId=0 块内
cur=WxH——这台 vivo 无 mRotation= 行,且 dumpsys 里有常驻 1080x2400 的
虚拟屏,匹配首个 cur= 会在已横屏时误报未旋转(探针实测踩过)。
横屏自动隐藏与既有 proof 的判据自冲突(HUD 随控制条 GONE,RTT= 读不到
被误判断线/未连接),三个 proof 统一加「确认隐藏态才走黑边唤醒」:
phone_chain_proof、ime_chinese_proof、rel_mouse_phone_proof。
修 rel_mouse_phone_proof 两处:
- 2c 基线判据:横屏滑动 768px 的残余相对事件成批延迟落地,旧的「两次
读数相等」被突发间隙骗过(实测假失败 351/1418px)。改为 6 读数沉降 +
4 次采样判漂移 + 绝对泄漏点特征比对。
- step 4 靶窗启动缺 3 个 Mandatory 参数,子进程瞬间失败退出(3b 起一直
如此,右键落在裸屏幕中心,靠近违反 R-11「不得注入他人窗口」)。现按
契约启动并把指针停在自家窗口内。
R-5 同源排查:靶窗结果目录固定路径 + 启动时删自己的 stop.flag,会让
上一轮失败遗留的靶窗永远收不到退休信号、继续抢前台并写脏 received.txt
(实测收到 abc\nabc)。5 个 proof 统一改每轮唯一目录(\47512 后缀)。
ime_chinese_proof 的切换键标定失效:设备 IME 已换成搜狗(原为讯飞),
旧坐标 (146,2096) 静默点空。IME 按键视图对 uiautomator 不可见、无法机械
标定,按 R-9 转人工确认,不硬凑自动化。
|
2026-11-01 21:58:19 +08:00 |
|
郑博文
|
cff7fc8ab3
|
feat(android): 横屏远控(自动方向 + 控制条自动隐藏 + 黑边唤出)
竖屏看 16:9 桌面太小(用户 2026-11-01 反馈)。探针实测(docs/output/
2026-11-01-landscape-probe/):App 级 setRequestedOrientation 在这台 vivo
上有效(761ms),且旋转只触发 surfaceChanged、不销毁 surface、不断会话、
横屏 fps=58~60——所以不需要 TextureView(探针另测 TextureView 掉 30~37%
帧率)、不需要任何解码器重建配合,旧 S3 作废。
- OrientationPolicy(AUTO/LANDSCAPE/PORTRAIT/SYSTEM)纯逻辑 + 10 单测,
常量值经 javap 对 android-34 核实;已做 mutation 反向验证(AUTO 判定
> 改 >= 时方屏用例确实红)。
- 表单页强制竖屏;连接成功按策略设方向(默认 AUTO:远端宽>高即横屏)。
- 横屏 immersive + 状态行压 36dp + 控制条 3s 无操作自动隐藏。
- 唤出区 = letterbox 黑边(stage 在隐藏态消费画面外触摸)。刻意不用
底部边缘带:R7 回归实测那种带会吃掉远端桌面底部 ~11%(Windows 任务栏
就在那里),点任务栏会变成唤出菜单。黑边不属于 SurfaceView,零冲突。
- 竖屏永不隐藏(裁决只针对横屏空间不足)。
|
2026-11-01 21:57:49 +08:00 |
|
郑博文
|
1a6c3f15a2
|
test(tools): 3b 真机验收脚本 + 修正 IME proof 的按钮定位
rel_mouse_phone_proof.ps1 覆盖 V-B1a(相对位移方向/符号/不跳变)、V-B2(切绝对
模式后 R7 重测)、V-B3(长按出右键、拖拽不出)。
三处 harness 教训(都已写进脚本注释,避免下次重踩):
1. uiautomator 不暴露 SurfaceView,裸 SurfaceView 塌成 android.view.View;
按 16:9 宽高比识别视频视图,并断言比例,避免静默抓错节点。
2. 上一段滑动的相对事件可能在 SetCursorPos 之后才落地,制造出 15px/298px 的
假跳变。判据前必须先等链路静默、并要求基线两次读数一致。
3. ime_chinese_proof 用「第一个 Button」当键盘按钮——3b 在按钮栏上方插入了
灵敏度行,第一个 Button 变成了减号,proof 报「IME not shown」。
改为按文案匹配,与位置无关。
|
2026-10-31 17:35:18 +08:00 |
|
郑博文
|
aede7b6c3c
|
test(android): 相对鼠标/长按右键纯逻辑单测 17 例
覆盖 V-B1 判据对应的行为,不是「跑通就算」:
- 慢速小幅不丢位移(10 次 0.3px 必须发出 3px)
- 负值向远离零取整(-0.7 取 -1,向零截断会让反向慢滑丢位移)
- 撞边不积债(回滑立即发出,不因补偿而空转)
- i16 越界 clamp 而非回绕
- 长按三条判据:够久、没动、非双指
已做 mutation 反向验证:把期望 3 改成 4 后测试确实红,改回后绿(断言非空转)。
|
2026-10-31 17:35:11 +08:00 |
|
郑博文
|
7c3b6b3e93
|
feat(android): 相对鼠标模式 + 灵敏度 + 长按右键 + 修复表单 IME 残留
相对模式(默认):手指滑动按增量移动远端指针,DOWN 不发绝对移动——轻点落在
远端指针当前位置,不再「手指落下指针跳到脚下」。换算基准为视图全宽=桌面全宽
(灵敏度 1.0),慢速滑动用小数累积取整,逐次 toInt() 会静默吞掉位移。
长按右键(Q5=A):>=500ms 且位移 <12dp;触发后抑制本次 ACTION_UP 的左键,
否则一次长按会「先右键再左键」把菜单立刻点掉。超过 slop 立即转拖拽并取消计时,
所以拖窗口不会弹菜单。双指右键路径保持不变。
真机暴露并修复(N19):切到远控面板后软键盘仍绑在已隐藏的表单 EditText 上
(dumpsys 里 mServedView 指向 EditText、mInputShown=true),键盘盖住面板底部
按钮栏,**所有按钮点不动**——3b 新增的模式/灵敏度按钮把它暴露出来。改为对每个
表单输入框显式 hideSoftInputFromWindow。
新增右键计数器:长按后紧跟的 ACTION_UP 会覆盖一行式诊断文本,验收脚本读不到
「长按右键」;计数器持久且能区分长按/双指来源。
|
2026-10-31 17:35:11 +08:00 |
|
郑博文
|
3773a0851c
|
feat(viewer-core,android-ffi): 相对位移上行链路 + 桌面像素级 proof
viewer-core 新增 send_mouse_rel(零位移不占消息:手指静止时 ACTION_MOVE
每帧都发,空事件只会白占 QUIC 帧)。
android-ffi 新增 nativeSendMouseRel,JNI 符号 13 -> 14,Int 入参 clamp 到 i16
而不是回绕(回绕会让指针跳到屏幕另一侧)。
新增 examples/rel_mouse_proof:走完整链路(viewer-core -> QUIC -> agent 注入
-> GetCursorPos 读回)验证 V-B1a/b/c。判据是「位移恰好等于请求值」,不是
「大概动了」——因为被推翻的 MOUSEEVENTF_MOVE 方案正是「看起来能动但比例错」。
运行前后记录并还原光标位置,不污染桌面状态。
|
2026-10-31 17:35:02 +08:00 |
|
郑博文
|
2022bbcca0
|
feat(agent-core): InputInjector 新增 mouse_move_rel 默认实现与分发
默认实现不注入但打印日志(与 v3 的 text() 同一条纪律):Linux/X11 后端本轮
未实现相对移动,用户用相对模式操作 Linux 被控端时「指针纹丝不动」不能是静默的。
相对事件不得被降级成绝对移动——单测显式断言默认实现不产生任何注入调用
(否则 Linux 端会把「相对增量」当成「绝对坐标」解释,指针乱跳)。
|
2026-10-31 17:35:02 +08:00 |
|
郑博文
|
78ac9d9972
|
feat(agent): 相对鼠标注入(读光标+增量+clamp 绝对注入)+ 显式 DPI 感知
注入实现刻意**不用** SendInput(MOUSEEVENTF_MOVE):实测该路径的 dx 不是
像素而是 mickey,会被「指针加速」与「DPI 缩放」双重放大——本机 dx=1 走 0px、
dx=20 走 41px、dx=100 走 309px,且倍率随用户系统设置与机器变化,无法标定。
改为「GetCursorPos + 增量 + clamp + 绝对注入」,实测 1:1 精确(±1/2/3/5/17/100
四次重复全部等于请求值),撞边后回滑立即响应、不积步数债。
同时显式设 PER_MONITOR_AWARE_V2:实测本机 agent.exe 虽是 per-monitor,
但那是注册表 AppCompatFlags\\Layers 里一条**路径绑定**的 HIGHDPIAWARE 覆盖
造成的(同一 exe 复制到 %TEMP% 启动即变 UNAWARE),换机器/换构建路径即失效。
UNAWARE 时 GetCursorPos 返回逻辑坐标(1536x864)而采集是物理像素(1920x1080),
clamp 上界与增量尺度都会错。设置失败时区分「已被外部设定」与「真正降级」并分别报日志。
|
2026-10-31 17:34:54 +08:00 |
|
郑博文
|
39af42e1b3
|
feat(common): 协议 v4 新增 MouseMoveRel 相对鼠标事件
v4 新增 InputEvent::MouseMoveRel{dx,dy},单位为被控端桌面物理像素,
语义是「把远端指针从当前位置移动 (dx,dy)」,越界由被控端 clamp。
为什么必须升版本:bincode 按位置编码枚举判别符,新增变体会改变其后
变体的编号,旧端会静默错解成别的输入事件。v4 后旧 v3 端由 Auth 握手
显式拒绝(已用 v3 二进制实测:client=3, server=4)。
为什么单位是像素而不是归一化增量:AuthOk 已下发桌面宽高,主控端换算
一次即可;被控端拿到像素可直接与 GetCursorPos 同尺度相加,少一次取整。
单测锁住:版本号、负值往返(i16 全范围)、与绝对 MouseMove 不混淆。
|
2026-10-31 17:34:47 +08:00 |
|
郑博文
|
114e8431a6
|
test(android-3a): 连接生命周期 proof 入仓 + 四个 proof 的主机默认值跟上环境
新增 tools\connect_lifecycle_proof.ps1(V-A1a/A1b/A2/A3/A4/A5 + 一键重连,
含 keep-awake、tap 前唤醒、期望文案码点从 Kotlin 源提取)。
四个既有 proof 的 -PhoneIp 默认值 .180 -> .110(2026-10-31 办公网实测;
stale 默认值曾静默吃掉一次 proof 运行,注释写明 DHCP 需按需传参)。
capture_evidence 的 192.168.1.199 保持不变——那是 V5 故意不可达的路径。
|
2026-10-31 15:57:57 +08:00 |
|
郑博文
|
3891c970eb
|
fix(android): 等待中的取消入口落在面板按钮上(连接后表单被隐藏)
真机 V-A1b 实测发现:startConnect() 切到远控面板后,表单上的按钮不可见,
「点取消」没有可点的目标。把底部按钮在 connecting 态显示为「取消」并共享
cancelConnect() 一条路径(两个入口一个语义),成功后复位为「断开」。
APK 重装后 proof step 3/4 通过(连接已取消文案 + 3.9s 回表单)。
|
2026-10-31 15:54:48 +08:00 |
|
郑博文
|
bb3cf6b09b
|
test(android): JUnit 测试基础设施 + 19 个纯逻辑单测(3a/U7,Q3=A 批准)
仅 testImplementation junit:4.13.2(测试作用域,不进 APK,runtime deps 仍空);
versionName→0.1-phase3a。覆盖:WinScancode 映射表(右 Shift 非扩展/UNKNOWN 落 null/
F 键值钉死待 U6)、ConnectErrorText(每类中文非空、编号契约、未知不猜)、
planBackspaces((0,0)/(after>0)/截断 64)。含 mutation 反向验证:断言写反即红。
|
2026-10-31 13:30:07 +08:00 |
|
郑博文
|
718b66617f
|
feat(android): 连接状态机 + 取消/断线回表单 + 错误中文映射 + HUD fps(3a)
- ConnectFailure(kind,detail) + ConnectErrorText:编号→中文可执行提示,原始英文第二行
- sessionGeneration 代际:取消/断开/退出作废在途回调;等待中按钮变「取消」即时回表单
- 断线可见:取帧 null 回表单显示原因 + 「重新连接」(修「冻结在已连接」);destroy 只在 UI 线程
- BACK 语义补连接中:取消而非关 Activity(修悬挂会话)
- 键盘按钮可见时收起(补唯一收起入口)
- HUD 1s 窗口 fps;触摸/按键诊断去坐标去内容(V-A5,proof 不依赖这些字符串)
- RemoteInputConnection 退格决策抽纯函数 planBackspaces(为 JVM 单测,语义不变)
|
2026-10-31 13:30:07 +08:00 |
|
郑博文
|
f28db050fb
|
feat(android-ffi): nativeLastErrorKind 出口带错误分类(3a)
LAST_ERROR 槽位改存 (kind, detail),新增 nativeLastErrorKind();FFI 契约
kind 先取、message 后取(消息读走清空槽位)。kind 编号与 viewer_core::
ConnectErrorKind::code 一致,跨 FFI 只传整数。JNI 符号 12→13 全部导出(字节扫描核)。
|
2026-10-31 13:29:48 +08:00 |
|
郑博文
|
4eb82f9779
|
feat(viewer-core): 连接失败分类 + 短预算×有限重拨(3a/U8)
ConnectErrorKind(Resolve/Timeout/Refused/Auth/Version/Other)在 Rust 侧分类,
Kotlin 只做编号→文案映射。分级预算:第一拨握手 5s(快速判不可达)、重拨拨 8s
(接住 agent 串行 accept 的僵尸释放 ~10.2s)、认证 11s、全程 14s 封顶。
is_retryable 只对 Timeout/Refused 重拨;Resolve/Auth/Version 确定性失败不拖慢。
判据演进(偏差 D2/D3,用户 2026-10-31 裁决方案①):单次 5s 总预算在串行 accept
架构下把合法排队压断(text proof 实爆),故改重拨。不可达 32s→~13s 且全程可取消;
僵尸重连第一拨 5s 失败第二拨 10.2s 自动成功(桌面 e2e 实测)。9 单测锁分类/预算/重拨。
|
2026-10-31 13:29:48 +08:00 |
|
郑博文
|
99ffffa393
|
test(android-2b): 真机 emoji 人工验收工具 + 链路 proof 参数化主机地址
V7 设备侧此前转人工,原因是 IME 的 emoji 面板对 uiautomator 不可见、adb 无法
驱动。工具只负责准备接收窗口并读回**实际到达的码元**,点 emoji 由人完成——
判据仍是真实字节,不是截图。
真机实测通过:靶窗收到 U+D83D U+DE02 -> U+1F602,两个 code unit 相邻成对、
无 U+FFFD。这证明手机 IME 把增补平面字符作为一次 commitText 送出,协议 v3
保住了代理对相邻性。至此 V7 双侧都有证据。
同时修掉 phone_chain_proof 里硬编码的 192.168.1.180:本机 LAN 地址变成
192.168.1.110 后,表单连接打向旧地址超时,看起来像产品缺陷,实为脚本过期。
改为从 -PhoneIp 参数逐字符生成 keyevent,并在断言里比对实际输入值。
|
2026-10-31 11:08:16 +08:00 |
|
郑博文
|
8993a38658
|
fix(android): BACK 键改用自测键盘可见性——isAcceptingText 会让会话永远断不掉
真机实测(vivo S6 + 讯飞,2026-10-30):软键盘已隐藏、mInputShown=false,
但 imm.isAcceptingText 仍返回 true——只要 InputConnection 还绑着它就是 true。
原先用它判断「先关键盘还是先断会话」,后果是第一次 BACK 空转,第二次也进不来
(我们已 return true),会话再也断不掉。这不是推测,是完整链路 proof 跑到
step 5 时实测失败的。
改为自测:比较根窗口可见区域与窗口高度,被 IME 吃掉超过 15% 视为可见;
用 OnGlobalLayoutListener 被动跟踪(没有公开 API 能直接查询,API 34 上
getInputMethodWindowVisibleHeight 也不是公开接口)。阈值用比例不用固定像素。
验证:diag 依次显示「键盘可见」→「键盘隐藏」→ 会话断开回表单;
随后完整链路 proof 与中文 proof 均复跑通过。
同时修正 phone_chain_proof 的 V4 断言:uiautomator 属性顺序是 text= 在
class= 之前,原正则要求反序,导致打印空表单行仍判 PASS(假绿)。
空 EditText 的 text 会回显 hint,所以判据改为比对实际输入值。
|
2026-10-30 13:31:28 +08:00 |
|
郑博文
|
9792662181
|
test(android-2b): 真机中文候选词 proof——拼音组词经 commitText 落到 Windows
V6 的判据是「手机中文输入法能把汉字送进远端」,此前只能靠人手点候选词。
实测发现两条关键事实,决定了 proof 的可行形态:
1. adb keyevent 相当于物理键盘输入,中文 IME **不组词**,字母原样透传到
Activity.onKeyDown——所以驱动候选词必须用 adb input text(走 IME)。
2. IME 自己的按键视图对 uiautomator 不可见(dump 里只有我们 App 的 15 个节点),
中/英切换按钮读不到坐标,只能按截图标定一次点击位;且该按钮**有状态**,
于是写成幂等重试:先试,只收到字母才翻转模式再来一次。
跑通结果:讯飞拼音 nihao + 空格 -> commitText 2u -> 靶窗收到 U+4F60 U+597D。
这补上了方案风险 R1(vivo ROM + 第三方中文 IME 行为未观测)的核心一环。
|
2026-10-30 13:15:27 +08:00 |
|
郑博文
|
0089ae86e1
|
test(android-2b): 真机 IME 提交 proof 与字节安全证据采集
ime_commit_proof 补上 phone_chain_proof 没覆盖的那半段:IME **显示**状态下
按键被输入法消费,只有 RemoteInputConnection.commitText 能到达我们——这正是
方案风险 R1(vivo ROM + 第三方中文 IME 行为未观测)此前唯一没有证据的点。
真机实测:mInputShown=true、mHaveConnection=true、状态栏 commit 2u、
Windows 靶窗收到 Hi(讯飞把首字母自动大写,属输入法行为,不是链路缺陷)。
back 键第一次只关键盘不断会话,也在这支脚本里断言。
shot.ps1 / capture_evidence.ps1:PS 5.1 的 '>' 以 UTF-16 写文件,
'adb exec-out screencap -p > x.png' 产出的 9 张证据图首字节全是 FF FE,
全部不可用;改为设备内 screencap 落地再 pull,并校验 PNG 签名。
V5 等待预算 30s→50s:不可达 IP 实测 32s 才回表单报 timed out,
预算过小会把「仍在超时中」误判成验收失败。
|
2026-10-30 13:06:08 +08:00 |
|
郑博文
|
9f0b0742a8
|
feat(android): 键盘事件双通道 + 手机到 Windows 全链路 proof
真机暴露出 IME/按键通道的缺口,一并补上并证明:
1. RemoteInputConnection.sendKeyEvent 与 Activity.onKeyDown 都先处理**可打印**
unicodeChar(走 v3 Text 通道),再退到扫描码查表。原先只查表:软键盘发的是
keyCode=UNKNOWN + unicodeChar,查不到表就被当未映射键吞掉,字整个丢了。
门槛排除控制值(Enter/Tab)——远端应用要的是按键事件而非 WM_CHAR。
2. onKeyDown/onKeyUp 补齐硬键与物理键盘路径(阶段 2 验收里的软键盘按键),
repeatCount>0 只发一次 down,避免长按把远端消息队列刷爆。
isAutorepeat() 是 @hide,改用公开的 getRepeatCount()(javap 核实)。
3. getUnicodeChar() 返回的是单个 UTF-16 code unit 而非码点,注释按此改正:
BMP 外字符由 IME commitText 成对送达,不经这条路。
tools/phone_chain_proof.ps1:手机按键 -> v3 Text -> agent SendInput -> 自有
Windows 靶窗自报内容,全链路一次跑通(真机 vivo S6 实测 abc 到位、ENTER 走
scan 28、BACK 断开回表单、表单 UI 重连成功)。要点:
- 靶窗必须自报窗口句柄(pid+hwnd 两行),Get-Process 取 MainWindowHandle 有竞态;
- 不能用 keybd_event(VK_MENU) 破前台锁:ALT 之后的首个字符被当菜单助记键吞掉,
实测三个字符只剩 b;改用 AttachThreadInput + SetForegroundWindow,不合成按键;
- 退役遗留靶窗只能 touch 已存在的 stop.flag:新建会命中即将启动那次的停止路径,
靶窗刚起就自杀(实测 pid 报了又消失);
- adb 进度信息走 stderr,PS 5.1 + ErrorActionPreference=Stop 会当终止错误,
按 build-android.ps1 同款办法临时放宽并只看退出码。
- 表单填字用 keyevent 而非 adb input text:设备只有第三方中文 IME,text 注入会被
组词(ASCII 句点变 U+3002)。跑前记录并禁用原 IME,finally 精确还原。
|
2026-10-30 12:58:36 +08:00 |
|
郑博文
|
a93430dd3f
|
chore(tools): 2b 注入 proof 路径随仓库位置推导,缺失参数报错文案改正
proof 脚本原先硬编码 D 盘绝对路径,换机器就失效;仓库内已有同约定
(build-android.ps1 用 \ 推导),此处对齐。
text_check 的报错文案说「为空」,实际触发条件是 --units 缺失,改正。
|
2026-10-30 12:21:19 +08:00 |
|
郑博文
|
79e3e77c97
|
fix(android): windowSoftInputMode=adjustPan——软键盘弹出不再踢掉会话
默认 adjustResize 会缩小窗口高度,SurfaceView 随之收到 surfaceDestroyed,
而现有回调把 surfaceDestroyed 当作「断开」处理——等于用户一点键盘输入,
2a 验收过的会话就自己断了。真机首验前先做这道防御;adjustPan 只平移窗口,
不改变 surface 尺寸。合并后的 manifest 已核对(android:windowSoftInputMode 存在)。
|
2026-10-30 12:17:20 +08:00 |
|
郑博文
|
8ffe19cd5f
|
test(agent-core): v3 分发路径单测——默认后端不 panic、截断契约、空输入
新功能必须带测试,agent-core 此前无测试模块,v3 新增的 text 默认实现与
截断分支正好落在盲区里:
- 后端不实现 text() 时(Linux/X11 的真实形态)必须被正常调用且不 panic;
- 超 MAX_TEXT_UNITS 必须以截断后的长度到注入侧,未超限原样;
- 空 Text 仍会下发到后端——被控端不假设对端守规矩,线格式输入按不可信处理。
|
2026-10-30 12:11:28 +08:00 |
|
郑博文
|
936986a940
|
test(android-2b): 文本注入真边界工具——viewer-core 直发 Text,靶窗自报收到内容
text_check 走的是产品同一条路径(Session::send_text → v3 线格式 → agent
SendInput KEYEVENTF_UNICODE),手机掉线也能先把 Windows 半段验实。
参数是十进制 code unit 列表而不是自然语言字符串:命令行到 Rust 的编码往返
会引入与本轮无关的变量,由 PowerShell 从 .NET 字符串取 char[] 更忠实。
proof 脚本刻意用自有 WinForms 靶窗而非记事本:Win11 记事本所有标签页同进程,
本机当时开着用户文档,往里注入等于改用户文件。靶窗自报文本(写文件),
判据不依赖截图和 OCR。注入前后各断言一次前台窗口——SendInput 打的是前台,
靶窗不在前台时的注入会进别的应用,宁可失败也不误注入。
|
2026-10-30 12:09:48 +08:00 |
|
郑博文
|
6c28a3f7e3
|
feat(android): 阶段 2b——连接界面 + IME 文本通道 + 控制键扫描码
两态 UI:连接态是 host/port/code 表单(原生 View 零依赖,intent extras 保留且
优先,构建脚本不受影响),成功后切远控态。只存 host/port;验证码不落盘(用户裁决)。
返回键分态:IME 显示时先收键盘,否则断开回表单——否则一次返回关键盘又杀会话。
IME 走 P1:onCheckIsTextEditor + BaseInputConnection 子类。commitText 整段
经 v3 Text 通道上行(char[]→GetCharArrayRegion,UTF-16 无损);控制键映射
Set-1 扫描码(值与 viewer/keymap.rs 同源,右 Shift 按官方文档不置 extended)。
composing 不上远端(U4),getText* 回空串——本地没有也不伪造远端缓冲区。
键盘按钮显式 showSoftInput,不自动弹(vivo 行为不一致 + 挡画面)。
RemoteSurfaceView 补 isFocusableInTouchMode:没有它触摸态拿不到焦点,
InputConnection 根本不会建立(2a 弹不出键盘的根因之一)。
|
2026-10-30 11:51:01 +08:00 |
|
郑博文
|
55cc8edbc1
|
fix(agent): 文本注入改为单次 SendInput 批量——按官方文档消除事件被打散的风险
实读 Keyboard Input Overview 后修正两处凭记忆的实现:
1. 原文承诺「单次调用的事件不会被其他输入插入」,此前拆成 down 批 + up 批
两次调用,两次之间可能夹进真实键鼠事件,代理对存在被拆开的风险。合并为
一次提交(全部 down 紧接全部 up)。
2. KEYBDINPUT 文档「只能与 KEYEVENTF_KEYUP 组合」那句按推断写成结论;
改为如实标注为推断,并写明真机失败时的退路(只发 UNICODE|KEYUP 单事件)。
|
2026-10-30 11:41:54 +08:00 |
|
郑博文
|
174a3f8ac0
|
feat(viewer-core,android-ffi): 主控端文本上行与 JNI 入口
Session::send_text 收 &[u16] 而不是 &str:整条链(Java 字符串 → JNI → 线格式 →
Windows 注入)的原生表示都是 UTF-16,任何一次 UTF-8 往返都有损,故中间层不做转换。
空输入与超限在本层拦掉,超限走 clamp_text_units 并打日志。
nativeSendText 的参数用 char[](JObject 收再转 JCharArray),经 GetCharArrayRegion
按 u16 直接拷贝。不用 JString + get_string:jni 0.21 那条路走 Modified-UTF-8,
IME 提交边界的落单代理项会在往返里丢。JNI 取数失败必须 eprintln,不能静默——
用户对着远端打字最坏的情况是「看起来发送了」。
|
2026-10-30 11:41:53 +08:00 |
|
郑博文
|
5075888861
|
feat(agent): v3 文本注入用 KEYEVENTF_UNICODE,一次批量提交成对代理项
Windows 侧按官方 KEYBDINPUT 语义实现:置 KEYEVENTF_UNICODE 时 wVk 必须为 0、
wScan 即要送出的字符,系统合成 VK_PACKET 经 TranslateMessage 产出 WM_CHAR。
down 批与 up 批分两次 SendInput,且让同一段文本的 code unit 在输入队列里相邻,
使代理对能被远端合成一个字符(最终由真机 emoji 验收定论)。
agent-core 的 InputInjector::text 用默认实现记日志而非静默忽略:Linux 后端
本轮不做文本注入(X11 需 keysym/XKB 映射,另立循环),但用户对着 Linux
被控端打字不能「什么都没发生还没痕迹」。Windows 端走自己的 handle(),
不经 agent-core,故超限截断日志在两侧同源补齐。
日志只打长度,不打用户输入内容。
|
2026-10-30 11:29:25 +08:00 |
|
郑博文
|
38df106fb6
|
feat(common): 协议 v3——新增 InputEvent::Text 文本注入通道
安卓软键盘的中文候选词结果不是扫描码,扫描码通道对中文天然不通,必须
另开一条按字符注入的通道。载体选 Vec<u16>(UTF-16 code unit)而非 String:
Windows 注入侧与 Java 字符串原生都是 UTF-16,中间任何一次 UTF-8 往返都会
把 IME 提交边界的落单代理项变成替换字符。
InputEvent 因携带 Vec 不再是 Copy,调用方改用 clone;三端必须同步升级,
旧端由 Auth 握手显式拒绝(这正是 PROTO_VERSION 递增的目的)。
新增 6 个单测覆盖 roundtrip、落单代理项、上限边界与 1MiB 长度闸共存。
|
2026-10-30 11:29:13 +08:00 |
|
郑博文
|
5079d33b0a
|
fix(agent): 显式 max_idle_timeout=10s——强杀主控端后 10 秒内可重连
问题(实测复现)
串行 accept 下,主控端被强杀(force-stop,非正常 close)时 QUIC 连接
不会立即消亡:agent 还在向死连接发帧,新连接只能排队,要等 quinn 默认
idle 超时(约 30s)才能建立。表现为「杀掉 App 重开,一直连不上」。
修法
TransportConfig 显式设 max_idle_timeout=10s。正常会话每 500ms 一个 Ping
永不触发该超时,只回收死连接。10s = 20 个 Ping 周期,抗抖动余量足够。
验证(vivo S6 实测)
- 会话1 auth ok -> force-stop App -> 立即重连
- 10.2s 后 peer connected + auth ok(修复前 ~30s)
|
2026-10-30 10:38:04 +08:00 |
|
郑博文
|
fe558a1c1e
|
feat(android): 状态栏触摸诊断通道(vivo logcat 被限流的对策)
本机(vivo S6 + 办公 Windows)logcat 只能抓到 4 行,App 日志完全不可靠,
排查触摸/坐标问题时没有任何可用通道。
- RemoteSurfaceView 加 onDebug 回调:ACTION_DOWN/MOVE/UP 的视图坐标与
发出的 norm 归一化坐标实时上报
- MainActivity 统一 refreshStatus():连接态 + 帧数/RTT + 最近触摸合并成
一处刷新(原来 8 处各自 statusView.text 互相覆盖,且引用过已删的 showHint)
- 删掉临时步骤标记与 lastMeasureInfo(诊断价值已被 onDebug 覆盖)
实测:状态栏正确显示 UP view=(643,222) norm=(39017,23968) 1080x607,
据此确认了 letterbox 视图尺寸与归一化换算全对,把排查焦点正确引向
agent 注入侧(VIRTUALDESK,已另行修复)。
|
2026-10-30 10:35:21 +08:00 |
|
郑博文
|
6a6f989595
|
fix(agent): 绝对坐标注入去掉 VIRTUALDESK——注入空间与采集空间对齐
根因(实测确认)
capture 用 EnumOutputs(0) 只采**主屏**,AuthOk 报告的 width/height 也是主屏;
但 input 注入带 MOUSEEVENTF_VIRTUALDESK,归一化 0..=65535 被映射到**整个虚拟
桌面**。双显示器机器上虚拟桌面(主屏 2560x1440 + 副屏 1920x1080 在左)比主屏
宽得多,手机画面与指针位置系统性错位——点主屏任务栏,指针落到别处。
单显示器时两个空间重合,所以桌面 viewer 联调从未暴露。
修法
MOUSEEVENTF_ABSOLUTE 不带 VIRTUALDESK 时,0..=65535 恰好映射到主显示器,
与采集空间一致。协议注释「鼠标坐标为虚拟桌面归一化」的语义由「整个虚拟桌面」
修正为「被采集的输出」——桌面 viewer 是绝对定位透传,行为不变。
验证(vivo S6 -> 本机双屏,修复后)
- 手机拖微信标题栏:窗口跟随手指移动(drag-before/after 截图对比)
- App 状态栏 norm 坐标与视图坐标换算一致(view=(643,222) norm=(39017,23968))
|
2026-10-30 10:30:53 +08:00 |
|
郑博文
|
5047618c01
|
feat(android): 触摸映射 + letterbox 显示(阶段 2a 代码部分)
问题:阶段 1 的 SurfaceView 用 MATCH_PARENT,2560x1440 的视频被拉进
1080x2400 竖屏,画面变形。变形状态下手指位置与远端指针不再一一对应,
触摸映射无从谈起。所以两者必须一起做。
新增 RemoteSurfaceView
- onMeasure 按视频宽高比 letterbox:视图尺寸 = 画面尺寸,黑边不属于视图,
触摸天然只在画面内产生事件,坐标归一化到 0..=65535 才准确
- 手势:单指轻点=左键、单指拖动=左键拖拽、双指轻点=右键、双指滑动=滚轮
- 刻意不做「手指即光标」的绝对跟随:手指会完全挡住目标,精细操作不可用
- 边界处理:进入拖拽后落下第二指,先抬起左键再切双指手势,
否则远端会留下一个被按住未释放的按键
协议侧(不改线格式,只加只读查询)
- viewer-core::Session 记录 AuthOk 的桌面尺寸,新增 desktop_size()
- android-ffi 新增 nativeDesktopSize(handle) -> i64(高32=宽,低32=高)
- RustBridge.desktopSize() 拆包
- 尺寸必须在建解码器之前拿到:等首帧再改视图尺寸会触发重新测量,
把刚创建的解码器 surface 打掉
MainActivity
- 接入 RemoteSurfaceView.Listener;连接成功后先取尺寸设宽高比,再起解码线程
- 返回键 = 断开(不退出 App,便于重连);去掉阶段 1 的双击断开
(双击现在语义上是「两次左键点击」,会和远端交互冲突)
验证:cargo build --workspace 通过;cargo test 全绿(common 20 + viewer-core 3)。
真机手势验收待手机重新连接后补。
|
2026-10-30 09:51:38 +08:00 |
|
郑博文
|
f119300a57
|
feat(agent): 串行多会话——会话结束不再退出进程
问题(阶段 2 的硬阻塞)
accept() 只接一次,会话结束进程退出。阶段 1 一次验收看不出来,
但阶段 2 要反复测试输入,每测一次都要手动重启被控端。
(即公共阻碍清单里的 P5,此前判断为「只影响调试效率」,
现在它阻塞迭代,提前做掉。)
改动
1. main:会话体抽成 serve_session(),外层 accept 循环。
会话失败(含认证失败)只记日志不退出进程。
明确不做并发多会话:DXGI Desktop Duplication 每个输出仅允许一个
活动实例,第二个 DuplicateOutput 会让第一个收到 ACCESS_LOST。
2. 认证失败路径补 conn.closed() 等待(agent-core 已有同款修复,
此处同源补齐 = 手册 §6.1 第 1 条):否则客户端只看到 connection
lost 而不是 AuthFail。
3. capture 循环加 tx.receiver_count()==0 守卫。
只靠「send 失败退出」不够——桌面完全静止时那个分支根本不发送,
线程会永远持有 duplication,下一个会话的 DuplicateOutput 直接失败
(表现为新会话黑屏)。agent-core 同源修复。
4. 会话收尾:finish 流 + drop 接收端 + 等采集线程退出(2s 超时)后才
返回,调用方才 accept 下一个连接。
验证(连续两个会话,中间不重启 agent)
会话1:收到帧 40/40,结论 ✅
agent 存活:✅
会话2:收到帧 40/40,结论 ✅
日志:session closed -> waiting for next connection -> peer connected
-> auth ok;stderr 有 [capture] no receiver, stop 证明线程真退出
|
2026-10-30 09:31:44 +08:00 |
|
郑博文
|
58bb66f4c2
|
feat(android): 阶段 1 安卓主控 App 工程(已真机验收出画面)
Kotlin 层三个文件(刻意零第三方依赖,纯 View 布局——阶段 1 只验证链路):
- RustBridge.kt:android-ffi 的 JNI 声明 + 帧头拆解(25 字节定长头 + Annex-B 载荷)
- H264Decoder.kt:MediaCodec 同步模式 + Surface 直出;内置两条实测教训——
只用 getInputBuffer(i)(Exynos Quirk)、awaitingIdr 状态机(R-3 跳帧断参考链)
- MainActivity.kt:连接/解码循环/触摸绝对坐标映射;断开顺序遵守
「先停解码线程 join 后 destroy」的 FFI 纪律
构建链(build-android.ps1,一条命令:cargo ndk -> .so -> gradle assembleDebug -> 安装启动):
- 路径自适应( 定位仓库根),local.properties 自动生成
- gradle.properties 固定 JDK 17,不动全局 JAVA_HOME
真机验收(vivo S6 / 192.168.1.163 -> PC / 192.168.1.180:47800):
- agent 日志:peer connected + auth ok, desktop 2560x1440
- 手机截图:Windows 桌面(WPS 文件列表)已渲染在 SurfaceView 上
证据:evidence/screen-connected.png(本地,不入库)
已知环境限制(记录在个人层 baseline):本机 Windows 防火墙的 agent.exe
程序级放行规则对 UDP 入站不生效(临时关防火墙即通),QQPC 驱动级防护
静默拦 TCP 入站(管理员无法停止/杀死)。阶段 2 前需落定长期方案:
端口级防火墙规则或换开发机。
|
2026-10-29 19:59:43 +08:00 |
|
郑博文
|
64d497e577
|
test(viewer-core): headless_check 增加 --host,可指定被控端地址
默认仍 127.0.0.1。加 --host 后能用局域网 IP 跑同一验收,
便于排查「桌面通、手机不通」这类只影响入站路径的问题
(注:本机回环连接不经过防火墙,只有跨主机连接才验证防火墙路径)。
|
2026-10-29 19:18:11 +08:00 |
|
郑博文
|
44767ed370
|
feat(android-ffi): viewer-core 的 JNI 薄层,arm64 交叉编译通过(QUIC 栈上手机的第一道关)
方案变更说明(A2 落地修正):原裁决选 UniFFI,但实查 Maven Central 与
maven.mozilla.org 均无 uniffi Kotlin 运行时构件(404),自编译发布成本超过收益。
改为裸 JNI:阶段 0(真机)已验证该链路,接口刻意保持窄(连接/取帧/发输入/销毁),
将来若引入 UniFFI 只替换本层。
线程与锁纪律(决定接口形态):
- nativeNextFrame 等帧期间不持会话锁(clone Arc<Session> 后即释放),
桌面静止 500ms 帧间隔不会卡住 UI 线程的输入调用
- destroy 前必须先停解码线程,Kotlin 侧顺序纪律保证(已登记风险清单)
- 句柄 = Box 裸指针(永不为 0),destroy 后置 0
帧返回约定:byte[] = [keyframe 标志 1][seq 8][ts 8][w 4][h 4][Annex-B payload],
定长 25 字节头,Kotlin 切片零 JNI 往返。
viewer-core 适配:Session.frames 改 Arc<tokio Mutex>,next_frame 取 &self,
使解码线程与输入线程可并发借用同一 Session。
验证:
- 桌面 cargo build -p android-ffi 零警告
- cargo ndk -t arm64-v8a --release:quinn/ring/rustls 交叉编译全部通过
(消除阶段 1 最大未知:QUIC 栈能否编到 Android)
- libandroidffi.so 4.2MB,10 个 Java_com_verify_remote_RustBridge_* 符号导出正确
|
2026-10-29 18:27:34 +08:00 |
|
郑博文
|
34df7b070b
|
feat(viewer-core): 新增安卓主控的连接/协议/收帧层(阶段 1 桌面验证通过)
定位(2026-10-29 方案裁决 A1-A4):
纯 Rust、平台无关,只做「怎么连 + 收发什么」——QUIC 会话、协议 v2 认证、
视频帧流、输入上行、Ping/Pong、KeyFrameReq。解码/渲染/触摸全在 Kotlin 侧,
本 crate 不碰像素、不依赖 openh264(dev-dependency 仅验证工具用)。
设计要点
- PeerTarget::Direct 是唯一的连接形态;不做中继、不定义 Relay 变体
(《多平台架构设计》§6.1 裁决)。将来设备码+打洞只换 connect() 内部。
- 帧经容量 2 的 mpsc 交付,**不丢帧**:跳帧决策属于看得到解码器状态的消费方
(Kotlin),Rust 层丢帧会重演 R-3 教训(跳帧断参考链)。
- insecure_skip_verify=false 时明确报错而非静默放行——证书钉扎登记在方案
阶段 4,安全红线不允许默默降级。
- 双流结构与桌面 viewer 一致(流1 认证+视频,流2 输入+控制),入会即请求 IDR。
验证(headless,桌面→桌面 127.0.0.1)
- 单测 3 passed(帧契约、默认配置、strict-verify 显式报错)
- examples/headless_check:300 帧,seq 违例 0、keyframe 标志不符 0、
解码成功 300/300、RTT 路径工作(RTT=1ms 出现)
- 全 workspace 回归:common 20 passed,构建全绿
|
2026-10-29 18:14:27 +08:00 |
|
郑博文
|
1837658c85
|
test(agent): dump_stream 增加 --save,可导出裸 H.264 码流供外部解码器验证
目的
安卓主控方案的唯一高风险未知项是「agent 发出的码流能否被 MediaCodec 解码」。
要验证它,需要一份脱离 QUIC/自定义帧头的**裸 Annex-B 码流文件**,供手机端
或任何标准解码器直接读取。
改动
- 新增 --save <路径>:把各帧载荷按到达顺序拼接成 Annex-B 文件
- 存的是**载荷**而非完整帧:外部解码器只认标准 Annex-B,不认识本项目的
长度前缀与 bincode 帧头,故不能存 encode_video_frame 的输出
- 保存时打印字节数与 IDR 数量,便于快速判断样本是否可用
不做的事
不引入独立的码流解析库,IDR 统计复用文件内已有的起始码扫描逻辑,
避免为一次性诊断工具增加依赖。
验证
cargo build --workspace 退出码 0;cargo test --workspace --exclude linux-agent 全绿。
实际使用:导出 90 帧、437823 字节、含 2 个 IDR,该文件已用于安卓 MediaCodec
兼容性验证(vivo S6 / Exynos 硬解,87/89 帧解出)。
|
2026-10-29 16:13:58 +08:00 |
|
郑博文
|
6e83bda8b8
|
perf(agent): 关键帧周期从 4s/10s 统一到 1s,缩短丢帧后的兜底恢复窗口
背景
协议 v2 与 vendor nvenc 补丁之后,KeyFrameReq 已成为**真正生效**的恢复主路径
(此前 NVENC 缺 FORCEIDR 标志,请求被静默忽略)。因此周期 IDR 的角色变了:
从「唯一恢复手段」降级为「兜底手段」,只用于防「请求连续丢失」这种极端情况。
既然是兜底,就没必要配得那么长。
问题:三处周期不一致,且都过长
- agent/src/capture.rs NVENC:gop_len = 240 帧 @60fps = 4 秒
- agent/src/capture.rs 软编: intra_period = 300 帧 @60fps = 5 秒
- agent-core/src/lib.rs Linux: intra_period = 300 帧 @30fps = **10 秒**
同一产品在三种编码后端上的恢复等待时间差 2.5 倍,且 Linux 端最差。
改动
- 新增 agent/src/capture.rs::INTRA_PERIOD_FRAMES = 60(Windows 两个编码器共用)
- agent-core 的 EncoderSettings::default() 改为按 max_fps 推导(1 秒对应帧数),
避免以后调帧率时周期又漂移
代价
IDR 是整帧内编码,实测一次约 95-120KB,而 P 帧约 3-10KB。1 秒一次 IDR 约
增加 0.8Mbps 开销,在 5Mbps 码率下可接受。
验证(dump_stream 观察 150 帧)
[ 0] seq=0 IDR ← 首帧
[ 60] seq=60 IDR ← 间隔精确 60 帧
[120] seq=120 IDR
@60fps 即每 1 秒一个自动 IDR,与 gop_len=60 配置一致。
|
2026-10-28 16:35:44 +08:00 |
|
郑博文
|
8410a8de2a
|
docs: 记录「不做中继」的最终裁决(2026-10-28)
背景:公司决定不提供中继服务器,中继方案从「暂缓」变为「排除」。
《多平台架构设计.md》
- §6 从「⚠️ 必须现在拍板」改为「✅ 已裁决」,保留原始分析但标注结论
- 新增 §6.1 裁决内容:不做中继、不预留中继接口、IPv6 一等公民、
连接层边界仍要做
- 新增 §6.2 澄清一件常被混淆的事:**连接层边界 ≠ 中继接口**
- 连接层边界(把「怎么连」与「连上干什么」分开):要做,与中继无关,
因为连接方式确定会变(IP→设备码)、安卓端要复用、解码逻辑要可单测
- 中继接口(Path::Relay 这类类型预留):不做。中继已排除,这些变体
永远不会被构造,只会产生 dead_code 警告——零收益的预留就是纯负担
- 新增 §6.3 对产品的影响:明确哪些网络环境连不上,并说明这不是 bug
而是已知边界,UI 必须给出可执行提示
- §10 第 1 条标记为已裁决
《打洞与信令设计.md》
- 状态更新为「已合入 main(703eb64)」
- 补充裁决确认,指向 §6.1/§6.2
补充事实:此前代码中并不存在 PathProvider / Path::Relay,唯一提到 relay 的
signaling.rs:7 是在声明「本协议不含中继」。因此本次是「确认不留」而非
「拆除预留」。
|
2026-10-28 16:35:32 +08:00 |
|
郑博文
|
192513dd2a
|
fix(viewer): 跳帧后进入「等待 IDR」状态,不再把必然失败的 P 帧喂给解码器
问题
H.264 的 P 帧依赖前一帧的参考图像。主控端的积压跳帧逻辑(读出但不解码)
会切断参考链,此后所有 P 帧都解不出来,且不会自行恢复——必须收到 IDR。
旧实现跳帧后只是 continue,后续 P 帧仍被送进 decoder,导致:
- 每帧解码失败一次 → stderr 刷屏「decode failed」
- 每次失败都触发一次 KeyFrameReq → 请求风暴
- 解码器反复在错误状态里挣扎
实测(修复前,10 秒会话):decode failed 刷屏数十条。
修复
引入 awaiting_idr 状态,语义是「参考链已断,等 IDR 重新同步」:
- 跳帧时置位,并请求关键帧
- 置位期间跳过所有 P 帧(这是正确性要求,不是优化)
- 收到 hdr.keyframe=true 的帧才清位并恢复解码
- 解码出错时也置位
- 初始值为 true:入会时已发 KeyFrameReq,拿到第一个 IDR 前不该硬解
该实现依赖协议 v2 新增的 hdr.keyframe 字段(从实际码流 NAL 类型判定),
因此必须在协议改动之后合入。
HUD 新增 STATE 行显示 OK / RECOVER。RECOVER 期间画面冻结是**预期行为**,
没有这个指示用户会误以为软件卡死。
验证(同机 agent+viewer,25 秒)
- decode failed:0 次(修复前刷屏)
- stderr 总行数:3(仅首帧日志 + 2 条积压提示)
- 窗口稳定无假死
已知局限(下一步)
本修复解决的是「跳帧后不再做无用功」,但跳帧本身仍会造成画面冻结直到
IDR 到达。NVENC 的 gop_len=240、@60fps 即 4 秒才有一个自动 IDR,恢复窗口
偏长。缩短 GOP 或降低跳帧触发频率是后续优化项。
|
2026-10-28 16:10:40 +08:00 |
|
郑博文
|
e778ff73cc
|
test(agent): 新增 dump_stream 码流诊断工具
用途:不依赖 viewer 的 Minifb 窗口,直接抓 agent 下发的码流并逐帧打印
NAL 结构与解码结果,用于定位解码恢复类问题。
能力
- 打印每帧的 seq / keyframe / 字节数 / NAL 类型(SPS PPS IDR P)
- --skip-at N:模拟主控端「积压跳帧」——读出但不解码,用于验证跳帧是否
切断参考链(实测确认:跳掉一帧后所有 P 帧持续解码失败,直到收到 IDR)
- 交叉校验帧头 keyframe 标志与码流实际 NAL 类型是否一致
- 末尾补发 KeyFrameReq 并观察 IDR 是否真的回来
用法
cargo run --release -p agent --bin dump_stream -- --port 47800 --code 123456
cargo run --release -p agent --bin dump_stream -- --port 47800 --code 123456 --skip-at 1
注意:KeyFrameReq 必须写在流 2(isend)——agent 的读循环只监听流 2,
写到流 1 会被静默忽略(本工具第一版踩过,误判「修复无效」)。
这个工具是发现 nvenc FORCEIDR 缺陷的直接手段,保留以备回归排查。
|
2026-10-28 16:05:41 +08:00 |
|
郑博文
|
e814cb64e7
|
fix(agent): vendor nvenc 并补 FORCEIDR 标志——修复运行期关键帧请求被静默忽略
根因(实测确认,非推测)
上游 nvenc 0.1.0 的 encode_picture 只设置 picture_type=NV_ENC_PIC_TYPE_IDR,
却从未设置 encode_pic_flags=NV_ENC_PIC_FLAG_FORCEIDR。NVENC 会据此忽略请求、
继续输出 P 帧。首帧之所以「看起来正常」,是因为 frame_idx==0 本来就强制 IDR,
与该字段无关——这也是该缺陷长期未被发现的原因。
证据(同一 frame_count=4,修复前后)
- 修复前:输出 NAL 首字节 0x61 → 0x61 & 0x1F = 1 = 普通 P 片
- 修复后:输出 NAL 首字节 0x65 → 0x65 & 0x1F = 5 = IDR 片
影响
这解释了构建运行手册 §6.1 记录的「静止桌面黑屏」:NVENC 路径的重编 IDR 从未
真正生效,KeyFrameReq 拿不到 IDR 就无法从解码错误中恢复。
改动
- 新增 vendor/nvenc(上游为 MIT 许可,可自由修改分发,符合商用要求)
- vendor/nvenc/src/safe/encoder.rs:picture_type=IDR 时置 ForceIDR;
IntraRefresh 时置 ForceIntra。改动处有「本项目补丁」注释标记
- agent/Cargo.toml:nvenc 改为 path 依赖指向 vendor 版本
建议后续向上游提 PR(github.com/AlsoSylv/nvenc),以降低长期维护成本。
验证:跳帧后发 KeyFrameReq,第 3 帧即收到 IDR 并解码成功
|
2026-10-28 16:05:41 +08:00 |
|
郑博文
|
9e50bc6237
|
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 新增)
|
2026-10-28 16:05:41 +08:00 |
|
郑博文
|
703eb64fc0
|
merge: 合入 feature/common-punch-signaling(P2P 信令协议 + 打洞探针 + 4 份架构设计文档)
|
2026-10-28 15:14:56 +08:00 |
|
郑博文
|
4533968057
|
merge: 合入 feature/agent-linux(测试期望值修正 + 冒烟脚本纳入单测 + cargo 网络配置文档)
|
2026-10-28 15:14:51 +08:00 |
|
yu
|
18d12baba1
|
fix(tools): 探针区分信令回复与打洞包,修掉打洞假阳性
问题:客户端反复注册引来的 PEER/WAIT 回复在 discover_peer 返回后才到达,
被接收线程当成「对方打进来了」,输出假的「✅ 打洞成功」。
实测形态:报告的对方地址实为信令服务器 47.98.245.66:47999,包长 22 字节
(真打洞包固定 16 字节 = PUNCHv1| + 8 字节时间戳)。
修复:
- 接收线程只认以 MAGIC 开头、且长度正确的包
- 显式排除信令服务器的 IP:端口
- 失败结论里报告被忽略的非打洞包数量,避免再次误读
- 验证记录补 4.5 节,写明 Rust 实现的对应判据
|
2026-10-23 18:02:14 +08:00 |
|
yu
|
2487e616c2
|
docs: 打洞设计/多平台架构/借鉴映射/验证记录
- 打洞与信令设计.md: 无中继方案的流程、成功率边界、Rust 实现约束
- 多平台架构设计.md: 6 层分层 + 平台矩阵 + 六项关键决策 + 分期路线
- 借鉴映射与落地规范.md: 每层看谁/抄什么/不抄什么 + 许可证红线(AGPL 只读)
- 打洞验证记录.md: POC 实测结论(锥形 NAT + 端口保留)与 4 个坑
- README: 文档索引补充
|
2026-10-23 17:32:17 +08:00 |
|
yu
|
d659a4e7f1
|
feat(tools): 打洞探针与极简 rendezvous 服务器(Python POC)
- punch_probe.py: UDP 打洞探针,支持 STUN 查映射、--peer 手工配对、--server/--room 信令配对
- rendezvous.py: 极简 UDP 信令服务器,只做房间配对与地址回推,不承载媒体
- 已在 47.98.245.66 实测跨公网注册/配对可用
|
2026-10-23 17:32:17 +08:00 |
|