Skip to content

Releases: Blank2007/DroidExtend

DroidExtend 1.6.4

Choose a tag to compare

@Blank2007 Blank2007 released this 17 Oct 06:44

1.6.4 —— 让虚拟屏真的消失,和那个从来装不上的更新(含 1.6.0 – 1.6.3 的全部改动)

用一根 USB 数据线,把安卓手机/平板变成 Windows 的第二块显示器 —— 真扩展桌面,不是投屏镜像,支持触摸反控。

这一次发布一次跨了 五个版本号(1.6.0 / 1.6.1 / 1.6.2 / 1.6.3 / 1.6.4)。它们不是五件独立的事,而是
同一份清单连着做完的五步:先是用户列的十四条需求,然后是「触控逻辑还是有问题」,
接着是三条收尾的小要求,再是两个「看着没生效」的菜单,最后是一块关不掉的屏和一个装不上的更新。

装这一个安装包就够了,下面五节会一起生效。不想看长文的,看这张表:

版本 一句话 关键数字
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.4 虚拟屏关得干净了;程序内更新终于真能装上 count 写 0 没用,禁用设备才消失;DETACHED_PROCESS 下子进程 exit 0 却一行不执行

一、1.6.0 —— 十四件被答应下来的事

这一版没有新概念,只有一堆**「本来应该是这样」**。十四条需求逐条落地:

# 需求 结论
① 双端隐私协议 + 首次弹窗同意 新增,条款版本号是字符串,改条款会重新问
② 双端原生动画 新增,尊重系统「关闭动画」设置
③ 托盘驻留 + 自启动 + 彻底退出 纯 ctypes,不引入任何第三方依赖
④ 安卓后台保活引导 13 个厂商的自启动页,每个都先验证存在
⑤ 源码全量开源 已满足
⑥ 自动更新补全 更新日志随包 + 下载位置可改 + 装后删包
⑦ 触控偏移 + 关窗后失灵 真 bug,两处(见下)
⑧ 熄屏/被关闭时通知 Windows 输入协议新增 NOTICE 记录
⑨ README 与日志加 emoji 已满足
⑩ 开机未启动就激活虚拟屏 + 位置自定义 真 bug(见下)
⑪ 多编码 + 自动检测与降级 真 bug,三处(见下)
⑫ 卡顿排查 + 显卡占用说明 + 分辨率切不了 真 bug(见下)
⑬ 安装界面与程序本体中英切换 安装程序、Windows 端、安卓端三处都能切
⑭ adb 通道占用检查 + 驱动安装 工具菜单新增「诊断连接问题…」

① 编码不再只有 H.264(需求 ⑪)

症状:编码器下拉里有 hevc_nvenc、av1_amf 这些名字,选完也能"连上",但画面是绿的、或者干脆黑屏。

根因有三个,全在打包好的代码里躺着:

  1. 拼 ffmpeg 命令行时,最后硬编码了一段 -f h264。选任何非 H.264 的编码器,
    产出的流都被当成 H.264 拆包 → 单元全是垃圾。
  2. 切分器只认 H.264 的 NAL 头(1 字节),而 HEVC 的 NAL 头是 2 字节,
    于是永远找不到图像单元。
  3. 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 次/秒远超隧道能承受的量,拖动时积压会持续增长——
这就是「越拖越滞后」的来源。

改了四处

  1. 手机端死区:手指落下时那一点点抖动不再被当成拖动。点一下就是点一下。
  2. 手机端长按 0.5 秒 = 右键,和常见远程控制软件一致。
  3. 手机端拖动按帧率节流:16 ms 一帧,也就是约每秒 60 次位置。
    比下一帧更快的位移人眼看不见,而且不便宜。
  4. 两端都只留下最新位置:中间那些已经过时的位置不再逐个发出去、也不再逐个注入。
    位置是幂等的所以可以丢,事件(按下、抬起、右键、滚动)一个都不丢。

改完的数字

改之前 改之后
一次拖动产生的批次 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 重启一遍 —— 画面闪一下、
    回来还是原来大小。所以看起来「做了点什么」,其实什么都没做。

