UURC-Web:如何愉快地从公司电脑连回家
目录
声明:本文包含 AI 辅助创作。
折腾仅因个人兴趣,记录分享仅为研究学习交流,请勿用于违法用途。
2026-07-19 更新:补上开源后两个月的排查记录,内容来自仓库里的实际提交和测试。
很久没写这种长篇“考古”记录了。最近又开了个坑:UURC-Web。
公司电脑不让装第三方远控软件,可我偶尔还得连回家里那台 Mac。客户端装不了,那就试试浏览器。
我给 AI 提了个需求:把网易 UU 远程(UURemote)接进网页。接下来几天,我主要在准备环境、描述现象和验收结果。
为什么要折腾这玩意?
UU 远程在 macOS 上延迟低,画质也不错。它没有网页版主控端,我便给 AI 接上 IDA Pro MCP,让它从官方客户端里找线索。
目标很朴素:网页能登录、看屏幕,键盘鼠标也能用。中转服务放到 Cloudflare 的免费额度里,直连控制照常工作。
考古现场
第一阶段:拆解 Android APK
第一步是看官方客户端怎么做。Android 版 com.netease.uuremote 交给 AI 后,它先跑了一次 apktool d。
业务逻辑基本都在 Java/Kotlin 层,走普通的 HTTPS 请求,也就是网易的 nrd API。音视频流、信令(Signaling)和 WebRTC 协商都塞在 native 库 libstreamer.so 里。
硬骨头也在这里。AI 通过 IDA Pro MCP 顺着 native 符号往下翻,找到了 socket.io、SDP、ICE 和 DTLS/SRTP,最后理出三层:业务 API、Socket.io 信令、WebRTC 数据通道。
第二阶段:WebRTC 协议的“买家秀”与“卖家秀”
原生客户端会在 native 层创建 5 路 video 收流和 1 路 audio。浏览器端要照着这套 WebRTC 布局协商,费了不少劲。
UU 的信令在 WebRTC 交换外又包了一层 SOAC(Signaling Offer/Answer/Candidate),还会用 Gzip 压缩 SDP。受控端只认官方格式,AI 便照着原生客户端构造 Protobuf 风格的 Envelope 和二进制 payload。
踩过的坑
鼠标一动,画面就停
刚开源时碰到过一个很邪门的问题:只看画面可以一直跑,接管鼠标后几秒就冻住,断开重连才能恢复。WebRTC stats 里显示的是 transport_stalled,RTP 收包已经停了,受控端压根没再推流。
问题出在输入热路径。每次 pointermove 都会触发大约三次整棵组件树重渲染,还要深拷贝完整的会话状态。鼠标以 60~120Hz 移动时,主线程忙着跑 React,100ms 一次的控制心跳和 EchoRequest 回复开始延迟。channel.send 偶尔遇到背压,还会把心跳永久停掉。受控端超时后把主控判成离线,视频也随之停了。
这部分修复由 AI 完成:调试事件放进环形缓冲,data channel 状态只在真正变化时推给 React,输入处理也从大 controller 中拆了出去。我负责反复移动和点击,把仍会卡住或丢落点的情况继续丢回去。后来它又把连续的鼠标移动合并到每个动画帧只发一次;点击发生前先补发当前位置,合并后也不会丢掉最后一个落点。
卡顿诊断也跟着细分了。transport_stalled 表示 RTP 已停,decode_stalled 表示包还在进、解码帧数却没动。后者连续出现两次时会触发自动恢复,控制台同时记录 RTP 增量、ICE candidate pair 和信令状态,下一次再卡就有东西可查了。
Windows 画面正常,键鼠全死
Windows 端最初只能看到画面,鼠标和键盘完全没反应,console 里也没有报错。我把 Windows 受控端和官方 Android 主控端连好,再让 AI 重翻 libinputmanager.so,对照控制通道抓包。它发现桌面端输入直接发送 JSON;Android 和 MuMu 会在外面包一层 send_to_rom protobuf。当时的代码把 Windows 送进了移动端路径,被控端收到后直接丢弃。
鼠标坐标和键码也各有一层转换。Windows 与 macOS 都收 [0, 1] 范围的绝对坐标,代码当时发的是像素;键盘则要把 Android KeyCode 映射成 Windows VK,并带上官方报文里的 interrept: true。把路由、坐标和键码补齐后,Windows 的键鼠终于开始工作。
紧接着又出现了 whoami 打成 whoooooami。网络延迟拉长了 kbd_press 到 kbd_release 的间隔,UU 的 Windows 服务端把中间这段时间当成长按。最终处理是让 Ctrl、Alt、Shift、Meta 保持到 keyup 再释放;普通按键在每次 keydown 时立即发一组 press + release,长按重复交给浏览器自己的 repeat 事件。中文直接用远端输入法,复制粘贴走文本通道。
右键、Alt+Tab 和触控板
输入能走通以后,还有些很碎、用起来又很烦的问题。浏览器右键菜单会吞掉 pointerup,远端便一直以为右键还按着;Alt+Tab、切应用和切标签页也可能让系统吃掉 keyup。现在 stage 失焦、window blur 或页面隐藏时都会调用 releaseAllInputs,把记录中的按键和鼠标按钮全部抬起。
滚轮也绕了一圈。React 的 onWheel 是 passive listener,preventDefault 拦不住页面跟着滚,所以控制区改用了原生的 non-passive listener。桌面端还要把 DOM 的内容滚动方向换成 UU 使用的原生滚轮方向,并统一 pixel、line、page 三种 delta。触控板的横轴噪声会先过滤,最终增益降到 0.5,不会轻轻一划就滚出去半页。
多路视频时,早期代码用 querySelector("video") 取到了第一路隐藏画面,鼠标坐标会按错误分辨率换算。现在只认正在显示的 video,并用同一套几何计算处理 contain 留黑边和 cover 裁切。最近接上的远端光标也复用了这套坐标,光标形状、热点和缩放能和画面对上。
最近补上的功能
连接流程现在会自动完成。进入控制页后,页面会加入设备并建立连接,control channel 打开后直接进入「控制中」,焦点也会落到画面上。设备若被自己上一个网页会话占着,会根据 participant 的 clientId 自动接管;占用者是其他人时,页面仍会等你明确确认。自动路径连续重连失败两次后,下一次会强制走 UU Relay。
伙伴远程协助也接进来了。登录后填对方的设备 ID 即可发起,前端会先查询受控端要求的验证方式。有设备验证码就带码加入,需要对方确认时留在等待状态;两种方式同时开启的设备也能走完。加入结果会返回目标平台,Windows、macOS 等输入协议由页面自己选择。
画面源会标出分辨率和「无信号」,切到空视频轨时直接显示「该画面暂无内容」,黑屏的原因一眼就能看到。命令栏平时停在画面上方,进入全屏后才变成可拖动浮层;「仅查看 / 控制中」可以随时暂停输入,连接和画面状态也换成了能看懂的中文。
声音也补上了。远端音轨交给独立播放器,工具栏可以静音和调音量;浏览器拦住自动播放时,按钮会提示手动恢复。
光标对操作感的影响比我预想中大。移到输入框、窗口边缘和链接上时,形状变化会直接告诉我远端当前能做什么。控制通道里的 cursor shape 会被解析成本地光标,文本、等待、移动、缩放、手型等状态都能跟着远端走;远端发来的自定义 PNG 也会保留热点和缩放。鼠标操作优先使用浏览器原生光标,触摸设备或浏览器不支持图片光标时,页面会显示一层跟随指针的光标。它与输入坐标共用画面几何计算,切换视频源、画面留黑边或被裁切时,光标和实际点击位置也能对上。
剪贴板可以来回同步。本机粘贴会通过文本通道发到远端,受控端回传的 RomMsg_Text 则写进浏览器剪贴板。浏览器拒绝写入时,内容会留在面板里,方便手动取用。
现在的样子
到 7 月,我已经会实际拿 UURC-Web 干活了。短信验证码登录、账号凭证导入导出、设备状态和控制占用都能在网页里处理,Windows、macOS、Android 和 MuMu 也各自走对了输入路径。
部署有两种方式:Docker 直接拉起,或者放到 Cloudflare Worker + Durable Objects。后者刚好能吃到免费额度。
后记
真让我自己沿着 IDA 追 libstreamer.so,我看几屏伪代码就会迷路。AI 能替我把调用链、协议字段和代码实现接起来;我把现象说清楚,判断它查得有没有跑偏,再拿设备验证。这样的分工很适合我。
当然,这也是因为公司电脑真的不让装远控……(再次强调)。
项目代码已经开源。有类似需求,或者也想看看这套远控协议怎么跑,可以直接去仓库翻代码。摸鱼愉快,2333。
使用边界
本文记录的是个人账号和自有设备上的研究过程。文中的逆向、抓包、调试与远控验证只应在自己拥有或明确授权的设备上进行,请勿连接或控制他人设备,也不要用这些方法规避平台或组织的管理要求。
-END-