Repository navigation
DroidExtend 1.6.3
1.6.3 —— 两个菜单都问错了问题(含 1.6.0 / 1.6.1 / 1.6.2 的全部改动)
用一根 USB 数据线,把安卓手机/平板变成 Windows 的第二块显示器 —— 真扩展桌面,不是投屏镜像,支持触摸反控。
这一次发布一次跨了 四个版本号(1.6.0 / 1.6.1 / 1.6.2 / 1.6.3)。它们不是四件独立的事,而是
同一份清单连着做完的四步:先是用户列的十四条需求,然后是「触控逻辑还是有问题」,
接着是三条收尾的小要求,最后是两个「看着没生效」的菜单。
装这一个安装包就够了,下面四节会一起生效。不想看长文的,看这张表:
| 版本 | 一句话 | 关键数字 |
|---|---|---|
| 1.6.0 | 十四条需求逐条落地,其中八个是真 bug | ffmpeg 自称支持 11 个编码器,实际只有 7 个能跑 |
| 1.6.1 | 触控重做:死区 / 长按右键 / 按帧率节流 | 手机端 91 次 write/秒 → 45 次 |
| 1.6.2 | 安装目录 / 隐私按钮 / 双击打不开文件 | Windows 的双击判定框只有 4×4 像素 |
| 1.6.3 | 分辨率和编码器两个菜单都问错了问题 | 22 个尺寸里 11 个驱动根本没有;11 个编码器里只有 7 个能跑 |
一、1.6.0 —— 十四件被答应下来的事
这一版没有新概念,只有一堆**「本来应该是这样」**。十四条需求逐条落地:
| # | 需求 | 结论 |
|---|---|---|
| ① | 双端隐私协议 + 首次弹窗同意 | 新增,条款版本号是字符串,改条款会重新问 |
| ② | 双端原生动画 | 新增,尊重系统「关闭动画」设置 |
| ③ | 托盘驻留 + 自启动 + 彻底退出 | 纯 ctypes,不引入任何第三方依赖 |
| ④ | 安卓后台保活引导 | 13 个厂商的自启动页,每个都先验证存在 |
| ⑤ | 源码全量开源 | 已满足 |
| ⑥ | 自动更新补全 | 更新日志随包 + 下载位置可改 + 装后删包 |
| ⑦ | 触控偏移 + 关窗后失灵 | 真 bug,两处(见下) |
| ⑧ | 熄屏/被关闭时通知 Windows | 输入协议新增 NOTICE 记录 |
| ⑨ | README 与日志加 emoji | 已满足 |
| ⑩ | 开机未启动就激活虚拟屏 + 位置自定义 | 真 bug(见下) |
| ⑪ | 多编码 + 自动检测与降级 | 真 bug,三处(见下) |
| ⑫ | 卡顿排查 + 显卡占用说明 + 分辨率切不了 | 真 bug(见下) |
| ⑬ | 安装界面与程序本体中英切换 | 安装程序、Windows 端、安卓端三处都能切 |
| ⑭ | adb 通道占用检查 + 驱动安装 | 工具菜单新增「诊断连接问题…」 |
① 编码不再只有 H.264(需求 ⑪)
症状:编码器下拉里有 hevc_nvenc、av1_amf 这些名字,选完也能"连上",但画面是绿的、或者干脆黑屏。
根因有三个,全在打包好的代码里躺着:
- 拼 ffmpeg 命令行时,最后硬编码了一段
-f h264。选任何非 H.264 的编码器,
产出的流都被当成 H.264 拆包 → 单元全是垃圾。 - 切分器只认 H.264 的 NAL 头(1 字节),而 HEVC 的 NAL 头是 2 字节,
于是永远找不到图像单元。 - AV1 的
-profile:v main会让 NVENC 拒绝初始化,报的还是
maybe incorrect parameters such as bit_rate, rate, width or height——
消息里既不提 profile 也不提 codec,只有把-profile:v去掉才好。
顺带发现第四个:旧切分逻辑「连续两个 VCL NAL 就切一帧」会把多 slice 的一帧切成多份,
实测 libx264 的 60 帧被切成 299 个单元。现在按 slice header 的最高位判断,回到正确的 60。
编码器检测也不再靠猜:以前只看 ffmpeg -encoders 列出什么名字,现在会真跑一帧
(合成画面 1 秒)看有没有输出。本机 ffmpeg 自称支持 11 个编码器,实际只有 7 个能用:
| 可用 | 说明 |
|---|---|
h264_nvenc / hevc_nvenc / av1_nvenc |
NVIDIA,首选 |
h264_amf / hevc_amf |
AMD |
libx264 / libx265 |
纯 CPU 兜底 |
不可用:*_qsv(本机没有 Intel GPU,Error creating a MFX session: -9)、
av1_amf(核显不支持 AV1 编码)、libsvtav1(这个 ffmpeg 构建里没有)。
保存的选择失效时会自动回退并在日志里说明,不会再给你一个绿屏。
② 触摸失灵与坐标偏移(需求 ⑦)
症状 A:主副屏位置一变,点哪都偏。
根因是拿虚拟桌面矩形的方式:GetSystemMetrics 对非 per-monitor DPI aware 的进程会说谎 ——
本机实测报 2048x2501,真实是 3200x3736。现在改用实时显示器清单求并集,并加 1 秒 TTL 跟随显示器移动。
症状 B:关掉前台窗口之后,整个手机屏就点不动了,但画面还在跑。
根因是左键卡在按下状态:手指按着时窗口被销毁/手机断开,UP 事件永远不来,
Windows 带着「左键按下」卡在模态拖动循环里,之后所有注入都被吞掉。
现在有三道保险:补发 LEFTUP、按住超过 30 秒的 watchdog 自动解卡、断开与退出时释放。
顺带修掉一个从未生效过的功能:垂直滚动。 输入协议里 z 字段是 uint16,
而手机端把 dx | (dy << 16) 塞进去,host 端用 z >> 16 取 dy ——
16 位变量右移 16 位恒为 0,纵向滚轮从来没动过。协议从 7 字节改到 9 字节(两端一起发,无兼容问题)。
③ 开机没启动程序,虚拟屏却已经在桌上(需求 ⑩)
根因:Windows 把显示配置持久化在
HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity,
其中 Recent 就是开机重建拓扑的依据。挂屏走的是持久化的 API,
而摘屏没有传 SDC_SAVE_TO_DATABASE —— 一正一反不对称:挂屏活过了重启、摘屏没有。
实测往返证据:Recent 从 BOE0D550_26_07E8_EC(干净)→ 挂屏后多出一条
MTT1337518463207 → 摘屏后回到原值。
同时加了防误伤:本机有 21 个显示输出,其中 10 个属于网易 UU 远程。
原来的认屏逻辑按「已连接且有 monitor」排序,UU 一跑就会把它的适配器当成我们的虚拟屏。
现在按驱动身份判定,只认我们自己的 MttVDD。
另外新增工具菜单 「清理残留的虚拟屏」(启动时也会检查一次并在日志里提示,只提示、不擅自动手),
虚拟屏位置也不再只有上下左右 —— 下拉里选「自定义」可以直接填 X / Y(允许重叠)。
④ 改了分辨率没反应(需求 ⑫)
根因:应用显示的代码只在点「开始投屏」时被调用一次,运行中改分辨率没有任何代码
去叫 Windows 换模式 —— 设置存了,但没人执行。
现在改分辨率 / 刷新率 / 位置会立刻应用(600 ms 去抖,因为每次都要重启采集),
最多重试 4 次,并一律以实际生效的模式为准判断成功,而不是「显示器在就算成功」。
(Windows 在每次拓扑变更前后会短暂锁住显示配置,期间相关调用会失败 —— 所以必须重试。)
⑤ 第八个 bug:带 BOM 的设置文件(不在需求清单里)
这一个是打包之后拿真正的安装包做冒烟测试才撞上的。
测试写了一份 "language": "en" 的设置文件,程序起来标题栏却是中文。查下去发现:
PowerShell 的 Set-Content -Encoding UTF8(以及记事本的「UTF-8」选项)会在文件开头
写三个字节 EF BB BF,而读设置时用的是 encoding="utf-8" —— json.load 见到 BOM 直接抛异常,
被 except Exception 当成「文件损坏」吞掉,于是整个设置文件被静默丢弃,一切回到默认值。
这是普通用户完全踩得到的:settings.json 就放在我们请用户去看的那个目录里,
用记事本打开改一个数、保存,所有设置就没了,而且日志里一个字都不说。
修法是两个字符:encoding="utf-8-sig"(自动吃掉 BOM,对没有 BOM 的文件行为完全一致)。
发现它的过程本身也值得记:如果最后没有真的把装好的 exe 跑起来,这个 bug 会原样发出去 ——
因为 17 个测试脚本里没有一个会去写带 BOM 的文件。
⑥ 其余九条
- 隐私协议:双端首次启动弹窗,不同意就退出;条款版本号是字符串,改条款后所有人会被重新询问。
- 原生动画:Windows 端用分层窗口 alpha 淡入淡出(不用
AnimateWindow,
它阻塞调用线程整个时长);安卓端用 Material 的动画 API。尊重系统「关闭动画」设置,低内存设备自动降级。 - 托盘驻留 + 自启动:纯 ctypes 调
Shell_NotifyIconW,不引入任何第三方依赖。
「彻底退出」会摘虚拟屏、清 adb 端口转发、关日志 —— 不留残余。 - 安卓后台保活引导:一键跳到省电白名单,并说明唯一没有公开 Intent 的那一步:在最近任务里下拉锁定。
- 更新:更新日志随程序打包(中英双语,因为 release notes 在断网时看不到)、下载位置可自定义、安装完自动删包。
删安装包必须在安装程序返回之后 —— 删一个还在跑的安装程序就是半装状态的来源。 - 熄屏通知:手机熄屏/亮屏/被系统关闭时主动告诉 Windows。
故意没有「退到后台」通知 —— Android 的onPause/onStop分不清熄屏与切后台,否则你会收到两句说同一件事的话。 - 中英切换:安装程序、Windows 程序、安卓端三处都能切,默认跟随系统。
- adb 诊断:工具菜单新增「诊断连接问题…」,会报端口占用情况、指出最常见的那条原因
(手机插上后停在「仅充电」模式),并提供 USB 驱动安装引导。
二、1.6.1 —— 触控:先把数字量出来
用户的原话是:「触摸逻辑还是有问题,参考一下其他远程控制软件的触控逻辑好吗,
而且为什么触摸延迟比画面延迟还要大」。
为什么触摸延迟一定比画面延迟大
这不是 bug,是恒等式:
你感觉到的触摸延迟 = 手指 → 注入(触摸链路)
+ 注入之后那一帧走完 采集 → 编码 → 传输 → 解码 → 显示
而界面上那个延迟数字只量了后半段。所以它必然更大,问题只在于前半段能不能更小。
三个数字
| 测什么 | 结果 |
|---|---|
| 隧道逐条发 600 条 9 字节记录 | 只到了 22 条,每条间隔 31 ms |
| 同样 600 条一次发出去 | 600 条瞬时全部到达 |
| PC 端注入一次鼠标移动 | 中位 0.457 ms(上限约 2180 次/秒) |
| 手机端旧版本发位置更新的频率 | 约 91 次/秒,比隧道能力超标 3 倍 |
结论很清楚:瓶颈是「每次 write」的开销,不是带宽;PC 端注入完全不是瓶颈;
而手机端 91 次/秒远超隧道能承受的量,拖动时积压会持续增长——
这就是「越拖越滞后」的来源。
改了四处
- 手机端死区:手指落下时那一点点抖动不再被当成拖动。点一下就是点一下。
- 手机端长按 0.5 秒 = 右键,和常见远程控制软件一致。
- 手机端拖动按帧率节流:16 ms 一帧,也就是约每秒 60 次位置。
比下一帧更快的位移人眼看不见,而且不便宜。 - 两端都只留下最新位置:中间那些已经过时的位置不再逐个发出去、也不再逐个注入。
位置是幂等的所以可以丢,事件(按下、抬起、右键、滚动)一个都不丢。
改完的数字
| 改之前 | 改之后 | |
|---|---|---|
| 一次拖动产生的批次 | 95 批 | 38 批 |
| 批次间隔 | ~11 ms(91 次/秒) | ~21–24 ms(约 45 次/秒) |
| 拖动时注入的移动次数 | 每个位置都注入 | 只注入最新的一个 |
顺手发现的一件事:铺满屏幕会吃掉两侧边缘
手机端有个选项叫「让画面铺满屏幕(裁掉多余边缘)」,默认开着。
它按屏幕高度算出画面宽度,在 2136×3200 的机器上是 3797 像素,
而横屏窗口只有 3200 —— 居中的画面被父容器裁掉,两侧各 298.5 像素永远收不到触摸,
也就是 PC 桌面上最左和最右各约 7.9% 点不到(1920 宽的桌面上各约 151 像素,
开始菜单和最大化窗口的关闭按钮都在里面)。
这不是 bug 而是用户可选的偏好,所以默认值一个字符都没动,
只是在文档里把这个代价写清楚了。
三、1.6.2 —— 四件说出口的事,其中一件底下是个真坑
① 更新要装回老目录
程序内更新本来就会传 /DIR="<当前安装目录>",缺口在安装程序自己的目录页:
原来的设置是「永远显示目录页」,于是静默安装时可能落到默认目录,装出第二份程序。
现在改成:只有在安装程序自己找到旧安装时才隐藏目录页并复用那个目录,
首次安装仍然会给你页面让你选。
② 「同意并继续」贴在协议正文最下面
用户的原话是「按钮直接放在协议最下面,不然窗口没拉伸的时候容易找不到同意按钮在哪」。
改动:两个按钮从底部固定的位置移进协议正文的最后一行,跟着文本一起滚;
窗口高度也会按屏幕高度自适应(想要 620,最多不超过屏幕高度减 140,下限 360)。
实测对话框高 620 像素(屏幕 1280),把窗口压到 300 像素高时按钮仍然滚得到。
③ 双击打不开文件 —— 那个真坑
根因不在我们这边,在 Windows 里。 Windows 判定「这是一次双击」的容错框默认只有
4×4 像素(SM_CXDOUBLECLK)。鼠标伸得进去,指尖永远伸不进去 ——
两次点击落点差几十像素,就必然被当成两次独立单击。
我们这边还有一半责任:抬手时发的是手指漂移之后的位置,而死区内最多会漂移几十像素,
正好把落点推出那个本来就很小的双击矩形。
两边各修一半:
- 主机侧:第二次按下如果落在上一次单击的时间窗内(读系统的双击时间,默认 0.5 秒,
夹在 0.1–1.0 秒之间)且容差内(约 1920 宽桌面的 58 像素),
就把这一对的坐标缝到第一次的落点上 —— 两次按下和两次抬起都对齐,Windows 无法不认。 - 手机侧:点击在按下点抬手,只有拖动才跟手指。
配套还有一整套「不许乱缝」的规则:中间发生过拖动、滚轮、右键、或者连接断开,配对立刻作废;
连点三下也有效(第三下继续链在同一个坐标上)。
④ 手机端设置里写明手势
安卓端设置面板里新增「触摸操作」一节,六行手势表:
| 手势 | 效果 |
|---|---|
| 单指点击 | 单击左键 |
| 单指连点两下 | 双击左键 |
| 单指拖动 | 按住左键拖动 |
| 单指长按半秒 | 单击右键 |
| 双指上下滑动 | 滚轮 |
| 双指快速轻点 | 单击右键 |
下面还有三句说明:死区(轻微抖动不算拖动)、按帧率发送且只发最新位置但事件不丢、
连点两下要对齐落点。设置面板本身也包进了可滚动容器 —— 内容变多之后,
「关闭」按钮不能跑到屏幕外面去。
四、1.6.3 —— 两个菜单都问错了问题
这一版修的是你连着问的两件事:「为什么分辨率更改后还是没变化」和「还是有部分视频编码不可用」。
两个都不是「又忘了做」,而是程序问错了问题——一次问的是「驱动应该支持什么」,一次问的是
「这个 ffmpeg 编译进了什么」,两次问的都不是「这台机器上到底行不行」。
① 改了分辨率还是没变化 —— 驱动只认它自己那张表
症状:分辨率下拉里挑一个,点下去什么都没发生。画面不闪、尺寸不变、也没有任何提示。
实测(tools\tmp\res_change_probe.py,真挂虚拟显示器):
set_mode \\.\DISPLAY10 1920x1080@60 -> rc = 0 SUCCESSFUL 1920x1080@60
set_mode \\.\DISPLAY10 2280x1080@60 -> rc = -2 BADMODE 1920x1080@60 (一点没变)
set_mode \\.\DISPLAY10 1920x1080@75 -> rc = -2 BADMODE 1920x1080@60 (一点没变)
set_mode \\.\DISPLAY10 3200x2136@120 -> rc = 0 SUCCESSFUL 3200x2136@120
根因:虚拟显示器驱动的模式表不是「随便什么尺寸都收」。它把
C:\VirtualDisplayDriver\vdd_settings.xml 里每一行 <resolution> 交叉每一档 <g_refresh_rate>
乘成一张平表——本机是 11 行 × 5 档 = 55 个模式。而程序的分辨率下拉是写死的 22 个尺寸,
其中 11 个驱动根本没有:
1600x720 1920x864 2000x1200 2280x1080 2340x1080 2412x1080
2700x1224 2712x1220 2800x1260 3040x1440 3120x1440
刷新率列表里的 75 / 165 / 240 同样不存在(驱动只有 30/60/90/120/144)。
而且失败是静默的,这一点比上面更糟:
retarget()里遇到「驱动没有这个模式」的分支连一句警告都没有;_set_mode_retry()会为一个注定失败的模式重试 4 次,每次sleep(1.0)+sleep(0.5),白等约 9 秒;- 最后还是照样调
_recover_capture("显示模式已更改")把 ffmpeg 重启一遍 —— 画面闪一下、
回来还是原来大小。所以看起来「做了点什么」,其实什么都没做。
修法四条:
host/vdd.py新增declared_modes():直接读驱动的设置文件,返回它会建出来的
(尺寸列表, 刷新率列表)。host/app.py新增_load_resolutions():分辨率下拉和刷新率下拉都由这张表生成,
读不到驱动才回落到原来那份静态列表(那份现在只当兜底,注释里写明白了)。
一个存着的、驱动给不出的尺寸会被换成最接近的、并且说明原因。host/session.py新增_resolve_mode(),统一裁决一次请求:- 精确命中 → 原样用,无提示;
- 同尺寸有别的刷新率 → 取最接近的那档,日志说明「3840x2160 没有 75 Hz,改用 60 Hz」;
- 尺寸整个不在表里 → 报「驱动给不出 2280x1080(模式表里没有,也没能写进去)。现在能用的:…」,
并标记为不可用。
- 不可用时各自做该做的事:
retarget()记一句「显示模式未更改」然后返回False,
不重启画面;apply_display()(点开始投屏那条路)退到wd.best_mode()找一个驱动
真有的模式继续——开始投屏比精确尺寸重要。另外_set_mode_retry()碰到DISP_CHANGE_BADMODE
直接返回,不再白等那 9 秒。
顺带:驱动的模式表还有空位时(上限 MAX_TARGET_MODES = 90,本机用掉 55,还能再加 7 行),
缺的那个尺寸会被写进 vdd_settings.xml 并标注 auto="1",同时告诉你重装一次驱动(或重启)
之后才切得过去。表满时会回收我们自己加的行,绝不动驱动自带的行。
实测(tools\tmp\mode_live_test.py,真挂虚拟显示器走完整链路):
apply_display 2280x1080 -> 驱动给不出 2280x1080(模式表里没有,也没能写进去)。
现在能用的:1280x720、1600x900、1920x1080、1920x1200、2160x1080、…
改用驱动确实有的 2160x1080@60 -> SUCCESSFUL
retarget 2280x1080 -> 显示模式未更改 -> 返回 False,采集没重启,尺寸不变
retarget 3200x2136@120 -> ChangeDisplaySettingsEx SUCCESSFUL -> 返回 True,采集重启
retarget 同一个模式 -> 显示模式没变,无需切换
retarget 3840x2160@75 -> 3840x2160 没有 75 Hz,改用 60 Hz -> SUCCESSFUL
② 还是有部分视频编码不可用 —— 菜单问的是「编译进去了吗」
症状:菜单里选 h264_qsv、av1_amf 这类,选完连不上,或者悄悄回退到别的编码器。
实测(capture.probe_encoder(),每个真编码一帧):
编译进这个 ffmpeg 的:av1_amf av1_nvenc av1_qsv h264_amf h264_nvenc
h264_qsv hevc_amf hevc_nvenc hevc_qsv libx264 libx265 (11 个)
真跑一帧活下来的: h264_nvenc hevc_nvenc av1_nvenc h264_amf hevc_amf
libx264 libx265 (7 个)
死掉的: h264_qsv hevc_qsv av1_qsv -> Error creating a MFX session: -9
av1_amf (Radeon 610M 核显没有 AV1 编码单元)
libsvtav1 (这个构建里根本没编译)
根因:app._load_encoders() 用的是 list_hardware_encoders() —— 回答的是
「这个 ffmpeg 构建里编译进了什么」;而 1.6.0 新加的 available_encoders() 回答的才是
「这台机器上跑得起来什么」。后者加了,前者没换掉,于是菜单照样列 11 个。
host/capture.py 那段 docstring 早就写着这两问答案不同、GUI 不该用前者 —— 写对了,就是没接上。
修法:菜单分两遍填。第一遍仍然是秒填(编译列表,保证窗口一开就有东西可选);
第二遍在后台逐个真编码一帧,回来后重建菜单,只留能跑起来的那几个,
被去掉的会写进日志:「本机跑不动这些编码器,已从列表里去掉: h264_qsv, hevc_qsv, av1_qsv, av1_amf」。
如果当前选中的那个恰好被去掉了,会自动换回一个能用的。
③ 顺手发现的:两个测试其实一直没通过
跑全量回归时才发现 tools/tmp 下有 31 个 *_test.py(不是一直报的 22 个),而且里面有两个红的。
tools\tmp\update_test.py 死在 KeyError: 'cleanup':host\update.py 的 WAIT_CMD 在 1.6.0
加了 {cleanup} 占位符(就是需求里那条「安装完成后自己删除安装包」),测试没跟着改。
也就是说:这个测试从 1.6.0 起每一次都是失败的,而之前一直报的「22 个脚本全绿」
根本没把它算进去。已经修好,并且补了两条本来就该有的断言:
删安装包必须排在 start "" /wait 之后(提前删正是「更新到一半」的成因),自己的 del "%~f0" 必须排最后。
tools\tmp\drag_test.py 则是从一开始就不可能通过。它把手机上的设置按钮拖 13 px,
再断言按钮的位置变化超过 0.1。可是 HudView 里有一条故意的防抖:
if (Math.abs(e.getX() - gearCx) + Math.abs(e.getY() - gearCy) > dp(9)) {
moved = true; // 手指走满 9dp 才算「拖动」
}9dp 在这台机器(密度 352)上是 19.8 px,所以 13 px 的 swipe 会被当成点击,
而点击的功能是打开设置面板——按钮当然不动。就算侥幸算成拖动也没用:13/3068 ≈ 0.004,
断言要的是 > 0.1。实测对比,同一个按钮:
| swipe | 拖后位置(分数) | 位移 |
|---|---|---|
| 13 px | (0.14596808, 0.74855125) | 0.0000 |
| 700 px | (0.37389198, 0.74855125) | 0.2279(预期 0.2282) |
还有第三条、也是最隐蔽的一条:读位置用的正则是 gear=([-\d.]+),([-\d.]+),可 Java 打印小浮点数
会用科学计数法——按钮停在顶边时日志里写的是 gear=0.99984306,2.8117592E-4(真实值是 0.9998
和 0.00028),那个正则只吃到 2.8117592,于是算出「按住屏幕下方 3565 px 的地方」,swipe 自然什么
都没碰到。改成允许指数、把结果夹进 [0, 1],并且按按钮所在的角落选拖动方向(原来写死往右上拖,
而按钮本来就在右上角,等于没得拖),就稳定了。
产品是对的,测试是错的。 已经改成:屏幕尺寸从截图的 PNG 头里读、边距按 wm density 自己算、
拖 700 px 且 x/y 两个方向分别断言,跑完把按钮拖回原处。
升级建议
- 已经中招过的机器(虚拟屏开机自己回来):升级后进工具菜单点一次「清理残留的虚拟屏」。
- 多编码:显卡不支持的编码器现在会从下拉里消失,而不是给你一个黑屏。
如果之前手动选过*_qsv或av1_amf,会自动回退到可用项并在日志里说明。 - 触摸:死区、长按右键、双击、垂直滚动都在这一版修好了。
如果你的手机没响应垂直滚动,这一版也修好了。 - 设置文件:如果你用记事本改过
settings.json,之前的版本会静默丢弃整个文件;
这一版不再有这个问题,但之前丢掉的设置需要重新设一次。 - 升级不会丢目录:从 1.6.2 起,程序内更新与安装程序都会装回原来的目录。
- 分辨率菜单变了:如果你之前手动选过一个驱动给不出的尺寸(比如 2280x1080),
升级后菜单里看不到它了——那是故意的,因为它本来就切不过去。想要那个尺寸就选「自定义」填宽高,
程序会把它写进驱动的模式表,按提示重装一次驱动(或重启)之后就能用。 - 编码器菜单也变了:下拉里只剩这台机器真跑得起来的。之前存的选择如果是跑不动的,
会自动换回一个能用的并在日志里说明。
验证
tools/tmp/ 下 33 个回归脚本全绿、非零退出 0,其中与这四版直接相关的:
encoder_matrix.py 7 个可用编码器全部 PASS(真跑一帧再拆包验证)
touch_test.py 25/25 坐标四角映射、自动解卡、release 语义
touch_batch_test.py 23/23 位置合并只留最新、事件一个不丢、跨接收边界重组
double_click_test.py 27/27 缝合规则与「不许乱缝」的每一条
retarget_test.py 32/32 运行中改分辨率、幂等、重试到第 4 次
install_dir_test.py 23/23 安装程序复用旧目录、/DIR= 与批处理顺序
privacy_layout_test.py 23/23 真 Tk 窗口,按钮在正文里且压到 300 px 仍滚得到
privacy_test.py 32/32 条款文本与版本号
settings_bom_test.py 23/23 带 BOM 的设置文件不再让设置全部丢失
notice_test.py 37/37 含「旧协议下 dy 恒为 0」的证明
db_roundtrip_test.py 4/4 挂屏 → 摘屏往返后 Recent 回到干净值
orphan_cleanup_test.py 摘屏后没有残留的虚拟屏
log_toc_test.py 19/19 两份日志的目录锚点、表格、代码块
mode_menu_test.py 76/76 真 Tk 窗口里两个菜单真的按驱动表 / 真探测结果生成
mode_live_test.py 19/19 真挂虚拟屏,走真的 ChangeDisplaySettingsEx
frozen_check_test.py 解开内嵌 PYZ,查新代码真的被打进 exe 了
drag_test.py 真手机:把设置按钮拖走再重启,看它有没有留在原地
真机验证(小米 24091RPADC,Android 16,2136×3200):
点击 ['DOWN','UP']
长按半秒 ['DOWN','UP','RCLICK'] (长按在按下后 503 ms 触发)
拖动 ['DOWN'] + 27 个 MOVE + ['UP'] (间隔约 22 ms)
抖动点击 ['DOWN','UP'] (一个 MOVE 都没有 —— 死区生效)
连点两下 ['DOWN','UP','DOWN','UP'] (两次按下相隔 125.6 ms,抬起落在按下点)
整条链路端到端(真机 + 真虚拟屏 + 真的注入到 Windows):
capture backend: ddagrab 1920x1080@60
tap 手机(1600,700) → Windows 光标 (3519,353)
swipe (320,900)→(1500,400) → 20 条事件,光标 (3469,202)
logcat: touch action=0 at 1898.0,700.0 / action=1 at 1898.0,700.0 ← 抬手落在按下点
touch action=0 at 618.0,900.0 / action=1 at 1798.0,400.0 ← 拖动跟手指
另外用打包好的 DroidExtend.exe 做了真实冒烟测试(窗口标题正确、日志正常落盘),
并直接读 exe 里的归档确认 20 个自有模块真的被冻了进去 —— 「构建成功」和「东西在里面」是两件事。
校验
| 文件 | 大小 | SHA256 |
|---|---|---|
DroidExtend-Setup-1.6.3.exe |
36559066 bytes | 860A356FBC2460B17506BA2E803B8861B6F7D0148B306616B5569D110C37134A |
DroidExtend.apk |
210774 bytes | D9CDE6FEC2B9F4F3DDA4FFFD0CD8F4D49AC1DFAD15FB26B68979722960533231 |
下载的每一步都要过五道检查(官方域名、官方校验和、字节数、SHA256、文件头是 MZ),
任何一条不过就不装,并当场告诉你哪一条没过。
这个项目没有代码签名证书,安装包始终是「未签名」。程序不会假装它是签过名的:
确认框里直接写明完整性靠校验和保证、不靠签名。校验和能挡什么(下载被篡改、中间人换包、
代理返回半截文件)和挡不住什么(仓库本身被控制)都写在 README 的常见问题里。
已知限制
- 没有声音,只投画面。
- 只在 Windows 11 + NVIDIA(NVENC) 上实测过。代码里有
h264_amf/libx264兜底,
编码器检测也是真跑一帧,但没在 AMD / Intel 机器上验证过。 - 必须装内核级虚拟显示驱动(会弹一次 UAC)—— 这是"真扩展屏"绕不开的前提。
- APK 用自动生成的调试密钥签名,不适合上架应用商店。
完整过程(每个坑的根因、实测数据、以及写错的每一次)记在仓库里:
- 开发日志.md 第 20 节 —— 1.6.0:十四条需求与八个真 bug
- 开发日志.md 第 21 节 —— 1.6.1:触控,先把数字量出来
- 开发日志.md 第 22 节 —— 1.6.2:双击打不开文件,坑在 Windows 里
- 开发日志.md 第 23 节 —— 1.6.3:两个菜单都问错了问题,以及两个从来没通过的测试
- CHANGELOG.en.md —— 同一份日志的英文版
这个项目的全部代码、文档和提交信息,都是 AI 编码代理(DeepSeek Harness / deepseek-flash)写的。
人类负责提需求、插上真机、在需要管理员权限时点「是」,以及验收。