首页
/ OpenScreen macOS 原生光标捕获测试管线:Helper 构建、冒烟测试与 Sidecar 验证实战

OpenScreen macOS 原生光标捕获测试管线:Helper 构建、冒烟测试与 Sidecar 验证实战

2026-09-05 11:22:27作者:魏献源Searcher

OpenScreen 在 macOS 上通过一个独立的 Swift helper 子进程(openscreen-macos-cursor-helper)捕获真实系统光标位图,并让位图光标进入编辑器与导出管线。本文基于仓库内的测试文档 macOS native cursor test pipeline,完整覆盖 helper 的工作原理、NDJSON 协议格式、构建与冒烟测试命令、macOS 权限矩阵、分优先级(P0/P1/P2)的手工测试清单,以及如何通过 .cursor.json sidecar 文件判定一次录制是否健康;并结合 Swift 源码TypeScript 会话实现 说明这些测试项背后的真实调用链,帮助你在 macOS 上独立搭建、诊断和回归验证这条原生光标捕获链路。

Helper 的工作原理:轮询、去重与点击事件

cursor helper(openscreen-macos-cursor-helper)在录制期间作为 Electron 的子进程运行,它做五件事:

  • 以配置的采样间隔轮询 NSCursor.currentSystem,拿到当前活动的 AppKit 系统光标;
  • 把每个光标图像编码为 PNG,并计算 SHA-256 内容哈希作为稳定的资产 id(assetId);
  • 每个唯一的光标形状在整场录制中只发送一次完整的 base64 位图负载,后续采样只携带 assetId,从而保持 stdout 体量很小;
  • 通过 CGEventTap(listen-only 模式)跟踪左键按下/抬起事件,为每个样本打上 interactionType 标签;
  • 在获得辅助功能(Accessibility)授权时,使用 Accessibility API 检测 text / pointer 亲和(affordance,即输入框/链接/按钮等角色),这类形状会用仓库自带的高质量 SVG 替换原始位图渲染。

main.swift 中可以直接看到这套实现:currentCursorAsset() 函数先用 NSCursor.currentSystem 取光标,转 NSBitmapImageRep 后编码 PNG,再用 SHA256.hash(data: png) 生成 id;scaleFactorpixelsWide / pointSize.width 推出(Retina 上为 2.0),hotspot 坐标会乘以 scaleFactor 转成像素单位,供渲染端再除回点(point)尺寸。主循环里的 emittedAssetIds: Set<String> 保证即使用户在 arrow → text → arrow 之间来回切换,同一形状也只序列化一次;整个采样循环包裹在 autoreleasepool 中,避免长录制时 Cocoa 对象堆积导致内存增长(这也是后文 P2 长录制内存检查项的依据)。

NDJSON 采样协议

helper 通过 stdout 输出按行分隔的 JSON(NDJSON),每行一个事件:

{ "type": "ready", "timestampMs": 1234567890, "accessibilityTrusted": true, "mouseTapReady": true }
{ "type": "sample", "timestampMs": 1234567891, "assetId": "a7472...", "asset": { "id": "a7472...", "imageDataUrl": "data:image/png;base64,...", "width": 64, "height": 64, "hotspotX": 16, "hotspotY": 16, "scaleFactor": 2.0 }, "cursorType": null, "leftButtonDown": false, "leftButtonPressed": false, "leftButtonReleased": false }
{ "type": "sample", "timestampMs": 1234567924, "assetId": "a7472...", "cursorType": null, "leftButtonDown": false, "leftButtonPressed": false, "leftButtonReleased": false }

注意 asset 字段只在某个 assetId 首次出现时携带。各字段含义:

字段 说明
type: "ready" helper 启动完成的握手事件,accessibilityTrusted 指示辅助功能是否已授权,mouseTapReady 指示 CGEventTap 是否建立成功
assetId 光标位图的 SHA-256 内容哈希,是位图资产的稳定标识
asset 仅首见时出现:imageDataUrl(base64 PNG)、像素宽高、像素单位 hotspot、scaleFactor
cursorType Accessibility 探测到的亲和类型("text" / "pointer"),无授权或非亲和形状时为 null
leftButtonDown / leftButtonPressed / leftButtonReleased 当前左键状态(CGEventSource.buttonState)、本采样周期内是否发生按下/抬起

TypeScript 端的 MacNativeCursorRecordingSession 按行解析这些事件,把唯一资产收集进 assets 的 Map,并在 stop() 时输出 provider: "native"(当且仅当至少捕获到一个位图),否则为 provider: "none"(纯位置遥测)。该数据结构与 contracts.ts 中的 CursorRecordingData / NativeCursorAsset 接口一一对应:version: 2providersamples[](归一化坐标 cx/cyvisibleinteractionType、可选 assetId/cursorType)、assets[]platform: "darwin"、hotspot、scaleFactor)。

