从零到开源:用 AI 驱动构建 macOS 鸿蒙投屏工具的踩坑全记录

lxiol
📝
缘起:鸿蒙生态缺失的那块拼图 做 Android 开发的人都知道 scrcpyhttps://github。com/Genymobile/scrcpy——USB 一插,手机画面就出现在电脑屏幕上,还能直接操控。iOS 有 QuickTime 投屏

缘起:鸿蒙生态缺失的那块拼图

做 Android 开发的人都知道 scrcpy——USB 一插,手机画面就出现在电脑屏幕上,还能直接操控。iOS 有 QuickTime 投屏。但 HarmonyOS 呢?只能在 DevEco Studio 那个笨重的 IDE 里开一个小窗口,延迟高、不可交互、体验糟心。

于是就有了 HarmonyMirror:一个 macOS 原生的鸿蒙设备投屏与控制工具。本文记录这个项目从零到开源的全过程——包括那些翻车时刻和 AI 编程的真实体验。


技术选型:为什么是 Swift + SwiftUI

第一反应是用 Electron。跨平台、生态好、开发快。

但投屏这个场景对延迟极度敏感——视频解码、触摸注入、窗口渲染,每一层都是性能瓶颈。Electron 的 Chromium 渲染层天然多了一层开销。而且鸿蒙开发工具链(hdc、DevEco Studio)都在 macOS 上运行得最好。

最终选了 Swift + SwiftUI 构建原生 macOS 应用:

  • AVSampleBufferDisplayLayer 做硬件解码,直接走 VideoToolbox
  • NWConnection (Network.framework) 处理 TCP 通信
  • SwiftUI 构建 UI,原生性能和动画

这是一个用纯 AI(Claude Code)驱动的项目,从第一天起就没写过一行代码,全靠 prompt。


架构设计:七层协议的”翻译官”

整个系统的核心是把手机画面搬到 Mac 屏幕上,同时把 Mac 的鼠标键盘操作送回手机。听起来简单,实际上经历了七层协议转换:

1
2
3
4
5
6
7
8
手机屏幕
→ H.264 编码 (libscreen_casting.z.so)
→ gRPC 传输 (Unix socket)
→ hdc 端口转发 (USB/Wi-Fi)
→ Python Bridge (gRPC → TCP)
→ TCPStreamReceiver (自定义帧协议)
→ H264VideoLayer (NAL 解析 → CMSampleBuffer)
→ AVSampleBufferDisplayLayer (硬件解码)

每一层都有坑,下面按时间线记录踩坑过程。


第一坑:视频流——从截图到实时流的蜕变

最初方案(已废弃):通过 hdc shell snapshot_display 定时截图,WebSocket 传到 Mac 端显示。

结果呢?每 500ms 一张截图,肉眼可见的幻灯片效果。CPU 占用爆表,截图命令本身就要 300ms+。

推翻重来:研究华为投屏扩展 libscreen_casting.z.so——这是 DevEco Testing 内置的投屏库,通过 gRPC 输出 H.264 实时编码流。

最大的挑战是 gRPC 协议桥接。Python bridge(deveco_cast_bridge.py)负责把 gRPC 数据中转成纯 TCP 帧,期间踩了无数坑:

  • Annex-B vs AVCC:H.264 两种封装格式,设备发出来的和 macOS 期望的可能不一样。写了个自适应检测器,先扫描起始码 0x00000001,没有就走 AVCC 长度前缀路径。
  • SPS/PPS 提取:不提取序列参数集和图像参数集,CMSampleBuffer 创建就失败,直接黑屏。
  • 关键帧等待:I 帧间隔 2 秒,连接时刚好错过就得等 2 秒。做了 bridge 60 秒延迟复用机制,重连同一设备时秒连。

第二坑:触摸延迟——从 100ms 到 2ms

触摸输入一开始直接用 hdc shell uinput -T -c x y。每次点击都要启动一个完整的 shell 进程——100ms 延迟。

这什么概念?你手指在触控板上划一下,100ms 后手机才开始响应。拖拽窗口时肉眼可见的跳跃感。

HarmonyAgent 方案:在手机上部署一个 C 语言写的常驻进程,通过 TCP 接收 Mac 端发来的 8 字节二进制触摸帧:

