摘要

这篇文章基于当前项目真实代码,讲清楚鸿蒙真机云平台第一阶段怎么落地:
真机接入、画面代理、Web 控制、平台治理闭环

核心目标不是“能投屏”,而是“能作为测试平台能力稳定运行”。


一、先说结论(给赶时间的同学)

  • 当前链路是鸿蒙真机链路,不是模拟器
  • 画面链路是 JPEG 帧流(不是 WebRTC / H264)
  • 控制链路是 /touch(手势注入)+ /control(画质控制)
  • 平台要跑稳,关键在占用、保活、回收和异常自愈

二、为什么“远控可用”不等于“平台可用”

很多团队做到“能看见、能点击”就收工,后面会很快遇到四个问题:

  1. 设备状态漂移:页面显示可用,平台却占用中
  2. 并发一上来就抖:触控延迟、帧丢失、超时增多
  3. 单次截图干扰实时控制:调试体验很差
  4. 异常恢复靠人工重启:值班成本高

所以这篇重点不是炫技,而是把这四类问题提前收敛掉。


三、当前项目的整体架构

我把鸿蒙云真机链路拆成四层:

  1. 设备层:Harmony 真机 + hdc + UiTest
  2. Agent 层server_harmony.py + device_harmony.py + proxy_harmony.py
  3. 平台层:设备注册、占用、保活、回收
  4. 前端层:远控页面(画面渲染 + 触控映射)

占用/保活

冷却指令

Harmony 真机
HDC + UiTest

Harmony Agent
server_harmony + device_harmony + proxy_harmony

iov-backend
设备状态与调度

iov-frontend
远控页面

在这里插入图片描述


四、投流实现原理

核心流程就是四步:

  1. 推送 agent.so 到手机 /data/local/tmp/agent.so
  2. 执行 uitest start-daemon singleness 启动服务
  3. hdc fport 把本地端口映射到设备 8012
  4. 客户端通过 _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 启动流程。

本地 agent.so

hdc file send
/data/local/tmp/agent.so

uitest start-daemon singleness

hdc fport
本地随机端口 -> 8012

RPC 协议通信
header + sid + len + body + tail

startCaptureScreen

JPEG 帧持续回传

前端 Canvas 持续绘制

在这里插入图片描述


五、真机接入链路:从 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_SCREENUI_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:只做技术可行,不做平台可用

缺少占用、保活、回收,就很难进入真实团队流程。


十一、下一阶段:从远控走向“云真机跑用例”

现在这个项目已经把基础打好了,后面可以顺着这条线往上走:

  1. 任务排队与设备调度(按标签和优先级)
  2. 并发执行与失败重试
  3. 产物聚合(日志、截图、录像)
  4. CI 门禁和质量趋势看板

这条路径比“直接做全量平台”成功率高很多。


FAQ

Q1:鸿蒙这套是不是模拟器方案?

不是,当前项目是真机接入,设备发现走 hdc 目标列表。

Q2:为什么不用 WebRTC?

当前阶段目标是平台稳定和快速落地,JPEG 帧流更易控。后续可在不改平台协议前提下升级编码链路。

Q3:这套设计能复用到 iOS/Android 吗?

可以,核心抽象是通用的:设备接入、能力代理、平台治理、前端统一协议。


结语

鸿蒙云真机真正的门槛,不是“能不能投屏”,而是“能不能稳定服务团队”。
你这个项目目前的路线是对的:

先把远控平台底座做稳,再把用例执行能力叠上去。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