开源 DeskPeek:把手机变成自己电脑的小监视器 一个 Windows 单文件小工具手机浏览器直接看自己电脑的屏幕直播需要时还能点一下。手机上不装 App、没有账号、不要配对码。一、为什么要做这个场景很具体电脑上正跑着一个十几分钟才出结果的活——AI 编程 agent 在改代码、dotnet build在编、几十 GB 的东西在下载、某个脚本在等回调。人已经从书桌挪到沙发上了但手边只有手机而你就是想瞄一眼到哪一步了是不是卡住了要不要点那个继续。我先把现成方案都试了一圈没有一个合这个尺度的需求方案为什么没选RustDesk / ToDesk / TeamViewer两端都要装要账号或配对码。我只要看一眼 点一下用不上完整桌面串流Windows 远程桌面家庭版根本不带 RDP 服务端就算带手机上是按键盘鼠标设计的交互触摸很难受VNC同样要装服务端手机还得装 Viewer定时截图刷新网页能看不能点延迟难看于是写了DeskPeek电脑上跑一个小 exe手机浏览器打开一个网址就完事。现在用得最多的功能就是第一条场景——躺着盯 AI agent 在电脑上干活需要点接受 / 继续 / 运行的时候从手机上点一下。二、它能做什么不能做什么能看整块虚拟屏幕含多显示器实时直播帧率 1–30fps默认 10、JPEG 质量 10–100默认 60、可缩到 1280 宽省流量默认开点手机点一下 电脑左键单击一次。要显式开启且必须设访问口令稳断流自动重连直播期间阻止系统睡眠防火墙规则跟着端口走走可以顺手把服务通过 frp 转到公网自备 frpc软件负责写配置和拉起不能说在前面比让用户自己撞到好只有左键单击。没有键盘、拖拽、右键、滚轮锁屏界面和 UAC 提权弹窗点不了—— Windows 不允许向安全桌面注入输入这是系统级限制不是 bug没有声音、没有多路画面、没有文件传输只跑在 Windows 上抓屏用的 GDI手机端不需要装任何东西三、技术选型为什么是 MJPEG整个链路一句话就能说完GDI 抓屏 → GDI 压 JPEG → multipart/x-mixed-replace 一路吐 → 手机 img 直接播MJPEG 最被低估的点是img src/stream这一行不需要任何 JavaScript、不需要播放器、不需要信令服务器。浏览器原生就认一个持续替换画面的图片响应这是 WebRTC 和 WebSocket 都换不来的省事。被问过要不要上更现代的方案都比一比方案真实代价WebRTC信令服务 ICE/TURN SRTP 编解码协商。为瞄一眼太贵WebSocket canvas要么在 JS 里自己解码 JPEG要么每帧转 blob URL心跳、重连、背压全得自己写——而这些为了 MJPEG 我照样要写截图轮询每帧一次 HTTP 请求延迟和流量都不好看DXGI Desktop Duplication帧率上限更高但要引 D3D11 互操作还要处理输出复制的独占语义。10fps 够用的场景不值得服务端就是.NET Framework 4.7.2 WinForms HttpListener零第三方依赖产物一百多 KB 一个 exe。选 Framework 而不是 .NET 8 的理由非常现实系统自带运行时不用打包 60MB 的 self-contained而 WinForms 设计器拖控件改界面比手写布局代码快一个数量级——这种小工具的界面改动极其频繁。四、真正花时间的地方九个坑功能本身一天就跑起来了剩下的时间全在处理为什么手机上看着不对。这部分才是我觉得值得写出来的。4.1 有时候连不上其实是电脑睡了不是 bug是电源策略。机器进了 S3 之后网络栈就没了手机当然打不开而现象呈现为时好时坏特别像代码问题。现在服务运行期间会调用SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);只拦系统睡眠不碰显示器电源策略ES_DISPLAY_REQUIRED会把屏幕也钉住那是在替你费电。停止服务或退出程序立刻恢复。4.2 MJPEG 断了浏览器不会自己重连这条最坑Wi-Fi 抖一下、电脑睡醒一次、手机切到后台流都会断。而断了之后画面永久停在最后一帧用户看到的结论就是连不上可服务端其实活得好好的。浏览器对multipart/x-mixed-replace没有任何自动恢复机制所以只能自己做每次开流分配一个 generation token回调里比对避免旧流的重试把新流干掉onload后 10 秒内没等到新帧就判定断开帧率下限 1fps10 秒足以区分卡和断按 1s → 2s → 4s → 8s 指数退避重连visibilitychange切后台停止重试回前台立即重开请求 URL 带时间戳。URL 一模一样的话有些浏览器会复用那条已经死掉的连接4.3 防火墙规则不跟端口走Windows 防火墙按程序放行看着够用但只要换端口、或者网卡被归到 Public profile就会出现电脑这边一切正常、手机就是打不开。现在启动时按当前端口重建自己那条入站规则DeskPeek-ScreenLive先删再加避免残留旧端口。而且绝不去动用户自己建的规则。netsh的中文输出没法可靠解析所以只判断退出码失败就在状态行追加一句防火墙放行失败请手动放行 TCP xxxx。不弹窗打断也不静默失败。4.4 DPI 缩放吃掉画面右下角系统 150% 缩放时进程如果不声明 DPI 感知SystemInformation.VirtualScreen拿到的是缩放后的逻辑尺寸抓出来的图右下各缺一块。在app.manifest里声明dpiAwarenessSystem之后拿到的才是物理像素另外用GetDeviceCaps(DESKTOPHORZRES / DESKTOPVERTRES)校验一次。4.5 多显示器负坐标点击落点跑偏副屏在主屏左边时VirtualScreen.X是负数。点击控制一开始传的是像素坐标结果一开缩小到 1280 宽、或者手机横竖屏一换落点就飘。最后改成传 0~1 的画面比例像素留给服务端算int x vs.X (int)Math.Round(fx * (vs.Width - 1)); // vs SystemInformation.VirtualScreen这样缩放开关、系统 DPI、多屏原点全都跟它无关——因为Cursor.Position和VirtualScreen本来就是同一个坐标空间。4.6 iPhone Safari 不支持屏幕旋转得用 CSS 兜底Android Chrome 走requestFullscreen()screen.orientation.lock(landscape)就完事了。iOS Safari 没有 Fullscreen API只能把整个画面容器transform: rotate(90deg)硬画出来。代价是触摸坐标要做轴交换——旋转 90° 之后视口轴和图片轴互换了if (rotated) { x sy / r.height; y 1 - sx / r.width; }else { x sx / r.width; y sy / r.height; }这行公式是拿浏览器自己的 transform matrix 反算验证过的不是猜的。另外 iOS 的100vh不随横屏和工具栏收放变化所以布局一律用window.visualViewport的真实尺寸。4.7 横屏之后回不到竖屏第一版横屏时把顶部工具栏visibility:hidden掉了自作聪明觉得这样沉浸。结果走 CSS 兜底的机器一进横屏就没有任何按钮可点永久回不去只能杀掉页面重来。现在横屏时工具栏变成浮在左上角的半透明小条避开刘海安全区还原和触控随时点得到退出横屏时额外调一次screen.orientation.unlock()因为有些浏览器退出全屏后方向锁定还留着。4.8 微信内置浏览器不弹认证框设了访问口令走的是 HTTP Basic。桌面浏览器会弹登录框但微信这类内置浏览器不允许弹于是它把我们 401 的正文当成网页内容直接显示出来——用户看到的是屏幕上孤零零一行401 需要访问口令然后来问我为什么连不上。解法是老老实实加一个登录页/未登录时返回一个正常 HTML 表单POST 到/login校验通过发一个随机 32 位 hex 的HttpOnly会话 cookie12 小时滑动续期整夜看不会被踢下线之后img的长连接和fetch(/click)都自动带上它Basic 认证同时保留桌面浏览器里存过口令的体验不变不再发WWW-Authenticate挑战否则某些浏览器还是会去弹那个它弹不出来的框页面上还加了兜底断流重连和点击都会探一次/auth-check登录态过期就自动跳回登录页不会卡在连接中断里无限重试。4.9 跨线程读控件症状是明明勾了却没生效HttpListener的请求处理跑在线程池线程上而鉴权逻辑要读允许手机点击控制这个复选框和两个口令输入框。从非 UI 线程读控件属性本身就是非法跨线程访问挂着调试器时会抛异常而异常被请求外层的try/catch吞掉了。现在改成 UI 线程改一处就同步一份快照字段请求线程只读快照行为跟有没有调试器、有没有断点都无关。五、安全上做了什么取舍点击控制等于把这台电脑的操作权交出去所以这里的默认值全都偏保守机制说明三条件缺一不可勾选开关 用户名非空 密码非空。少哪一项403 文案就点名哪一项状态行同步显示不用你猜手机端触控默认关闭按钮变橙色才发包防止口袋误触和页面重试打串限流/click最快 100ms 一次约每秒 10 次超了返回429 点击太快会话管理cookie 随机 32 位 hex、HttpOnly、SameSiteLax改口令或停止服务旧登录态立即作废公网警告开着 frp 转发又开着点击控制时状态行直接写公网那端可以试口令并直接操作这台电脑明文这一点不遮掩全程 HTTP。局域网内够用要过公网就设强口令或者只看不点403 长这样故意写成缺什么而不是复读需求403 点击控制未开启电脑上的访问用户名是空的六、上手三步右键 → 以管理员身份运行。服务绑的是http://:8080/监听所有网卡不是管理员会直接启动失败。点启动服务。状态行会打印手机真正能访问的地址——取的是默认路由那张网卡的 IPv4多网卡时其余地址也会列出来避免给你个 VPN 地址。手机连同一个路由器/Wi-Fi浏览器打开那个地址输口令进画面。登录页是普通的网页表单不是浏览器弹的认证框所以在微信内置浏览器里也能登。想用手机点击的话电脑上勾选允许手机点击控制并把用户名密码都填上手机上再点触控然后点哪儿就在哪儿落一次左键七、源码与构建项目信息详情项目名称DeskPeek开源地址https://gitee.com/known/DeskPeek技术栈.NET Framework 4.7.2 Windows Forms HttpListener零第三方依赖许可证Apache 2.0运行环境Windows手机端任意浏览器含微信内置浏览器MSBuild.exe -t:Rebuild -v:minimal DeskPeek.csproj# 产物bin\Debug\DeskPeek.exe代码量很小三个文件值得看MainForm.cs服务、鉴权、抓屏推流、点击注入、frp 集成、ScreenCapture.csGDI 抓屏与物理分辨率、内嵌的IndexHtml手机端播放与重连、触摸坐标换算。八、接下来可能做的键盘输入 / 右键 / 滚轮点击已经是够用的边界再往上就是远程桌面了DXGI Desktop Duplication 提高帧率上限自签 TLS让公网场景不再是明文手机端 PWA 化加到主屏、独立窗口如果你有其它想法欢迎开 issue 或直接改。写在最后做这个工具的动机小得不好意思说出口我就是想在沙发上瞄一眼电脑上的编译跑到哪了。但正是这种不值一提的需求最容易掉进现有工具的缝里——远程桌面太重截图刷新太笨装 App 太麻烦。工具不需要大能在你需要的时候把信息递到手边就够了。