1
2
3
4
5
6
7
struct TouchFrame {
uint8_t cmd; // TOUCH_DOWN / TOUCH_UP / TOUCH_MOVE
uint8_t slot; // 手指编号 (支持多点触控)
uint16_t x; // X 坐标 (0~65535 归一化)
uint16_t y; // Y 坐标
uint16_t reserved; // 压力值 / 触摸面积
} __attribute__((packed));

Agent 收到帧后直接 write()/dev/uinput,延迟从 100ms 降到 <2ms

这套方案的开发过程也踩了不少坑:

  • 交叉编译:ARM aarch64 二进制用什么工具链?最后用 HarmonyOS NDK 交叉编译。
  • 自动部署hdc file send 推送到 /data/local/tmp/chmod +x,后台启动。每次投屏自动检测 Agent 是否已部署。
  • 健康检查:ping/pong 心跳,15 秒无响应自动重连。
  • 多点触控ABS_MT_PRESSUREABS_MT_TOUCH_MAJOR、独立 tracking ID,完整的 10 指多点触控协议。

最妙的是 fallback 策略

1
2
Agent TCP → hdc shell uinput → hdc shell uitest
↓ 失败自动降级 ↓ 再失败继续降级

不管什么场景,总有一条路走通。

第三坑:锁屏/安全屏幕——安卓和鸿蒙共同的”叹息之墙”

这是最让人头疼的问题。HarmonyOS(和 Android 一样)对锁屏、密码输入、支付等敏感界面有 FLAG_SECURE 保护:

  1. 录屏被屏蔽——投屏画面黑屏
  2. 触摸注入被拒绝——uinput 事件直接丢弃

华为自家的”远程协同”为什么可以?因为它跑在 DSoftBus 系统服务通道上,经过安全芯片认证,权限完全不同。

我们尝试了三条突破路径:

层级 1:在 Agent 上设置 INPUT_PROP_DIRECT 设备属性,模拟直接输入设备。

层级 2:绕过 uinput,直接往 /dev/input/eventXinput_event 结构体。

层级 3(理论可行但未落地):研究 OpenHarmony 开源的 DSoftBus 代码,逆向分析 libnstackx.so

是的,我们甚至用 Ghidra 逆向分析了鸿蒙系统库。在 DSoftBusResearch/ 目录下有完整的 Ghidra 分析指南和 CoAP 消息格式的研究笔记。虽然最终因为 DSoftBus 需要设备信任认证 + 加密通道,macOS 端实现工作量太大而暂停,但这个过程本身就是一次非常硬核的技术探索。

目前方案:建议用户用”滑动解锁”,Agent 可以通过唤醒按钮点亮屏幕后,用户在物理设备上完成解锁,画面自动恢复。

第四坑:稳定性的”死亡一千刀”

功能跑通只是开始,稳定性才是真正考验。以下是几个差点让人崩溃的 bug:

黑屏 Bug

现象:TCP 连接成功,数据在传输,屏幕就是黑的。

排查了整整一个晚上,最后发现是 H264VideoLayer 里一行看似无害的判断:

1
2
3
4
5
// Bug: 在 layer 还没挂到视图层级时就丢弃了帧
guard layer.superlayer != nil else { return }

// Fix: AVSampleBufferDisplayLayer 自己会缓冲帧
guard let layer = self._displayLayer else { return }

AVSampleBufferDisplayLayer 在未添加到视图层级时依然可以接收帧并缓存——它会在挂载后自动渲染。提前丢弃导致前几帧全丢了,而由于关键帧间隔 2 秒,画面就一直黑着。

崩溃 Bug

HDCCommand.swift 里进程超时被 terminate() 后,后续代码尝试读取已关闭的 pipe,readDataToEndOfFile() 直接抛异常崩溃。

修复简单但教训深刻:异步 IO 的边界条件处理是 Bug 高发区

并发 Bug

15+ 个线程同时扫描局域网端口,CPU 飙到 100%。修复是引入 Actor 隔离和并发限制。

窗口 Bug

多显示器 + Spaces + 全屏状态切换后,窗口经常”消失”——实际是落在了不可见的坐标区域。修复是在启动时清理旧 frame 并把窗口强制拉回主屏可见区域。


AI 编程的真实体验

这个项目 100% 由 Claude Code 完成。说几个真实感受:

