鸿蒙真机云平台实战:画面代理与 Web 控制如何落地到测试平台
这篇文章详细介绍了鸿蒙真机云平台第一阶段的落地实现方案。全文围绕四个核心环节展开:真机接入、画面代理、Web控制和平台治理闭环,目标是构建稳定的测试平台能力而非简单的投屏功能。 技术实现上,系统采用分层架构:设备层通过HDC+UiTest接入鸿蒙真机;Agent层处理服务启动和端口映射;平台层负责设备状态管理;前端层实现远控交互。关键创新点包括:基于agent.so的扩展机制、JPEG帧流传输方案
摘要
这篇文章基于当前项目真实代码,讲清楚鸿蒙真机云平台第一阶段怎么落地:
真机接入、画面代理、Web 控制、平台治理闭环。
核心目标不是“能投屏”,而是“能作为测试平台能力稳定运行”。
一、先说结论(给赶时间的同学)
- 当前链路是鸿蒙真机链路,不是模拟器
- 画面链路是 JPEG 帧流(不是 WebRTC / H264)
- 控制链路是
/touch(手势注入)+/control(画质控制) - 平台要跑稳,关键在占用、保活、回收和异常自愈
二、为什么“远控可用”不等于“平台可用”
很多团队做到“能看见、能点击”就收工,后面会很快遇到四个问题:
- 设备状态漂移:页面显示可用,平台却占用中
- 并发一上来就抖:触控延迟、帧丢失、超时增多
- 单次截图干扰实时控制:调试体验很差
- 异常恢复靠人工重启:值班成本高
所以这篇重点不是炫技,而是把这四类问题提前收敛掉。
三、当前项目的整体架构
我把鸿蒙云真机链路拆成四层:
- 设备层:Harmony 真机 + hdc + UiTest
- Agent 层:
server_harmony.py+device_harmony.py+proxy_harmony.py - 平台层:设备注册、占用、保活、回收
- 前端层:远控页面(画面渲染 + 触控映射)

四、投流实现原理
核心流程就是四步:
- 推送
agent.so到手机/data/local/tmp/agent.so - 执行
uitest start-daemon singleness启动服务 - 用
hdc fport把本地端口映射到设备8012 - 客户端通过
_uitestkit_rpc_message_head_/_tail_协议收发命令和 JPEG 帧
4.1 agent.so 推送与 daemon 启动
REMOTE_AGENT_PATH = "/data/local/tmp/agent.so"
HDC_REMOTE_PORT = 8012
def _prepare_agent(self):
...
code, out, err = hdc_util.run_hdc(
self.serial, ["file", "send", local_agent, REMOTE_AGENT_PATH], timeout=180
)
if code != 0:
raise RuntimeError("push agent.so failed: ...")
def _start_uitest_daemon(self):
hdc_util.hdc_shell(self.serial, "param set persist.ace.testmode.enabled 1", timeout=15)
code, out, err = hdc_util.hdc_shell(self.serial, "uitest start-daemon singleness", timeout=45)
4.2 fport 映射到 8012
def _forward_tcp(self, remote_port: int) -> int:
local_port = _alloc_local_port()
local = "tcp:{}".format(local_port)
remote = "tcp:{}".format(int(remote_port))
code, out, err = hdc_util.run_hdc(self.serial, ["fport", local, remote], timeout=20)
4.3 协议头尾与消息结构
HEADER_BYTES = b"_uitestkit_rpc_message_head_"
TAILER_BYTES = b"_uitestkit_rpc_message_tail_"
项目里的包结构是:
- 头标识(header)
- 4 字节 sessionId
- 4 字节 body 长度
- body(JSON 或帧数据)
- 尾标识(tailer)
4.4 投流启动命令(startCaptureScreen)
payload = {
"module": "com.ohos.devicetest.hypiumApiHelper",
"method": "Captures",
"params": {"api": "startCaptureScreen", "args": {"options": options}},
}
ret = self._send_request(payload, timeout=8.0)
self._capture_session_id = int(ret["sessionId"])
之后服务端会持续回传 JPEG 帧,前端/代理不断绘制即可形成实时画面。
4.5 agent.so 从哪来(来源说明)
- 我们项目里明确依赖一个本地
harmony/agent.so,并在运行时推送到设备:local_agent = os.path.join(root, "harmony", "agent.so") ... run_hdc(self.serial, ["file", "send", local_agent, REMOTE_AGENT_PATH], timeout=180) - 设备端是通过
uitest start-daemon singleness去加载扩展并启动服务; - 这个机制对应 OpenHarmony 自动化测试框架的扩展能力(
uitest/arkxtest 体系),可以在公开仓库看到相关实现入口和命令能力。
公开仓库参考:
实操上更准确的说法是:agent.so 是基于 uitest 扩展机制加载的动态库,Hypium IDE 插件会在运行时下发;我们在自研平台里复用了同一思路,把 agent.so 显式纳入 agent 启动流程。