修法四条:

  1. host/vdd.py 新增 declared_modes():直接读驱动的设置文件,返回它会建出来的
    (尺寸列表, 刷新率列表)。
  2. host/app.py 新增 _load_resolutions():分辨率下拉和刷新率下拉都由这张表生成,
    读不到驱动才回落到原来那份静态列表(那份现在只当兜底,注释里写明白了)。
    一个存着的、驱动给不出的尺寸会被换成最接近的、并且说明原因。
  3. host/session.py 新增 _resolve_mode(),统一裁决一次请求:
    • 精确命中 → 原样用,无提示;
    • 同尺寸有别的刷新率 → 取最接近的那档,日志说明「3840x2160 没有 75 Hz,改用 60 Hz」;
    • 尺寸整个不在表里 → 报「驱动给不出 2280x1080(模式表里没有,也没能写进去)。现在能用的:…」,
      并标记为不可用。
  4. 不可用时各自做该做的事: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...
Read more

DroidExtend 1.6.3

Choose a tag to compare

@Blank2007 Blank2007 released this 16 Oct 16:14

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 这些名字,选完也能"连上",但画面是绿的、或者干脆黑屏。

根因有三个,全在打包好的代码里躺着:

  1. 拼 ffmpeg 命令行时,最后硬编码了一段 -f h264。选任何非 H.264 的编码器,
    产出的流都被当成 H.264 拆包 → 单元全是垃圾。
  2. 切分器只认 H.264 的 NAL 头(1 字节),而 HEVC 的 NAL 头是 2 字节,
    于是永远找不到图像单元。
  3. 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 次/秒远超隧道能承受的量,拖动时积压会持续增长——
这就是「越拖越滞后」的来源。

改了四处

  1. 手机端死区:手指落下时那一点点抖动不再被当成拖动。点一下就是点一下。
  2. 手机端长按 0.5 秒 = 右键,和常见远程控制软件一致。
  3. 手机端拖动按帧率节流:16 ms 一帧,也就是约每秒 60 次位置。
    比下一帧更快的位移人眼看不见,而且不便宜。
  4. 两端都只留下最新位置:中间那些已经过时的位置不再逐个发出去、也不再逐个注入。
    位置是幂等的所以可以丢,事件(按下、抬起、右键、滚动)一个都不丢。

改完的数字

改之前 改之后
一次拖动产生的批次 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 重启一遍 —— 画面闪一下、
    回来还是原来大小。所以看起来「做了点什么」,其实什么都没做。

修法四条:

  1. host/vdd.py 新增 declared_modes():直接读驱动的设置文件,返回它会建出来的
    (尺寸列表, 刷新率列表)。
  2. host/app.py 新增 _load_resolutions():分辨率下拉和刷新率下拉都由这张表生成,
    读不到驱动才回落到原来那份静态列表(那份现在只当兜底,注释里写明白了)。
    一个存着的、驱动给不出的尺寸会被换成最接近的、并且说明原因。
  3. host/session.py 新增 _resolve_mode(),统一裁决一次请求:
    • 精确命中 → 原样用,无提示;
    • 同尺寸有别的刷新率 → 取最接近的那档,日志说明「3840x2160 没有 75 Hz,改用 60 Hz」;
    • 尺寸整个不在表里 → 报「驱动给不出 2280x1080(模式表里没有,也没能写进去)。现在能用的:…」,
      并标记为不可用。
  4. 不可用时各自做该做的事: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

② 还是有部分视频编码...

Read more

DroidExtend 1.5.1

Choose a tag to compare

@Blank2007 Blank2007 released this 12 Oct 13:29

1.5.1 —— 投屏点不动的那两天,是驱动的一张表撑爆了

症状

驱动装得好好的:设备管理器里 MttVDD 在,Get-PnpDevice 报 Status=OK、ProblemCode=0,
DLL 和安装包里的哈希一致。但只要一点「开始投屏」就失败:

虚拟显示器: \\.\DISPLAY21 (Virtual Display Driver)
  激活显示器 attach rc=-1
  扩展拓扑 rc=31
RuntimeError: 虚拟显示器 \\.\DISPLAY21 无法激活(未上桌面)

重启驱动、重启电脑都没用。

根因

驱动把 vdd_settings.xml 里每一条分辨率和每一档全局刷新率相乘,铺成一张平表交给
Windows,而这张表存在一个固定大小的缓冲区里。溢出的那一声是驱动日志里的

[WARNING] Input buffer is too small for target modes.
[ERROR] Failed to report monitor arrival. Status: -1073741637