会话层还有一个值得注意的细节:captureSample() 里会统计连续越界样本数,达到 OUTSIDE_HIDE_THRESHOLD = 3(33ms 间隔下约 100ms)才把 visible 置为 false——短暂滑出屏幕的快划会被渲染端按 clip-path 裁到画布边缘,而不是突然消失。这正是后文多显示器测试项要验证的行为。

构建 helper

构建命令一条:

npm run build:native:mac

它会同时构建两个 Swift helper(openscreen-screencapturekit-helperopenscreen-macos-cursor-helper),并复制到两个位置:

  • electron/native/screencapturekit/build/ —— 本地 dev server 使用;
  • electron/native/bin/darwin-arm64/darwin-x64/ —— 打包(packaged)构建使用。

对应实现是 build-macos-screencapturekit-helper.mjs:它先用 xcodebuild -version 检查完整 Xcode 是否处于激活状态(只有 Command Line Tools 会因缺少 SwiftPM 需要的 SDK/平台元数据而失败),然后对每个目标架构执行 swift build -c release --arch <arch>,再把产物拷入上述两处并 chmod 0755。构建脚本还支持 OPENSCREEN_MAC_HELPER_ARCHS 环境变量按架构矩阵构建(CI 用),每个架构产出独立的单架构二进制、分放在各自的 darwin-<arch> 目录,不生成 fat binary。

如果构建报错提示缺少 SDK 元数据,切换到完整 Xcode 并接受许可:

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept

另外注意 Package.swift 声明了 platforms: [.macOS(.v13)],即 helper 本身要求 macOS 13(Ventura)及以上,与后文原生录制的可用性规则一致。

直接冒烟测试 helper

在启动整个应用之前,可以先单独运行 cursor helper 观察原始输出:

BIN=electron/native/screencapturekit/build/openscreen-macos-cursor-helper
("$BIN" '{"sampleIntervalMs":100}' & PID=$!; sleep 2; kill $PID) | head -20

helper 把命令行第一个参数作为 JSON 请求解析(字段 sampleIntervalMs 可选,缺省 33ms,且被 max(8, ...) 钳制到不低于 8ms)。预期第一行输出:

{"type":"ready","mouseTapReady":true,"accessibilityTrusted":false,"timestampMs":...}

accessibilityTrusted: false 在开发/未签名构建中是正常现象——它意味着 text/pointer 亲和检测被禁用,但基于 NSCursor 的原生位图捕获仍然工作。

预期采样行:

{"type":"sample","assetId":"a7472...","asset":{"id":"a7472...","imageDataUrl":"data:image/png;base64,...","width":64,"height":64,"hotspotX":26,"hotspotY":16,"scaleFactor":2.0},...}
{"type":"sample","assetId":"a7472...",...}

在 helper 运行期间把光标移到一个文本输入框上方(需已授权 Accessibility),应看到出现一个新的 assetId 且位图不同。对应源码路径:currentCursorType() 先检查 AXIsProcessTrusted(),未授权直接返回 nil;授权后调用 AXUIElementCopyElementAtPosition 取鼠标下的元素,沿最多 5 层父链向上找角色——文本类角色(AXTextField / AXTextArea / AXTextView / AXComboBox)返回 "text",指针类角色(AXLinkAXButtonAXMenuButtonAXCheckBoxAXTabAXMenuItem 等)返回 "pointer";其余情况返回 nil,渲染端回退到原生捕获的位图——这正是默认箭头与自定义光标能以真实图像呈现的机制。

让应用使用自定义 helper 二进制

在本地诊断时,可以用环境变量把会话指向自编译的 helper:

export OPENSCREEN_MAC_CURSOR_HELPER_EXE=/path/to/openscreen-macos-cursor-helper
npm run dev

macNativeCursorRecordingSession.tshelperCandidates() 可以看到完整的解析优先级:

  1. OPENSCREEN_MAC_CURSOR_HELPER_EXE 环境变量;
  2. electron/native/screencapturekit/build/openscreen-macos-cursor-helper(dev 路径);
  3. electron/native/bin/<arch>/openscreen-macos-cursor-helper(源码树内的打包路径);
  4. resources/electron/native/bin/<arch>/...(打包应用的 process.resourcesPath 下)。

