HarmonyOS 6.0跨端远程控制

各位掘友们好,前面发表过自己在学习鸿蒙时总结的一些小笔记,而这篇则是我学习的实现!

我就卖关子了,直接开门开门见山!我利用AI的帮助实现了跨端远程控制。可以在win/mac系统电脑上控制鸿蒙的2in1/pc设备。具体如何实现的呢?请听我一一道来。

背景

那天老大语重心长的对我说,小林啊,客户那边需求有一个远程控制的功能,我一听心想这很简单市面上那么多,多看几款把功能仿出来不就行了。后面老大一说具体需求给我听的一愣一愣的,什么!在win/mac系统通过浏览器去控制鸿蒙设备。说完老大给我讲了一下大概思路然后眼神坚定的看着我,没办法还是硬着头皮接下这个任务,(也可能是年轻觉得别能做自己也能做!)。

相关技术栈

  1. win(控制侧):JavaScript、Html、Css3、JMUXER视频渲染库
  2. 服务:Node.js
  3. HarmonyOS(被控制侧):C++、ArkTS

功能

  1. 基本键鼠操作
  2. 双向复制粘贴

整体逻辑

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_AVScreenCaptureStartScreenCaptureWithSurface 模式,录屏输出直接绑定到编码器的 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

  1. 在 DOM 中创建一个隐藏到屏幕外的<textarea>
  2. 用户按下 Ctrl+V 时不调用 preventDefault(),让浏览器执行原生粘贴
  3. 利用 e.clipboardData.getData('text/plain') 在 paste 事件回调中读取剪贴板
  4. 向鸿蒙发送 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          # 核心控制逻辑
|__

连接成功的效果

总结

在这个功能实现的过程中我收获到了以下果实。

  1. H.264 NAL 单元、SPS/PPS/I帧/P帧之间的关系。
  2. 对浏览器 PointerEvent API 的掌握上升了一个档次:button vs buttonspointerdown vs mousedown 的区别、setPointerCapture 的作用时机。
  3. 对鸿蒙 OH_Input_Manager 注入 API 有了实操理解:ActionTime 必须用 GetBootTimeUs() 微秒级时间戳,否则事件时序错乱。
  4. 浏览器安全策略不是"限制"是"设计"------navigator.clipboard 要求安全上下文是合理的。Plan C 是在这个前提下找一个合法的旁路,而不是打破它。
  5. 双向同步系统必须考虑回环问题,这是一个经典设计模式------任何 "写 --> 通知 --> 读 --> 写" 的循环链路都需要标记来源。

虽然实现的是局域网状态下的demo状态,收获也是满满!

相关推荐
枫树下x2 小时前
NestJS基础框架
前端
胡志辉2 小时前
从v8源码和react深入浅出理解 JavaScript 作用域链与闭包
前端·javascript
天蓝色的鱼鱼2 小时前
React Router v8 来了:react-router-dom 没了,老项目该怎么迁移?
前端·react.js
苏三说技术3 小时前
全网爆火的Loop到底是什么?
后端
神奇小汤圆3 小时前
Loop Runtime 架构拆解:别再手动催 Agent,先把工程闭环跑起来
后端
程序员cxuan3 小时前
幽默,一个 Github 名字叫“马尾辫”,但是他给你省了 80% 的 token
人工智能·后端·程序员
程序员晓琪3 小时前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
银卡3 小时前
RAG Embedding 模型选型
后端