各位掘友们好,前面发表过自己在学习鸿蒙时总结的一些小笔记,而这篇则是我学习的实现!
我就卖关子了,直接开门开门见山!我利用AI的帮助实现了跨端远程控制。可以在win/mac系统电脑上控制鸿蒙的2in1/pc设备。具体如何实现的呢?请听我一一道来。
背景
那天老大语重心长的对我说,小林啊,客户那边需求有一个远程控制的功能,我一听心想这很简单市面上那么多,多看几款把功能仿出来不就行了。后面老大一说具体需求给我听的一愣一愣的,什么!在win/mac系统通过浏览器去控制鸿蒙设备。说完老大给我讲了一下大概思路然后眼神坚定的看着我,没办法还是硬着头皮接下这个任务,(也可能是年轻觉得别能做自己也能做!)。

相关技术栈
- win(控制侧):JavaScript、Html、Css3、JMUXER视频渲染库
- 服务:Node.js
- HarmonyOS(被控制侧):C++、ArkTS
功能
- 基本键鼠操作
- 双向复制粘贴
整体逻辑
sequenceDiagram
participant WIN as Win 浏览器<br/>(controller)
participant SVR as Node.js 中继<br/>(Express + ws)
participant HAV as 鸿蒙设备<br/>(device)
Note over WIN,HAV: ═══ 连接建立阶段 ═══
WIN->>SVR: 1. HTTP GET /?room=8888
SVR-->>WIN: index.html + controller.js + JMuxer
WIN->>SVR: 2. WSS /ws?room=8888&role=controller
SVR-->>WIN: WebSocket 连接成功
WIN->>SVR: 3. {type:'join', role:'controller', room:'8888'}
HAV->>SVR: 4. WSS /ws?room=8888&role=device
SVR-->>HAV: WebSocket 连接成功
HAV->>SVR: 5. {type:'join', role:'device'}
HAV->>SVR: {type:'screen_started', width, height}
SVR->>WIN: 6. {type:'device_connected', width, height}
Note over WIN,HAV: ═══ 主流程:视频 + 控制 ═══
loop 持续录屏 (30fps)
HAV->>SVR: 7. [Binary] H.264 NAL 裸流
SVR->>WIN: 转发 [Binary] H.264
WIN->>WIN: JMuxer 封装 fMP4 → MSE → <video> 播放
end
WIN->>SVR: 8. {type:'pointer', action, x, y}
SVR->>HAV: 转发 pointer
HAV->>HAV: injectMouseEvent --> 系统鼠标注入
WIN->>SVR: 9. {type:'wheel', deltaX, deltaY}
SVR->>HAV: 转发 wheel
HAV->>HAV: injectMouseWheelEvent --> 系统滚轮注入
WIN->>SVR: 10. {type:'key', action, key, code}
SVR->>HAV: 转发 key
HAV->>HAV: injectKeyEvent --> 系统键盘注入
Note over WIN,HAV: ═══ 剪贴板同步 ═══
WIN->>WIN: 11. Ctrl+V → PlanC 读取 Win 剪贴板
WIN->>SVR: {type:'clipboard', data: text}
SVR->>HAV: 转发 clipboard HAV->>HAV: pasteboard.setDataSync(text)
WIN->>SVR: 注入 Ctrl+V 按键序列
SVR->>HAV: 转发 Ctrl+V 按键
HAV->>HAV: 系统级 Ctrl+V 粘贴
HAV->>HAV: 12. 用户复制 → pasteboard.on('update')
HAV->>SVR: {type:'clipboard', data: text}
SVR->>WIN: 转发 clipboard
WIN->>WIN: navigator.clipboard.writeText()
Note over WIN,HAV: ═══ 保活 ═══
par 心跳保活
WIN->>SVR: {type:'ping'}
SVR-->>WIN: pong
and
HAV->>SVR: {type:'ping'}
SVR-->>HAV: pong
end
相关设计
- c++层使用
OH_AVScreenCapture的StartScreenCaptureWithSurface模式,录屏输出直接绑定到编码器的 Input Surface, 无任何拷贝避免了 YUV 数据在内存中来回拷贝。 - 编码器回调
OnCodecOutput通过napi_threadsafe_function将 NAL 数据从编码线程安全投递到 ArkTS 主线程中。 - 浏览器端使用 JMuxer 将裸
H.264 NAL单元封装为fMP4片段,交由 MSE(Media Source Extendions) 喂给<video>标签解码播放。
双向剪贴板同步
Plan C Paste 穿透 + @ohos.pasteboard + navigator.clipboard
- 在 DOM 中创建一个隐藏到屏幕外的
<textarea> - 用户按下
Ctrl+V时不调用preventDefault(),让浏览器执行原生粘贴 - 利用
e.clipboardData.getData('text/plain')在 paste 事件回调中读取剪贴板 - 向鸿蒙发送
clipboard消息 + 注入Ctrl+V按键序列化完成远程粘贴
防回环设计 :HarmonyOS ClipboardSyncHelper 使用 syncLock 标记,当自身写入粘贴板时加锁, pasteboard.on('update') 回调检测到锁则路过发送,避免无限回环。
服务
服务端是一个无业务逻辑的纯中继层,不做任何编解码,只根据 role 做房间内广播转发。
项目架构
bash
RemoteControl/
├── entry/src/main/
│ ├── cpp/ # C++ Native 层
│ │ ├── napi_init.cpp # NAPI 入口:录屏/输入注入 JS 绑定
│ │ ├── screen_capture/
│ │ │ ├── screen_capture_manager.h/cpp # 录屏 + H.264硬件编码
│ │ └── types/libentry/
│ │ └── index.d.ts # NAPI TypeScript 类型声明
│ └── ets/
│ ├── pages/
│ │ └── WebSocketTestPage.ets # 主页面:连接配置 + 日志面板
│ └── util/
│ ├── service/
│ │ ├── WebRemoteHandler.ets # 核心调度:连接/录屏/消息路由
│ │ ├── InputInjector.ets # 输入注入封装(坐标转换)
│ │ ├── ClipboardService.ets # 系统剪贴板读写
│ │ ├── ClipboardSyncHelper.ets # 剪贴板同步 + 防回环
│ │ └── WebSocketData.ets # 消息类型接口定义
│ └── backgroundTask/
│ └── BackgroundTaskHelper.ets# 长时任务申请
├── server/ # Node.js 中继服务器
│ ├── server.js # 入口:HTTP + WebSocket Server
│ ├── message-handler.js # 消息路由:按 type 分发
│ ├── room-manager.js # 房间管理:角色分组 + 广播
│ ├── client-manager.js # 客户端生命周期管理
│ ├── config.js # 配置管理 (端口/Token)
│ └── web/
│ ├── index.html # 控制端页面
│ └── controller.js # 核心控制逻辑
|__
连接成功的效果

总结
在这个功能实现的过程中我收获到了以下果实。
- H.264 NAL 单元、SPS/PPS/I帧/P帧之间的关系。
- 对浏览器
PointerEventAPI 的掌握上升了一个档次:buttonvsbuttons、pointerdownvsmousedown的区别、setPointerCapture的作用时机。 - 对鸿蒙
OH_Input_Manager注入 API 有了实操理解:ActionTime必须用GetBootTimeUs()微秒级时间戳,否则事件时序错乱。 - 浏览器安全策略不是"限制"是"设计"------
navigator.clipboard要求安全上下文是合理的。Plan C 是在这个前提下找一个合法的旁路,而不是打破它。 - 双向同步系统必须考虑回环问题,这是一个经典设计模式------任何 "写 --> 通知 --> 读 --> 写" 的循环链路都需要标记来源。
虽然实现的是局域网状态下的demo状态,收获也是满满!