按顺序探测第一个可执行(X_OK)的文件,全部失败则返回 null,会话转入"仅位置"回退模式(startPositionOnlyFallback(),只记录鼠标坐标、不捕获位图)。另外会话启动时会用 READY_TIMEOUT_MS = 5_000(5 秒)等待 ready 事件,超时或子进程提前退出都会触发同样的回退并打印 [cursor-macos] falling back to position-only cursor telemetry 警告。

macOS 权限:两项独立授权

这条链路需要两个相互独立的权限:

权限 作用 授权位置
Screen Recording ScreenCaptureKit 视频捕获 System Settings → Privacy & Security → Screen & System Audio Recording → Electron ✅
Accessibility text / pointer 光标类型检测(affordance 提示) System Settings → Privacy & Security → Accessibility → Electron ✅
  • Screen Recording 是必需的:没有它,录制根本不会开始(原生录制的三项可用性规则之一)。
  • Accessibility 是可选的:没有它,cursorType 永远为 null,所有光标都从捕获到的位图渲染(不做 SVG 替换)。对非 text/pointer 形状而言这不是质量损失,而是预期的回退行为。

授予任一权限后,必须完全退出并重启 dev server——getMediaAccessStatus 按进程缓存结果。还有一个更可靠的信号:文档特别指出,在未签名的 dev 构建中 getMediaAccessStatus("accessibility") 可能不反映实际开关状态,应以 helper 在 ready 事件中上报的 accessibilityTrusted 为准(对应源码中 requestAccessibilityTrust() 调用 AXIsProcessTrustedWithOptions 的探测结果)。

手工测试清单

以下清单按优先级组织,覆盖核心捕获、亲和替换、热点对齐、点击检测、优雅降级、多显示器与长录制内存。

P0 — 核心位图捕获

  • [ ] 录制一段短视频,打开编辑器,确认默认箭头光标是真实系统箭头(而不是仓库自带的 SVG 近似图)。
  • [ ] 在悬停网页浏览器时录制,确认自定义 CSS 光标(如 cursor: grabcursor: crosshair)以其实际形状出现。
  • [ ] 导出 MP4,确认导出视频中的光标渲染正确。
  • [ ] 导出 GIF,做同样的检查。

P1 — 亲和替换(需要 Accessibility)

  • [ ] 授予 Accessibility 权限并重启应用。
  • [ ] 录制悬停文本输入框,确认 text I-beam 使用的是仓库自带 SVG 版本(比系统位图更精致)。
  • [ ] 录制悬停链接/按钮,确认 pointer 手型使用自带 SVG。

P1 — 热点对齐(Retina)

  • [ ] 在 Retina 显示器上录制一次对小型按钮的精确点击,在编辑器中确认光标尖端与实际点击点重合。helper 上报 scaleFactor: 2.0,渲染器会把像素尺寸和 hotspot 除以该值恢复为点(point)尺寸——即 Swift 端 hotSpot.x * scaleFactor 的逆运算。

P1 — 点击检测

  • [ ] 录制若干次左键单击,确认编辑器中每次点击都触发点击回弹(click-bounce)动画。
  • [ ] 确认录制会话 sidecar(<videoPath>.cursor.json 内的 cursorRecordingData)中存在 interactionType: "click""mouseup" 事件。

sidecar 中 interactionType 的判定在 macNativeCursorRecordingSession.tscaptureSample() 里:本周期出现按下(leftButtonPressed)或状态由松变按记为 "click";出现抬起或由按变松记为 "mouseup";其余为 "move"click/mouseup 成对出现意味着 CGEventTap 事件流完整。

P2 — 优雅降级

  • [ ] 移走两份构建产物位置的 helper 二进制后开始录制,会话应当以 provider: "none" 成功(仅位置遥测、渲染默认箭头),之后再恢复两份二进制:

    ARCH=$([ "$(uname -m)" = "arm64" ] && echo darwin-arm64 || echo darwin-x64)
    mv electron/native/screencapturekit/build/openscreen-macos-cursor-helper /tmp/cursor-helper-build
    mv electron/native/bin/$ARCH/openscreen-macos-cursor-helper /tmp/cursor-helper-bin
    # ... 开始录制,然后恢复:
    mv /tmp/cursor-helper-bin electron/native/bin/$ARCH/openscreen-macos-cursor-helper
    mv /tmp/cursor-helper-build electron/native/screencapturekit/build/openscreen-macos-cursor-helper
    
  • [ ] 撤销 Accessibility 授权,确认录制仍可用、光标从位图渲染(无 SVG 替换)。

这一项验证的正是 findMacCursorHelperPath() 全部候选失败后 startPositionOnlyFallback() 的路径:回退模式按同一采样间隔持续记录鼠标位置,stop() 时因 assets 为空而输出 provider: "none"