-1073741637 就是 STATUS_NOT_SUPPORTED:适配器起来了、监视器对象也创建了,
只有 IddCxMonitorArrival 被系统拒绝——于是桌面上根本没有这块屏,之后任何
「设置分辨率 / 接上桌面」的调用都注定失败。

而随安装包发出去的 vdd_settings.xml 是 22 条分辨率 × 5 档刷新率 = 110 种组合。
上游 issue #254
记录得很清楚:108 种组合失败,同一个文件删到 96 种就成功。

那 22 条全是手机/平板面板尺寸,是我为了「跟随手机分辨率」一条条加进去的——每一条都有理由,
唯独没有人做过加法。

修复

  1. 模式表从 22 条砍到 11 条 × 5 档 = 55 种组合。 砍掉的是重复的近似尺寸
    (1600x720 / 1920x864 / 2000x1200 这类),留下的仍然覆盖常见 16:9 / 16:10 / 21:9
    以及主流手机平板面板。
  2. host/vdd.py 加了硬上限 MAX_TARGET_MODES = 90。 add_resolution()
    在写入前先算还剩几行位置;满了就删掉我们自己加的那一条(标了 auto="1"),
    驱动自带的条目一条都不碰。宁可少一个尺寸,也不能再交出驱动接不住的表。
  3. 安装脚本会自愈。 tools/vdd_install.ps1 在安装时解析 vdd_settings.xml,
    发现超过 90 种组合就把随包的那份覆盖回去并写进日志。
    已经中招的机器,点一次「安装虚拟显示器驱动(需要管理员)」就好。

已经中招了怎么办

点一次 「安装虚拟显示器驱动(需要管理员)」,UAC 点“是”,驱动设备会重启,屏幕就回来了。
(不想升级也可以手动把 C:\VirtualDisplayDriver\vdd_settings.xml 里
<resolutions> 下的条目删到 18 条以内,然后重启驱动设备。)

验证

修好后:

state   : ready
adapter : \\.\DISPLAY46 | Virtual Display Driver
monitors: 1
   \\.\DISPLAY1           2560x1600  primary=True
   \\.\DISPLAY46          1280x720   primary=False
best_mode -> (2560, 1600, 144, True)

完整端到端(虚拟屏 → ddagrab → adb reverse → 手机):

live: ['\\.\DISPLAY1 2560x1600@300+0+0', '\\.\DISPLAY46 1920x1080@60+2560+0']
ddagrab 输出: #0=2560x1600, #1=1920x1080
已发送握手 1920x1080@60
perf fps=60 mbps=2.14 latency_avg_ms=10 latency_max_ms=42
FINAL capture: frames=1604 bytes=7488821 fps=56.3

一并说清楚

这一版的教训写在 开发日志.md 第 19 节:「文件是我生成的」不等于「文件是对的」,
而 resolutions 和 g_refresh_rate 在 XML 里看着像"二选一",实际是"全都乘一遍"。
另外,这类失败在界面上没有任何提示——真正的原因只在默认关闭的驱动日志里,
所以排查时先去驱动包里找日志开关,别在驱动编号一路上涨这条线索上浪费时间
(那是症状,不是原因)。

校验

文件 大小 SHA256
DroidExtend-Setup-1.5.1.exe 36444826 bytes 0979ce0da99713fd2d9e59c12a602387f06580603c9e6b1689d90d708a5cd8e9
DroidExtend.apk 194390 bytes 0b7dbe3418e9b67c68dd33ad77b6b3569d1a645793a7b4ab6ff98e90adc8cf52

DroidExtend 1.5.0

Choose a tag to compare

@Blank2007 Blank2007 released this 12 Oct 10:16

1.5.0 —— 更新改成在程序里做,而不是把你丢给浏览器

以前的「检查更新」只会弹一句"有新版本",然后打开浏览器。剩下的全靠你自己:
判断该下哪个文件、下完对不对、装到哪。这一版把整条路走完。

在程序里下载、校验、安装

工具 → 检查更新 → 有新版本时列出更新说明和安装包大小 → 点「下载并安装」,
带进度条下载完后再确认一次,然后程序自己退出、安装、重新启动。

