Files
curios/docs/research/harmony-hdc-only-screen.md
T

157 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 官方发布页](https://pypi.org/project/hypium/6.1.0.210/)、[设备测试资源包](https://pypi.org/project/xdevice-devicetest/6.1.0.210/)、[设备连接包](https://pypi.org/project/xdevice-ohos/6.1.0.210/)
**主任务真机进展:**官方 `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](https://files.pythonhosted.org/packages/a2/b5/55bdfe4d669558331499848dbc68269506abb9b4a771e3524a65f9845127/xdevice_devicetest-6.1.0.210-py3-none-any.whl)、[Apache 2.0 条款](https://www.apache.org/licenses/LICENSE-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](https://files.pythonhosted.org/packages/78/a6/8ac621bb958813cde648d515069c30188939b492ffcdd29e8d1e234550cf/xdevice_ohos-6.1.0.210-py3-none-any.whl)
以下最小命令形态已由主任务用本轮官方原件在当前手机验证;参数仍必须来自所选设备与本机会话:
```text
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`;官方框架内部的通配清理、强杀其他进程不应照搬到插件。[扩展加载器](https://github.com/openharmony/testfwk_arkxtest/blob/b04e30c40cf266c9abfbd52933e43a0456eb99a2/uitest/addon/extension_executor.cpp)
## 持续 JPEG 与控制协议
公开客户端已有实际代码实现,不依赖教程中的口头描述。本节以 `devhelmkit` 锁定提交 `05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00` 为协议参考,设备 agent 原件仍从官方 wheel 获取。该项目的 `device/hdc.py`、`rpc/proxy_v2.py`、`screenshot_stream.py` 分别实现传输、RPC 和连续画面。[传输实现](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/device/hdc.py)、[RPC](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/rpc/proxy_v2.py)、[连续帧](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/screenshot_stream.py)
客户端发送请求的 TCP 封包顺序:
```text
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 是另一字段。
```json
{
"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 发送:
```json
{
"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/错误后无限等待。[采集请求与分帧](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/screenshot_stream.py#L677)
实时触控使用相同外层与 `method: "Gestures"`,内层为 `api: "touchDown" | "touchMove" | "touchUp"`、`args: {"x": ..., "y": ...}`。客户端实现按下、移动、抬起接口,适合替换拖动后才发送整段 swipe 的延迟;必须限制 move 排队,并保证取消、失焦时结束本会话已按下触点。键盘、文字可继续使用现有已验证的 UITest 输入路径。[触控实现](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/driver.py#L687)
未找到 `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](https://files.pythonhosted.org/packages/b4/dc/f3b0abf2c518d11d57d8e9487026c133cc7eb0e3479af4b90d13c6db6169/hypium-6.1.0.210-py3-none-any.whl)、[客户端按键调用](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/driver.py#L712)
官方 Python 的 `Driver.inputText` 参数为 `[{"x":x,"y":y},"text"]`,有定位输入的语义;它并不是已确认的 `Gestures` 当前光标文字接口。公开客户端对当前焦点使用 `uitest uiInput text <quoted text>`。当前阶段保留已有验证的文字通道,可避免引入未经验证的光标、追加与清空语义。[当前焦点输入](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/driver.py#L1487)
停止时在采集连接发送 `method: "Captures"`、`params: {"api":"stopCaptureScreen","args":{}}`,随后关闭 socket、停止本机接收线程、移除本会话转发。若启用设备守护进程清理,仅结束自己创建并持有身份的进程,不使用 `killall uitest`。主机解码与渲染只保留最新完整帧;静止画面没有新帧不应误判断线,可用独立控制通道探活。[停止实现](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/screenshot_stream.py#L565)
`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 每一项内部行为均相同的证明;本机静止画面不连续发帧与这套行为一致。[变化检测与屏幕关闭](https://github.com/openharmony/testfwk_arkxtest/blob/b04e30c40cf266c9abfbd52933e43a0456eb99a2/uitest/addon/screen_copy.cpp#L153)、[消费最新帧](https://github.com/openharmony/testfwk_arkxtest/blob/b04e30c40cf266c9abfbd52933e43a0456eb99a2/uitest/addon/screen_copy.cpp#L245)
核查的公开扩展与客户端只暴露 `displayId`、`scale`,没有可据以实现的 fps 或 JPEG quality 参数。`scale` 在获取原屏截图后缩放像素,能降低输出尺寸与编码、传输成本,但不消除原屏截图成本。Devhelmkit 的 `MAX_RECORD_FPS=30` 只约束事后 MP4 合成,与实时采集上限无关。不要把虚构的 `fps:60` 参数加入产品后声称已提速。[缩放位置](https://github.com/openharmony/testfwk_arkxtest/blob/b04e30c40cf266c9abfbd52933e43a0456eb99a2/uitest/addon/screen_copy.cpp#L221)、[实际采集参数](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/screenshot_stream.py#L670)
参考客户端的采集 socket 连接后设为阻塞读取,没有周期发送采集心跳;尚无“必须在采集 socket 定期发送心跳”的源码证据。可保留本机读取超时用来检查取消状态,但超时本身不应标为失败;用独立控制连接的 `Driver.getDisplaySize` 区分静态画面与设备失联。若控制连接也无响应、TCP 关闭或 HDC 确认失联,再终止会话。[socket 建立与持续读取](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/src/devhelmkit/harmony/screenshot_stream.py#L651)
## 两个开源客户端的来源核对
| 项目 | 本轮源码与文件核对 | 结论 |
| --- | --- | --- |
| `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 许可](https://github.com/yabi-zzh/devhelmkit/blob/05c9bc0f7f9ea763d61c8b4b5d1cfbb076626b00/LICENSE)、[Cloud-Phone agent](https://github.com/yiyifred/Cloud-Phone/blob/98d130b20474cf8b18d51161d3bb0b65472c4778/backend/assets/harmony/uitest_agent_v1.1.10.so)、[Cloud-Phone RPC](https://github.com/yiyifred/Cloud-Phone/blob/98d130b20474cf8b18d51161d3bb0b65472c4778/backend/node/src/services/harmony-cast/uitest-rpc.js)、[Cloud-Phone 许可](https://github.com/yiyifred/Cloud-Phone/blob/98d130b20474cf8b18d51161d3bb0b65472c4778/LICENSE)
## 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](https://pypi.org/project/xdevice-devicetest/6.1.0.210/)
设备内置 UITest 公开 CLI 的 screenCap 是单张 PNG,uiRecord 是操作记录,start-daemon 是测试服务入口;不提供已确认的直接视频 stdout 命令。官方系统录屏组件的公开流程是录制后导出文件。对已经确认不存在的 `screenrecord`,不应反复尝试 Android 参数。[UITest 命令](https://github.com/openharmony/docs/blob/f41b9345badd47c7ab0c263344cd7f4b5a549afb/zh-cn/application-dev/application-test/uitest-guidelines.md)、[官方系统录屏](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-screen-recording)
自编 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`;默认销毁代理对象可不杀进程,显式重连则先停止。插件只借鉴结束自己代理的方式,绝不照搬按名称批量结束所有工具进程的逻辑。[官方连接包](https://pypi.org/project/xdevice-ohos/6.1.0.210/)
生产后端现已连续通过三轮真机开始、解码 JPEG、停止和重新开始,每轮都验证代理和转发清理完成。正常画面 JPEG 宽度实测含尾部对齐填充(640 × 1422 对应逻辑半尺寸 630 × 1422),UI 应裁掉该部分,不能按 JPEG 宽度直接映射物理触点。该结果只证明本轮设备的连通与重连,不代表动态帧率、实时手势或其他固件兼容性全部验收。