好的一面

  • 速度惊人:从”鸿蒙能不能投屏到 Mac”这个想法到第一个能用的原型,只用了几天。AI 直接输出可运行的 Swift 代码、C 代码、Python 脚本。
  • 跨语言无压力:Swift 写 Mac 端、C 写设备端 Agent、Python 写协议桥接、Shell 写自动化脚本——AI 在四种语言间无缝切换,不需要我切换脑子。
  • 架构建议有价值:当我说”触摸延迟太高”,AI 直接提出了设备端常驻进程 + TCP 二进制协议的一整套方案,包括交叉编译、部署策略和 fallback 机制。这种从需求 → 架构设计 → 实现细节的端到端推理能力,是 AI 编程最让我惊喜的地方。

真实痛点

  • 幻觉问题:AI 偶尔会”发明”不存在的 API。比如它用过 AVPlayer.play() async 这种不存在的异步方法。需要每段代码都实际编译验证。
  • 上下文衰减:长对话中 AI 会”忘记”之前的设计决策。超过 100 轮对话后,需要频繁提醒它”Agent 的端口转发已经在 MirrorService 里做了”。
  • 调试靠人:AI 能写出 90% 正确率的代码,但剩下的 10%(通常是并发问题、内存管理、硬件兼容性)需要人去排查。黑屏 Bug 排查了一晚上,AI 给出的所有”直接原因”都不对,最后是自己逐行 review 代码找到的。
  • 需要领域知识:如果我不懂 H.264 的 Annex-B vs AVCC 格式差异,AI 说”格式不兼容”时我完全不知道它是不是在胡扯。VibeCode 的前提是你对所做领域有基本判断力。

开源发布:双仓库架构

考虑到开发过程中大量实验性代码和真实测试环境信息(IP、设备序列号等),最终采用双仓库架构

  • 私有仓库 (HarmonyMirror):完整开发历史 + 所有实验代码
  • 公开仓库 (HarmonyMirror-macOS):脱敏处理后的干净版本,main 分支含完整功能,no-agent 分支是纯 hdc 方案(不含 Agent)

脱敏处理包括:替换真实 IP、移除设备特定信息、清理 Git 历史中的敏感 commit。


同目录下的其他项目

harmony/ 目录下还躺着几个”兄弟项目”:

  • Kimi_Agent:早期用 Kimi 对接鸿蒙投屏的实验项目,主要探索不同的 AI Agent 接入方式
  • Kimi_Agent_HarmonyMirror:Kimi Agent + HarmonyMirror 的融合尝试,包含 macOS 原生方案和 TypeScript 方案两个方向
  • DevEcoCastMac / DevEcoCastMac_2:早期的 DevEco Cast 协议分析项目,为后续的投屏实现奠定了基础
  • 多个 backup 目录:每次大重构前都会留一份备份——事实证明这个习惯救了好几次命

这些项目之间不是隔离的——DSoftBus 的研究在 HarmonyMirror 中复用,Kimi Agent 的协议设计思路影响了 HarmonyAgent,早期 DevEcoCastMac 的反编译分析笔记直接变成了 DESIGN.md 中”安全屏幕专题”的素材。


总结

开源地址:github.com/lxiol-star/HarmonyMirror-macOS

这个项目让我对 AI 编程有了更清醒的认识:AI 是放大器,不是魔法棒。它能 10x 加速”已知路径”上的开发,但遇到真正的未知问题时(安全屏幕突破、DSoftBus 逆向),AI 提供的是探索方向而不是答案。

如果你也在用 AI 做类似的项目,几点建议:

  1. 每个阶段都留备份——AI 的一次错误重构可能毁掉几个小时的成果
  2. 先编译,再信任——不要假设 AI 写的代码能跑
  3. 保留设计文档——DESIGN.md 是 AI 最大的”记忆外挂”
  4. 不要跳过 debug 环节——AI 写的 Bug 往往比人写的更难找,因为你会下意识信任它
  • 标题: 从零到开源:用 AI 驱动构建 macOS 鸿蒙投屏工具的踩坑全记录
  • 作者: lxiol
  • 创建于 : 2026-04-28 23:00:00
  • 更新于 : 2026-05-12 16:47:34
  • 链接: https://blog.lxiol.cn/2026/04/28/harmonymirror-ai-driven-development/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。