19 KiB
Harmony Connector:仅通过 HDC 的连续投屏
调研日期:2026-09-10。产品约束:最终用户只启用 USB 调试并授权 Mac,不注册开发者账号、不安装 HAP、不修改手机安全策略。目标是可交互的连续画面,不限定 H.264;帧率和延迟必须在真机上测量。本篇研究未操作设备。
当前结论
找到了一条此前漏查的公开分发路线:使用华为 Hypium 的公开 PyPI 依赖包中原样分发的 UITest agent,由设备内置 uitest 加载,通过 HDC 转发持续 JPEG 与触控 RPC。 该路线无需用户编译、签名或安装手机应用;它仍会向设备临时推送一个 agent 文件,作用类似 scrcpy 的临时设备端服务。不能把自编 unsigned ELF 或 addon 被 XPM 拒绝,推广为所有官方签名 agent 均无法加载。
Hypium 的公开说明支持 USB/Wi-Fi 调试连接,并明确 Driver 模式可作为 SDK 集成到其他工具。其 xdevice-devicetest 包公开包含原生 agent;对应的 xdevice-ohos 包提供选择、推送、启动和转发实现。Hypium 官方发布页、设备测试资源包、设备连接包
**主任务真机进展:**官方 v1.2.2.so 使用唯一 --extension-name 已成功加载,Unix socket 能返回 JPEG,Driver.create、屏幕尺寸和旋转查询成功;不需要安装 HAP、开发者账号或录屏确认弹窗。初次收到的是 630 × 1422 黑帧,首帧约 195–235 ms,15 秒仅收到 3 帧;当时可能处于锁屏状态,这些数字只证明链路连通,不能作为动态画面流畅度或端到端延迟的结论。本文作者未操作手机,真机结果来自主任务当轮报告。
精确来源与许可证据
本轮从 PyPI 元数据取得 wheel 下载地址,在内存读取并校验 SHA256,没有安装或执行其中代码。
| 资源 | 版本、路径及 SHA256 |
|---|---|
| xdevice-devicetest wheel | 6.1.0.210;1c80f0da8d497d4e034f9873e966e8b2d8ef23ecc376c64c839c1493a813980e |
| 新版 ARM64 agent | devicetest/res/prototype/native/uitest_agent_v1.2.2.so;e1b8e75fad983aa29640784ee4b457fe7ac1c916a15f72c8b899d5a7716da651 |
| 旧版 ARM64 agent | devicetest/res/prototype/native/uitest_agent_v1.1.10.so;1c9286456fb003ee15d86fef04e8c93f004027349e6dd2ef972c792e0a6d4bf8 |
| xdevice-ohos wheel | 6.1.0.210;548a09bb6fc9fcfd92a7dbb5b69180de0159ce8959057e133dec34774054bb12 |
资源包的 METADATA 明确列出 License-File: LICENSE,xdevice_devicetest-6.1.0.210.dist-info/LICENSE 是完整 Apache License 2.0;本次清单未发现额外 EULA、NOTICE 或对 agent 的另行许可文件。Apache 2.0 提供商业使用及 object form 再分发条件;打包时保留该许可证、来源版本和校验值,保留现有版权声明,修改自有封装代码应说明。不要以第三方仓库的许可证替代原始发布包的许可。原始 wheel、Apache 2.0 条款
原始 v1.2.2.so 尾部包含 ELF 签名块标记,并含 Huawei CBG Software Signing Service CA 证书链字符串。这里只确认原件附带签名结构,没有进行独立密码学验签,也没有据此声称目标设备必然接受。部署必须保持字节不变,不 strip、不重链接、不截掉 ELF 尾部。该 wheel 没有提供 agent 的 C++ 源码,因此应称“公开分发的官方 agent”,不能称为设备端也能完全自行重建的开源实现。
真实选择与启动路径
当前手机 UITest 版本由主任务报告为 6.0.2.3。官方 xdevice-ohos 6.1.0.210 内:
ohos/environment/device.py::check_if_unix_socket_mode:UITest 版本高于6.0.2.1且设备不是 x86_64 时使用 Unix socket。ohos/proxy/proxy_base.py::_init_so_resource:此分支选择uitest_agent_v1.2.2.so;旧分支才使用 v1.1.10/v1.1.5/v1.1.3。ohos/proxy/ui_proxy.py:默认推送到/data/local/tmp/agent.so,运行/system/bin/uitest start-daemon singleness,连接localabstract:uitest_socket。旧协议为设备 TCP 8012。
因此不能把“设备 6.x”硬编码为旧 TCP 8012,也不能只依据文件名相似选择第三方 agent。对应公开 wheel
以下最小命令形态已由主任务用本轮官方原件在当前手机验证;参数仍必须来自所选设备与本机会话:
hdc -t <device> file send <official-agent.so> /data/local/tmp/<session-agent.so>
hdc -t <device> shell uitest start-daemon singleness --extension-name <session-agent.so>
hdc -t <device> fport tcp:<local-port> localabstract:uitest_socket
独有 --extension-name 来自 OpenHarmony UITest 的扩展加载器:它将名称解析到 /data/local/tmp/ 并加载扩展。官方 Python 默认使用 agent.so,独有名已在本轮商业手机上验证,其他固件仍需兼容验证。不要覆盖其他工具已有的 agent.so;官方框架内部的通配清理、强杀其他进程不应照搬到插件。扩展加载器
持续 JPEG 与控制协议
公开客户端已有实际代码实现,不依赖教程中的口头描述。本节以 devhelmkit 锁定提交 05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00 为协议参考,设备 agent 原件仍从官方 wheel 获取。该项目的 device/hdc.py、rpc/proxy_v2.py、screenshot_stream.py 分别实现传输、RPC 和连续画面。传输实现、RPC、连续帧
客户端发送请求的 TCP 封包顺序:
ASCII "_uitestkit_rpc_message_head_"
u32 big-endian session ID
u32 big-endian UTF-8 JSON byte length
JSON body
ASCII "_uitestkit_rpc_message_tail_"
初始化和控制使用独立连接。调用 Driver.create 后使用返回的实际引用值,不能把 Driver#0 当成所有连接的固定保证。外层 request_id 为本次唯一字符串,transport session ID 是另一字段。
{
"module": "com.ohos.devicetest.hypiumApiHelper",
"method": "callHypiumApi",
"params": {
"api": "Driver.create",
"this": null,
"args": [],
"message_type": "hypium"
},
"request_id": "<unique-request>"
}
后续只替换 params 中的 API、引用与参数,外层保持相同。以下是主任务确认的解析后结果值,不代表复制完整回包结构:
| API | this / args |
当前手机结果 |
|---|---|---|
Driver.create |
null / [] |
"Driver#0" |
Driver.getDisplaySize |
返回的 Driver 引用 / [] |
{"displayId":0,"x":1260,"y":2844} |
Driver.getDisplayRotation |
返回的 Driver 引用 / [] |
0 |
Driver.wakeUpDisplay |
返回的 Driver 引用 / [] |
null |
Driver.getDeviceInfo |
返回的 Driver 引用 / [] |
No handler found,当前手机不可用,不用于探活。 |
物理输入坐标应依据屏幕尺寸与旋转转换,不能直接使用 JPEG 缩小后的像素坐标。getDisplaySize 可作为独立控制连接的低频探活;无新图不等于连接已断。
在独立采集 socket 发送:
{
"module": "com.ohos.devicetest.hypiumApiHelper",
"method": "Captures",
"params": {
"api": "startCaptureScreen",
"args": { "options": { "displayId": 0, "scale": 0.5 } }
},
"request_id": "<unique-request>"
}
注意 Captures.args 是对象,与 callHypiumApi.args 数组不同。JPEG 被推送到发送采集请求的 socket。**当前官方 v1.2.2 真机回包实测为 JSON 行 {"result":true,"pts":...}\n 与裸 JPEG FF D8 … FF D9 加换行,不使用上述 HEAD/TAIL 请求封包包住 JPEG。**不能只实现带 HEAD/TAIL 的接收器。参考客户端兼容协议封装与裸 JPEG;实现应依据实际格式使用有界长度及完整帧解析,不能假设一次 recv 对应一张图,也不能忽略 ACK/错误后无限等待。采集请求与分帧
实时触控使用相同外层与 method: "Gestures",内层为 api: "touchDown" | "touchMove" | "touchUp"、args: {"x": ..., "y": ...}。客户端实现按下、移动、抬起接口,适合替换拖动后才发送整段 swipe 的延迟;必须限制 move 排队,并保证取消、失焦时结束本会话已按下触点。键盘、文字可继续使用现有已验证的 UITest 输入路径。触控实现
未找到 Gestures.keyDown、Gestures.keyUp 或 Gestures.text 的公开调用依据。普通按键使用 callHypiumApi:Driver.triggerKey 的 args 为 [keyCode];组合键 Driver.triggerCombineKeys 为 [key1,key2] 或三个键;Home/Back 分别是 Driver.pressHome / Driver.pressBack、args:[]。不要使用不存在的 Driver.pressKey。官方 hypium/uidriver/arkuidriver.py 声明 triggerKey、triggerCombineKeys、pressBack;其 Home 包装存在 UiDriver.pressHome 命名差异,因此仍应按当前设备能力验证客户端使用的 Driver.pressHome。官方 Hypium wheel、客户端按键调用
官方 Python 的 Driver.inputText 参数为 [{"x":x,"y":y},"text"],有定位输入的语义;它并不是已确认的 Gestures 当前光标文字接口。公开客户端对当前焦点使用 uitest uiInput text <quoted text>。当前阶段保留已有验证的文字通道,可避免引入未经验证的光标、追加与清空语义。当前焦点输入
停止时在采集连接发送 method: "Captures"、params: {"api":"stopCaptureScreen","args":{}},随后关闭 socket、停止本机接收线程、移除本会话转发。若启用设备守护进程清理,仅结束自己创建并持有身份的进程,不使用 killall uitest。主机解码与渲染只保留最新完整帧;静止画面没有新帧不应误判断线,可用独立控制通道探活。停止实现
startCaptureScreen 是 agent 提供的 RPC 名称,不是设备 uitest help 中能直接运行的 CLI 子命令。设备帮助没有它,不证明公开 agent 不能提供它。
画面变化、帧率与保活
公开 UITest screen_copy.cpp 的采集线程调用 DisplayManager::GetScreenshot,通过 memcmp 比较前后像素,只有变化才通知 JPEG 编码线程。屏幕关闭时提供黑色画面并暂停常规采集,50 ms 的 usleep 仅用于检查关闭的屏幕是否重新亮起,不能据此推算正常采集被限制在 20 fps。常规采集循环没有显式 fps 计时上限,编码线程取最新帧。商业 agent 未提供其 C++ 源码,此处解释公开 UITest 实现,不能当作官方 v1.2.2 每一项内部行为均相同的证明;本机静止画面不连续发帧与这套行为一致。变化检测与屏幕关闭、消费最新帧
核查的公开扩展与客户端只暴露 displayId、scale,没有可据以实现的 fps 或 JPEG quality 参数。scale 在获取原屏截图后缩放像素,能降低输出尺寸与编码、传输成本,但不消除原屏截图成本。Devhelmkit 的 MAX_RECORD_FPS=30 只约束事后 MP4 合成,与实时采集上限无关。不要把虚构的 fps:60 参数加入产品后声称已提速。缩放位置、实际采集参数
参考客户端的采集 socket 连接后设为阻塞读取,没有周期发送采集心跳;尚无“必须在采集 socket 定期发送心跳”的源码证据。可保留本机读取超时用来检查取消状态,但超时本身不应标为失败;用独立控制连接的 Driver.getDisplaySize 区分静态画面与设备失联。若控制连接也无响应、TCP 关闭或 HDC 确认失联,再终止会话。socket 建立与持续读取
两个开源客户端的来源核对
| 项目 | 本轮源码与文件核对 | 结论 |
|---|---|---|
yabi-zzh/devhelmkit |
Apache 2.0;有连续 JPEG、独立控制 socket、down/move/up 和采集帧率统计;仓库 agent_v2.so SHA256 与本次官方 wheel 的 agent 不一致。 |
可参考其客户端协议,不把其未核清来源的 blob 作为发行原件。 |
yiyifred/Cloud-Phone |
Apache 2.0;包含 uitest_agent_v1.1.10.so,其 SHA256 完全匹配官方 wheel v1.1.10;Gestures 代码注释中的 ECHO/hdckit 指同类 RPC 操作,并不是设备原生内置 H.264 服务。 |
证明实际客户端采用 Hypium agent;其旧 agent/TCP 假设不能直接用于当前设备。 |
devhelmkit 许可、Cloud-Phone agent、Cloud-Phone RPC、Cloud-Phone 许可
H.264 与其他路径的边界
同一官方资源 wheel 包含 devicetest/res/recorder/libscrcpy_server1.z.so 到 libscrcpy_server4.z.so。其 controllers/tools/recorder/record_agent.py 推送为 /data/local/tmp/libscreen_recorder.z.so,通过 UITest extension 启动,参数为 -p <port> -m 1 -screenId <id>,默认 gRPC 端口 5001;对应 rpc_manager.py 和生成的 protobuf 定义含 ScrcpyService/onStart、onEnd、onRequestIDRFrame。当前 Python 调用方主要用于步骤视频,停止后拉取 mytest.mp4。仅凭资源名不能认定它是可直接使用的实时 H.264 投屏服务;格式、stream payload 与当前固件兼容性仍需专门验证。它不是本轮 JPEG 路线的前置条件。官方资源 wheel
设备内置 UITest 公开 CLI 的 screenCap 是单张 PNG,uiRecord 是操作记录,start-daemon 是测试服务入口;不提供已确认的直接视频 stdout 命令。官方系统录屏组件的公开流程是录制后导出文件。对已经确认不存在的 screenrecord,不应反复尝试 Android 参数。UITest 命令、官方系统录屏
自编 ELF 的执行权限错误与自编 addon 的 XPM 加载拒绝已由主任务复现;它们不应作为继续尝试关闭 XPM、SELinux、root 或借用私人签名的理由。公开 Hypium 原件是另一条正常工具分发与加载链路,需按原件、版本、协议重新验证。
产品验收与明确未知
- 当前证据确认公开来源、许可文件、官方选择和加载方式,以及开源客户端连续帧/触控协议;主任务已验证当前手机原件加载、Unix socket、Driver 初始化、JPEG 解码与屏幕尺寸查询。
- 初测黑帧只证明连通,仍需设备解锁后的真实内容与交互画面验证;不得把收到 JPEG 等同于已达到 scrcpy 的流畅体验。
- 用手机实际动画/滑动测量收到与呈现的帧率,独立记录排队及输入往返;静止画面频率不能当作最大帧率。没有实际结果前不承诺 30/60 fps。
- 验证拖动跟手、旋转后的物理坐标、拒绝/断开、停止后不再采集与控制,以及重复启动清理;不影响其他使用 UITest 的工具。
- 未确认
Captures/Gestures跨版本稳定性;应固定已验收的 agent,报告能力与错误,避免无提示降为旧截图模式。
最终用户流程可以保持“连接 USB → 允许调试 → 开始投屏”。官方 agent 的获取、校验、部署、转发和清理应由插件完成,不能再把开发者账号或 HAP 签名作为该流程的要求。
2026-09-10:主任务重连验证补充
实际商业手机拒绝普通 kill <pid>,返回 Permission denied;同一身份的 kill -9 <pid> 成功,进程随即消失。此前残留代理能响应 Driver RPC,却无法恢复采集,因此不将无条件复用残留代理作为恢复策略。官方 xdevice-ohos 6.1.0.210 的 ohos/proxy/ui_proxy.py::stop_ui_rpc 与 proxy_base.py::_kill_uitest 均使用 kill -9;默认销毁代理对象可不杀进程,显式重连则先停止。插件只借鉴结束自己代理的方式,绝不照搬按名称批量结束所有工具进程的逻辑。官方连接包
生产后端现已连续通过三轮真机开始、解码 JPEG、停止和重新开始,每轮都验证代理和转发清理完成。正常画面 JPEG 宽度实测含尾部对齐填充(640 × 1422 对应逻辑半尺寸 630 × 1422),UI 应裁掉该部分,不能按 JPEG 宽度直接映射物理触点。该结果只证明本轮设备的连通与重连,不代表动态帧率、实时手势或其他固件兼容性全部验收。