P2 — 多显示器

  • [ ] 录制期间把光标移到副显示器,确认光标被裁切(clip)到画布边缘、而不是快速划动时突然消失,并在连续约 100ms 越界后隐藏。

对应实现即前文提到的 consecutiveOutsideSamples 计数与 OUTSIDE_HIDE_THRESHOLD = 3:短暂越界走 clip-path 裁切,持续越界才置 visible: false,避免多屏移动产生的残影光标与运动拖尾。

P2 — 长录制内存

  • [ ] 在浏览器、终端、编辑器之间切换并录制 3–5 分钟。helper 不应内存增长,因为每次循环通过 autoreleasepool 排空 Cocoa 对象。用 Activity Monitor 观察 openscreen-macos-cursor-helper 的 RSS 应在头几秒后保持平稳。

健康录制的 Sidecar 长什么样

检查与录制视频一同写出的光标 sidecar 文件:视频保存为 /tmp/rec.mp4 时,sidecar 即 /tmp/rec.mp4.cursor.json(IPC 层以 `${videoPath}.cursor.json` 生成该路径,见 handlers.ts):

{
  "version": 2,
  "provider": "native",
  "assets": [
    { "id": "a7472...", "platform": "darwin", "imageDataUrl": "data:image/png;base64,...", "width": 64, "height": 64, "hotspotX": 26.0, "hotspotY": 16.0, "scaleFactor": 2.0 }
  ],
  "samples": [
    { "timeMs": 0, "cx": 0.42, "cy": 0.38, "visible": true, "assetId": "a7472...", "interactionType": "move" },
    ...
  ]
}

判读标准:

  • provider: "native"assets 非空 → 位图捕获处于活动状态;
  • provider: "none"assets: [] → helper 未找到,或在发出 ready 之前就退出了(可结合启动日志中的 falling back to position-only 警告定位)。

samples 中的 cx/cy 是相对录制显示区边界的归一化坐标(0–1),由 screen.getCursorScreenPoint() 减去显示区原点再除以宽高得到,并做 clamp(0,1)

原生 macOS 捕获后端与视频/光标分离

OpenScreen 在 macOS 上会把录制路由到 ScreenCaptureKit helper(openscreen-screencapturekit-helper),使真实系统光标不出现在视频帧内;光标的位置与位图由 cursor helper 独立捕获,最终在编辑器与导出管线中合成。这条路由的可用性规则为:

  • macOS 13(Ventura)或更新版本;
  • openscreen-screencapturekit-helper 二进制存在;
  • 已授予 Screen Recording 权限。

构建两个 helper 仍是 npm run build:native:mac;本地诊断自定义 cursor helper 二进制时同样使用 OPENSCREEN_MAC_CURSOR_HELPER_EXE 环境变量叠加 npm run dev。这个"视频去光标、位图单独采样、后期合成"的架构意味着两条链路可以独立调试:视频侧问题看 SCK helper 的 warning/error 事件,光标侧问题看 cursor helper 的 NDJSON 输出与 sidecar 文件。

已知限制

  • Intel(x86_64)Mac:分发的 helper 构建目标是 darwin-arm64。Intel Mac 需要在目标机器上用 npm run build:native:mac 从源码构建(构建脚本会按 process.arch 选择 x86_64 并输出 darwin-x64 目录)。
  • 未签名/dev 构建中的 AccessibilitygetMediaAccessStatus("accessibility") 对 dev 模式下的未签名 Electron 可能不反映开关状态。helper 始终会自行探测并在 ready 事件中上报 accessibilityTrusted——应把它当作权威信号。
  • 应用自定义光标(CGS 层)NSCursor.currentSystem 捕获的是活动 AppKit 光标;某些游戏或 GPU 加速应用通过 CoreGraphics/CGS 层设置的光标在这里可能不可见,这是 macOS API 的已知限制。

小结

这条测试管线的核心思路是"分层验证":先用 standalone 冒烟命令确认 helper 的 NDJSON 协议与 ready/sample 事件,再用 sidecar 文件核对 providerassetsinteractionType 是否完整,最后按 P0→P2 清单覆盖位图真实性、SVG 亲和替换、Retina 热点对齐、点击事件、降级行为、多显示器与长录制内存。每一步的"预期值"都能直接对应到 main.swift 的采样实现与 macNativeCursorRecordingSession.ts 的解析/降级逻辑,使得任何一步失败时都能快速区分是 helper 侧(Swift 进程、权限)还是会话侧(TS 解析、路径解析)的问题。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384