五、真机接入链路:从 hdc 发现到平台可见
5.1 Agent 轮询 hdc 目标,发现新设备
async def harmony_device_poll():
target_list = []
while True:
await gen.sleep(1)
new_list = hdc_util.list_targets()
...
这一段决定了“设备是否能稳定被发现”。项目里是 1 秒轮询,属于偏稳妥做法。
5.2 初始化 HarmonyDevice 并上报平台
device = HarmonyDevice(serial, FREE_PORT)
await device.init()
DEVICES[serial] = device
await HBC_HARMONY.device_update(
{
"command": "init",
"serial": serial,
"agent": device.addrs,
"properties": await device.properties(),
}
)
这里重点是 addrs,后续前端所有控制都依赖它给出的 harmonyServerAddress。
5.3 设备下线时同步删除
if serial in DEVICES:
DEVICES[serial].close()
DEVICES.pop(serial, None)
await HBC_HARMONY.device_update({"command": "delete", "serial": serial})
这一步做对了,平台状态才不会出现“僵尸设备”。
六、画面链路:为什么当前阶段用 JPEG 帧流
先说答案:为了先把平台跑稳。
你现在这套项目是“先稳定、再升级编码方案”的典型正确路径。
6.1 代理明确就是 JPEG 流
"""
Per-device WebSocket proxy: /screen (JPEG stream), /touch (JSON, compatible with Android scrcpy proxy).
"""
6.2 画质策略在代理层统一控制
class StreamConfig:
@classmethod
def encode_options(cls):
if cls.mode == "low":
return {"scale": 0.4, "quality": 15, "passthrough": False}
if cls.mode == "middle":
return {"scale": 0.6, "quality": 30, "passthrough": False}
return {"scale": 1.0, "quality": 75, "passthrough": True}
这意味着前端只管“切档位”,不需要懂编码细节。
6.3 帧采集 + 广播给多客户端
jpeg = await loop.run_in_executor(None, lambda: UI_RPC_SCREEN.wait_capture_frame(1.5))
...
fut = client.write_message(frame, binary=True)
前端对应接收:
let ws = new WebSocket("ws://" + this.device.sources.harmonyServerAddress + "/screen");
ws.onmessage = (message) => {
if (message.data instanceof Blob) {
this.drawBlobImageToCanvas(message.data, this.canvas.bg, this.display.width > this.display.height);
}
};
【远控中画质】
【远控高画质】
七、控制链路:/touch + /control 双通道
7.1 /control 负责控制指令配置(如画质)
const ws = new WebSocket("ws://" + this.device.sources.harmonyServerAddress + "/control");
...
this.sendControlCommand({ type: "setScreenMode", detail: target })
代理侧回执:
if cmd in ("setScreenMode", "screen"):
StreamConfig.set_mode(data.get("detail") or "middle")
payload = {"ok": True, "type": cmd, "mode": StreamConfig.mode}
7.2 /touch 负责手势注入
let streamUrl = "ws://" + this.device.sources.harmonyServerAddress + "/touch";
ws.send(JSON.stringify({ msg_type: 2, action, x: pix_data[0], y: pix_data[1] }));
代理将归一化坐标映射为设备坐标后执行:
x = int(float(data["x"]) * dw)
y = int(float(data["y"]) * dh)
await self._run(lambda: UI_RPC_TOUCH.touch_down(x, y))
7.3 为什么要限频
项目里对 move 事件做了 50ms 节流:
if now - float(TOUCH_STATE.get("last_move_at") or 0.0) >= 0.05:
TOUCH_STATE["last_move_at"] = now
await self._run(lambda: UI_RPC_TOUCH.touch_move(x, y))
这是实战里非常关键的一步,不做限频,UiDriver 超时会显著增加。
八、平台侧能力:不只是“点屏幕”
当前项目已经把鸿蒙设备接成了可治理能力,而不是单纯远控:
- HAP 安装(URL 和上传)
- 截图下载
- UI 层级抓取
- 按键事件(Home/Back/Power)
- 文本输入
对应接口例如:
(r"/app/install", AppInstallHandler),
(r"/device/screenshot", DeviceScreenshotHandler),
(r"/device/hierarchy", DeviceHierarchyHandler),
(r"/device/key", DeviceKeyHandler),
(r"/device/text", DeviceTextHandler),
这套接口设计对后续“跑用例平台化”非常重要,因为执行器会直接依赖这些能力。

九、稳定性优化(这部分最值钱)
8.1 画面与触控 RPC 分离
UI_RPC_SCREEN 和 UI_RPC_TOUCH 分离,避免相互阻塞,这是体验稳定的关键。
8.2 截图链路隔离
截图走独立 hdc_util.capture_screen_jpeg,避免打断左侧实时画面。
8.3 断链自动恢复
出现 UiDriver socket closed 时自动 reset RPC,并带退避重试。
8.4 设备属性清洗
_fetch_properties_sync 里对版本和型号有容错清洗,减少设备脏数据污染平台筛选。
十、踩坑复盘(少走弯路)
坑 1:先做了远控,没做治理
结局通常是设备池很快“假满”。
坑 2:触控高频直通
短期灵敏,长期会把 UiDriver 打到超时。
坑 3:截图和直播共用链路
会造成“截一次图,左侧画面抖一下”的体验问题。
坑 4:只做技术可行,不做平台可用
缺少占用、保活、回收,就很难进入真实团队流程。
十一、下一阶段:从远控走向“云真机跑用例”
现在这个项目已经把基础打好了,后面可以顺着这条线往上走:
- 任务排队与设备调度(按标签和优先级)
- 并发执行与失败重试
- 产物聚合(日志、截图、录像)
- CI 门禁和质量趋势看板
这条路径比“直接做全量平台”成功率高很多。
FAQ
Q1:鸿蒙这套是不是模拟器方案?
不是,当前项目是真机接入,设备发现走 hdc 目标列表。
Q2:为什么不用 WebRTC?
当前阶段目标是平台稳定和快速落地,JPEG 帧流更易控。后续可在不改平台协议前提下升级编码链路。
Q3:这套设计能复用到 iOS/Android 吗?
可以,核心抽象是通用的:设备接入、能力代理、平台治理、前端统一协议。
结语
鸿蒙云真机真正的门槛,不是“能不能投屏”,而是“能不能稳定服务团队”。
你这个项目目前的路线是对的:
先把远控平台底座做稳,再把用例执行能力叠上去。
更多推荐

所有评论(0)