下载的每一步都要过五道检查,任何一条不过就不装,并当场告诉你哪一条没过:

  • 地址必须是 GitHub 官方域名(https),重定向之后的地址要重新检查一遍
  • 官方必须给出校验和(优先取 GitHub API 的 digest 字段,其次取 release 正文里的校验表)
  • 字节数要与官方一致(先对传输头,落盘后再对一次)
  • SHA256 必须与官方一致
  • 文件头必须是 MZ(可执行文件)

最后一条不是形式主义:测试里拿 APK 冒充安装包时,只要把校验和一起改成 APK 的,
前四条全能过——只有 MZ 能拦住。

关于签名,说实话

这个项目没有代码签名证书,安装包始终是「未签名」。程序不会假装它是签过名的:
确认框里直接写明完整性靠校验和保证、不靠签名。README 的常见问题里也写清楚了
校验和能挡什么(下载被篡改、中间人换包、代理返回半截文件),
以及挡不住什么(仓库本身被控制)。

安装包也换了个方式落地

正在运行的程序没法覆盖自己的 exe,所以更新时会写一个临时批处理:等你原来的进程
真的退出后,用 /SILENT /NORESTART /DIR="<当前安装目录>" 安装,
再把新版本拉起来。/DIR 是写死的——万一注册表里那条安装记录丢了,
更新会静默装出第二份程序,桌面快捷方式还指着旧的那份,用户只会觉得"更新没生效"。

另外

  • 更新说明现在会渲染成正常文字(标题、加粗、链接),不再把 Markdown 语法原样糊脸上
  • README.md 的安装包链接改成指向 releases 页面(安装包文件名带版本号,直链每版都会失效)
  • 修复 release.py 上传大文件的问题:urllib 会去读 Windows 注册表里的代理设置,
    上传 36 MB 安装包时被本机代理掐断([Errno 10054]),现在显式不使用代理

CI 补上了

仓库门面(issue 模板、CONTRIBUTING、SECURITY、LICENSE/NOTICE)之后,CI 也推上去了
(之前缺 token 权限)。两个 job:

  • 静态检查 —— compileall;版本号三处一致(host/version.py、安装包 #define AppVer、
    AndroidManifest.xml 的 versionName)——这一条正是当初让四个发布版版本号漂移的同类问题;
    禁止写死开发机路径(就是 issue #1 的根因);PowerShell 脚本语法
  • 安卓端能编译 —— 起 JDK 17,下 build-tools_r34-windows.zip 与 platform-34-ext7_r03.zip,
    aapt2 生成 R.java 后用 javac -bootclasspath android.jar 编译全部 8 个 java 文件

仍未完成

  • 安装包仍无代码签名(同上,已如实写在程序的更新确认框里)
  • 更新不做差分下载,每次完整下载约 35 MB
  • CI 只证明"能编译",不产出 APK、不跑测试——仓库里 40 多个探针脚本都依赖真机,上不了 CI

感谢 @Bai-2008 在 issue #1
里那份写得非常细的报告——v1.1.0–v1.4.0 的「安装虚拟显示器驱动」按钮对所有用户都失效,
是他找出来的。

DroidExtend 1.4.1

Choose a tag to compare

@Blank2007 Blank2007 released this 12 Oct 06:00

1.4.1 — 修复一个把前四个版本全废掉的 bug

感谢 @Bai-2008:这个 bug 是他找出来的,
issue #1 里那份排查报告写得很细,省了我大量时间。

这个 bug 有多严重

v1.1.0、v1.2.0、v1.3.0、v1.4.0 四个发布版,「安装虚拟显示器驱动(需要管理员)」
按钮对任何人都不起作用。

点下去界面什么都不显示,驱动永远装不上,于是虚拟屏造不出来 —— 也就是说,
用安装包安装的人,从来没真正用起来过这个程序。作者自己在开发机上一直是好的,
因为坏掉的那行代码里写的就是作者那台机器的路径。

出了什么事

tools/vdd_install.ps1 的四个参数默认值是开发机上的绝对路径:

param([string]$Src = 'D:\dsh projects\droid-extend\tools\vdd\...')

安装包把这个脚本装到 {app}\tools\,程序调用它时一个参数都不传,
脚本就去找那个只存在于开发机上的目录,报 source missing 并以退出码 3 结束。
而提权子进程有自己的控制台,stdout 拿不到,日志又写去了别处,
所以界面上表现为「点了没反应」。

修法:默认值全部改成 Join-Path $PSScriptRoot '...',装到哪儿就解析到哪儿。
另外补了三个真实缺陷:拷贝后数一遍目标目录(不再只看它有没有报错)、
日志写入失败不再被空的 catch {} 吞掉、路径改用 -LiteralPath。

顺手做完的事(都来自同一份报告)

  • tools/ 下 24 个探针脚本 + disp_probe.cs + sdk_manifest.json 里写死的开发机路径
    全部改成基于脚本自身位置推导。
  • CI 加了守卫:host/、android/、tools/ 里再出现开发机路径直接红。
    这类 bug 只靠 review 是抓不住的。
  • licenses:LICENSE 拆成纯 MIT(之前那段附加声明让 GitHub 判成 NOASSERTION),
    新增 NOTICE 说明第三方组件与 AI 编写情况。
  • 新增 .github/(issue / PR 模板、CONTRIBUTING.md、SECURITY.md)、.editorconfig。
  • tools/release.py 现在也会递增 AndroidManifest.xml 的 versionCode ——
    Android 拒绝安装 versionCode 没变大的更新,之前这步是漏的。
  • 仓库 homepage 不再指向它自己;Discussions 按 README 的承诺打开了。

升级建议

v1.4.0 及以前的所有版本,如果你需要手动装虚拟显示驱动,请务必升到这版。
如果你已经在用(驱动是更早手动装好的),这次更新本身也不会破坏任何东西。


1.4.0 — 卡死自愈 / 数据目录 / 手机端重画

(本版内容,供对照)

  • 画面卡死会自愈了。 在 Windows「显示设置」里改虚拟屏参数、或某个软件弹 UAC 提权时,
    桌面复制(ddagrab)会以 DXGI_ERROR_ACCESS_LOST 失效。现在有一个 3 秒静默看门狗,
    检测到流断掉就重读屏幕矩形、重启采集,并让手机重连解码器。实测恢复 8.1 秒 → 4.1 秒。
    顺带的好处:改完分辨率不用再点一次「开始投屏」。
  • 设置和日志放在一起,就在你安装时选的目录里。 文件 → 更改数据目录… 可以改;
    写不进去时回落到 %LOCALAPPDATA%\DroidExtend。1.4.0 之前的设置会自动搬过来。
  • 手机端重画:状态浮窗换成浅色小字(不再挡视线),设置按钮可以拖动(位置会记住),
    清空日志有「已清空」提示,图标重画。
  • APK 122 KB → 190 KB,新增自适应图标。

DroidExtend 1.4.0

Choose a tag to compare

@Blank2007 Blank2007 released this 11 Oct 14:18

手机端

  • 状态浮窗改成浅色小字(帧率 / 码率 / 延迟三行),不再是一大块半透明黑底挡画面
  • 右上角设置按钮可以拖动到任意位置,位置会记住
  • 清空日志后有「已清空日志」提示
  • 应用图标重画

Windows 端

  • 修掉「在 Windows 显示设置里改虚拟屏参数,或软件弹 UAC 提权窗口之后,画面永久卡住」的问题。根因是桌面 DXGI 输出作废(AcquireNextFrame failed: 887a0026),而 ffmpeg 此时返回码是 0;现在改成静默 3 秒判卡死并自动重建采集、让手机重连,虚拟屏真的没了就停下并提示,不再假装还连着。
  • 顺带把虚拟屏的识别速度提上来,卡死恢复从 8.1 秒缩到 4.1 秒。
  • 设置文件和日志文件现在放在同一个目录(默认就是你安装时选的那个目录),菜单里可以随时改;1.4.0 之前的旧设置会自动搬过去。

安装包:DroidExtend-Setup-1.4.0.exe(装完即用,自带 ffmpeg / adb / 虚拟显示驱动)
手机端:DroidExtend.apk

完整过程与实测数据见仓库里的 开发日志.md 第 16 节。

DroidExtend 1.3.0

Choose a tag to compare

@Blank2007 Blank2007 released this 11 Oct 06:29

用一根 USB 数据线,把安卓手机变成 Windows 的第二块显示器 —— 是真的扩展桌面,
不是投屏镜像。

这一版几乎全在手机端,外加一处让画面真的「撑满」的能力。

手机端

  • 有图标、有界面了。 图标是 Pillow 一个像素一个像素画的(蓝色圆角方块 + 白色显示器 + 白色手机),
    含自适应图标前景层,mdpi 到 xxxhdpi 五种密度;以前 manifest 里根本没有 android:icon,
    桌面显示的一直是系统默认图标。
  • 实时状态浮窗:左上角半透明面板,显示 分辨率 / 帧率 / 码率 / 延迟(含峰值)/ 画质,
    延迟按 18 ms / 40 ms 两档染色(绿 → 琥珀 → 红),追帧时额外标出来。
    整个浮窗是 onDraw 手绘的(没有 layout XML、没有每秒两次的 inflate),
    只在右上角齿轮上吃触摸,其它位置的点按照常传给电脑。
  • 设置面板:点齿轮打开,三个开关 —— 状态浮窗 / 让画面铺满屏幕(裁掉多余边缘)/ 把日志写到文件。
    选择记在 SharedPreferences 里,重开 App 还在。
  • 日志改成可选:默认只写 logcat;打开开关后写到
    Android/data/com.droidextend/files/DroidExtend.log(不需要存储权限),
    面板里显示完整路径并给一个「清空日志」按钮。

电脑端

  • 虚拟屏跟着手机分辨率走:启动时读手机的物理分辨率(wm size),从驱动模式表里挑一个
    长宽比完全相同的模式撑满整块屏。驱动分辨率表从 5 条扩到 22 条
    (含 3200x2136、2712x1220、1600x720 这些常见机型横屏尺寸);表里真没有就把手机分辨率写进
    C:\VirtualDisplayDriver\vdd_settings.xml,下次重装驱动或重启后生效。
    界面上也加了「跟随手机分辨率」勾选框。
  • 认屏不再慢:上一版靠抓缩略图逐像素比对确认哪块屏是虚拟屏,但缩略图按 1 fps 抓、
    还要跳过每路流必黑的第 0 帧,三次调用要等 9 秒;改成 30 fps 之后降到 1 秒。

实测

tools\exe_e2e.py 对着装好的冻结 exe 跑完整生命周期,全部通过:

  • 点「开始投屏」→ 1 秒后虚拟屏出现在系统里
  • 11 秒后触摸通道就绪(大头是 adb install 装 APK)
  • 触摸注入:Windows 光标从 (1280, 800) 移到 (383, 2720),与理论值偏差 1 像素
    (虚拟屏被放在主屏下方,所以是 2720 而不是 756)
  • 点「停止」→ 1 秒内虚拟屏从系统里消失,桌面边界回到 2560x1600

手机端实拍:画面铺满整块屏,浮窗显示 2560 × 1600 / 89.4 fps / 6.25 Mbps / 延迟 6 ms(峰值 22)/ NVENC VBR Q18 · 40Mbps。

下载

  • DroidExtend-Setup-1.3.0.exe —— Windows 安装程序(35 MB,内含 ffmpeg / adb / 虚拟显示驱动 / APK)
  • DroidExtend.apk —— 手机端 App(122 KB,也可以由电脑端自动安装)

装好之后启动程序,点界面上的「安装虚拟显示器驱动(需要管理员)」,UAC 弹窗点「是」——只需要装一次。


这个项目的全部代码、文档和提交信息,都是 AI 编码代理(DeepSeek Harness / deepseek-flash)写的。
人类负责提需求、插上真机、在需要管理员权限时点「是」,以及验收。
整个过程(包括踩过的每一个坑和实测数据)记在仓库的 开发日志.md 里。

DroidExtend 1.2.0

Choose a tag to compare

@Blank2007 Blank2007 released this 10 Oct 15:48

DroidExtend 1.2.0

用一根 USB 数据线,把安卓手机 / 平板变成 Windows 的第二块显示器 —— 真扩展桌面,不是投屏镜像,支持触摸反控。

这一版加了什么

  • 参数记忆:分辨率 / 帧率 / 码率 / 编码器 / 模式等设置会存到 %USERPROFILE%\.droidextend\settings.json,下次打开还是上次的参数。写盘用的是临时文件 + 原子替换,断电也不会把配置写坏。
  • 检查更新:启动时自动检查(可以关掉),也可以从 工具 → 检查更新… 手动查。
  • 软件内使用说明:帮助 → 使用说明 里有完整中文说明(九节),不用再翻 README。
  • 界面整理:加了菜单栏、日志工具条(复制全部 / 另存为… / 清空)、底部状态栏,窗口大小和位置也会记住。
  • 第三方许可:安装目录里放了《第三方组件许可.txt》,逐条列出用到的开源组件与许可证全文。
  • 认屏更保险:ddagrab 的输出索引以前只按分辨率匹配,主屏和虚拟屏分辨率相同时会认成主屏。现在多个候选会逐像素比对内容,认不出来就退回 gdigrab(按坐标抓,必然正确),绝不猜。
  • 修掉了启动后头两秒点「开始投屏」会弹「请稍候」然后什么都不做的问题。

下载

  • Windows:DroidExtend-Setup-1.2.0.exe(约 34 MB,装完约 116 MB)
  • 安卓端:DroidExtend.apk(安装包里已经自带,一般不用单独下载)

安装程序第一个页面就是「选择安装位置」,想换目录点旁边的「浏览…」。虚拟显示器驱动需要管理员权限,装完之后在程序里点「安装虚拟显示器驱动(需要管理员)」就行。


全部代码由 AI 编码代理(DeepSeek Harness / deepseek-flash)编写,人类负责提需求和验收。

DroidExtend 1.1.0

Choose a tag to compare

@Blank2007 Blank2007 released this 10 Oct 06:33

用一根 USB 数据线,把安卓手机/平板变成 Windows 的第二块显示器 —— 真扩展桌面,不是投屏镜像。

🤖 本版本的全部源代码由 AI 编码代理编写(DeepSeek Harness / deepseek-flash 模型,2026-10-10)。
人类负责提需求、插真机、点 UAC 和验收。过程记录见仓库里的 开发日志.md。

下载

文件 说明
DroidExtend-Setup-1.1.0.exe(34 MB) Windows 安装程序。内含 ffmpeg、adb、虚拟显示驱动文件和手机端 APK,装完不需要再准备任何东西。装到 %LOCALAPPDATA%\Programs\DroidExtend,第一个页面就能自己改路径,全程不请求管理员权限。
DroidExtend.apk(21 KB) 安卓端 App,装到手机上。

装完之后:① 打开程序,点「安装虚拟显示器驱动(需要管理员)」并同意 UAC(只需一次)
② 手机开 USB 调试并插上数据线 ③ 点「开始投屏」,把窗口往手机那个方向拖过去。

1.1.0 修了什么

  • 拓展屏模式不会再传成主屏画面。 1.0.0 是拿分辨率去认屏的 —— 笔记本屏和虚拟屏分辨率
    相同的时候(1920×1080 的本子最典型)会静默选中「0 号输出」=笔记本自己的屏幕。
    现在多个输出尺寸相同时会抓缩略图逐像素比对,认不出来就退回 gdigrab 按桌面坐标抓
    (慢一点但永远是对的),绝不猜。
  • 安装程序第一个页面就能自己选安装路径。 原来 DisableDirPage=auto 在检测到已安装过时
    会静默沿用旧目录、把整页跳过,用户根本看不到这个选项。
  • 顺手:虚拟屏空着的时候,手机上看就是同一张壁纸加一条任务栏,很容易被误认为在传主屏 ——
    现在日志会直接标出虚拟屏的桌面坐标,告诉你要把窗口往哪个方向拖。

实测数据

项目 结果
端到端延迟 5–11 ms 平均、26–45 ms 最大(修复前 31–42 / 56–79)
1080p60 连续投屏 sent 1482 帧 dropped 0,手机端 Render 303 Drop 0
触摸反控 归一化坐标映射到 Windows 绝对坐标,落点误差 1 像素
点停止 / 拔线 虚拟屏 1 秒内从系统里消失,桌面拓扑干净复原
USB 链路 好线实测 597 Mbps(71 MB/s,超过 USB 2.0 上限,属 USB 3 级别)

已知限制

  • 没有声音,只投画面。
  • 只在 Windows 11 + NVIDIA(NVENC) 上实测过。代码里有 h264_amf / libx264 兜底,
    驱动安装脚本也是通用的,但没在 AMD / Intel 机器上验证过。
  • 必须装内核级虚拟显示驱动(会弹一次 UAC)—— 这是"真扩展屏"绕不开的前提。
  • APK 用自动生成的调试密钥签名,不适合上架应用商店。