首页
/ OpenScreen 原生录屏 Helper:Windows WGC 与 macOS ScreenCaptureKit 的进程契约实现详解

OpenScreen 原生录屏 Helper:Windows WGC 与 macOS ScreenCaptureKit 的进程契约实现详解

2026-09-05 22:10:57作者:姚月梅Lane

OpenScreen 的本地录屏能力由两个独立的原生进程承载:Windows 上基于 Windows Graphics Capture(WGC)的 C++ helper,以及 macOS 上基于 ScreenCaptureKit 的 Swift helper。它们与 Electron 主进程之间通过"单个 JSON 请求 + 换行分隔 JSON 事件 + stdin 控制命令"的进程级契约通信,将采集、编码、混流全部移出渲染进程,使"可编辑光标"录屏不再把系统光标烧进画面。读完本文,你可以理解这套跨平台 native capture helper 的完整工作流、二进制解析优先级、V2 JSON 请求格式、stdin 控制协议与事件流,并能在对应系统上构建 helper、跑通冒烟测试、手动验证指定麦克风/摄像头。

一、整体架构:Electron 编排,Helper 负责采集编码

无论 Windows 还是 macOS,原生录屏都遵循同一个进程边界模型(见 electron/native/README.md):

  1. Electron 负责解析选中的录屏源(显示器或窗口)、输出路径,以及用户选择的麦克风/摄像头设备;
  2. 启动 helper 子进程,传入一个结构化的 JSON 请求;
  3. Helper 完整拥有采集、计时(含暂停/恢复的时间对齐)、编码和混流(muxing);
  4. Electron 持久化最终的媒体文件/会话清单(session manifest),并显式上报 helper 的错误。

这个边界在源码中可以直接对应。以 macOS 为例,主进程在 electron/ipc/handlers.tsstart-native-mac-recording IPC 处理器中:先确认平台是 darwin、没有已在运行的 helper,再定位 helper 二进制,然后用 spawn(helperPath, [JSON.stringify(config)]) 以管道 stdio 启动子进程——即"一个 JSON 参数 + 管道 stdio"的进程契约在两端是对称的。Windows 侧的对应逻辑在 electron/ipc/handlers.ts,同样是 spawn(helperPath, [JSON.stringify(config)]),且额外带 windowsHide: true 隐藏控制台窗口。

Helper 本身是"哑"的:它不感知 Electron 的窗口管理、i18n 或项目结构,只负责把采集到的像素和音频按时序写入 MP4。这种设计让两个平台的 helper 可以各自用最优语言与 API(Swift + ScreenCaptureKit / C++ + Windows.Graphics.Capture + Media Foundation)实现,而 Electron 侧的编排代码保持统一形态。

二、macOS 侧:ScreenCaptureKit Helper

2.1 二进制解析优先级

macOS 的 ScreenCaptureKit helper 按以下顺序解析(electron/native/README.md):

  1. 环境变量 OPENSCREEN_SCK_CAPTURE_EXE,用于本地开发与诊断;
  2. electron/native/screencapturekit/build/openscreen-screencapturekit-helper,本地构建的 Swift 输出;
  3. electron/native/bin/darwin-arm64/openscreen-screencapturekit-helperelectron/native/bin/darwin-x64/openscreen-screencapturekit-helper,打包后的预构建 helper。

源码中这一优先级在 electron/ipc/handlers.tsgetNativeMacCaptureHelperCandidates / findNativeMacCaptureHelperPath 中逐条落地:先取 OPENSCREEN_SCK_CAPTURE_EXE,再按 process.arch 计算 darwin-arm64 / darwin-x64 目录,最后对每个候选路径执行 fs.access(candidate, X_OK) 检查可执行性,取第一个可用的。打包态下还会额外检查 resources/electron/native/bin/darwin-${arch}/ 路径(resolvePackagedResourcePath),非打包态则走 .asar.unpacked 替换(resolveUnpackedAppPath),确保原生二进制不会被压缩进 asar。

2.2 构建命令与非 macOS 宿主的行为

构建命令:

npm run build:native:mac

该脚本对应 package.json 中的 node scripts/build-macos-screencapturekit-helper.mjs。行为约定(README 原文):

  • 在非 macOS 宿主上:命令直接成功退出,不影响 Windows/Linux 上的开发流程——这保证了 CI 与跨平台开发者可以无差别执行完整构建链;
  • 在 macOS 上:构建 electron/native/screencapturekit 下的 Swift 包,开发产物写入 electron/native/screencapturekit/build,可再分发的二进制拷贝到 electron/native/bin/darwin-${arch}

electron/native/screencapturekit/Package.swift 可以看到,这个 Swift 包(swift-tools-version 5.9,platforms 为 .macOS(.v13))实际产出两个可执行产物:

  • openscreen-screencapturekit-helper(目标 OpenScreenScreenCaptureKitHelper),即录屏主 helper;
  • openscreen-macos-cursor-helper(目标 OpenScreenMacOSCursorHelper),即光标形状辅助工具。

2.3 能力边界:当前版本支持什么

README 明确给出了当前 helper 的能力清单:

  • 显示器/窗口的 ScreenCaptureKit 视频采集;
  • 通过 SCStreamConfiguration.showsCursor 实现光标排除;
  • H.264 编码、MP4 混流;
  • ScreenCaptureKit 系统音频采集;
  • 当运行中的 macOS 版本暴露该能力时,尝试 ScreenCaptureKit 原生麦克风采集;
  • 摄像头录屏仍留在 Electron 侧作为 sidecar,在原生屏幕采集停止后附加到同一录制会话。

这些描述与 electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift 的实现吻合:makeStreamConfigurationconfiguration.showsCursor = !request.video.hideSystemCursor(即可编辑光标模式下由 Electron 采样、后期叠加,而不是烧进画面)、minimumFrameInterval 由请求的 fps 换算、queueDepth = 6、像素格式 kCVPixelFormatType_32BGRA、音频 sampleRate = 48000channelCount = 2,并且 excludesCurrentProcessAudio = true 排除本进程自身音频以免自激。原生麦克风采集通过 SCStreamOutputType(rawValue: 2) 这种"版本号探测式"方式接入——只有当前 macOS 版本存在该 output type 时才启用,否则发出 microphone-unavailable 警告并降级(对应 README 说的"尝试原生麦克风采集")。

2.4 能力探测与 missing-helper

Electron 暴露 IPC 通道 is-native-mac-capture-available 供前端探测能力,实现见 electron/ipc/handlers.ts:非 darwin 平台直接返回 reason: "unsupported-platform";darwin 平台上复用上述 helper 解析逻辑,找不到可用二进制就返回 reason: "missing-helper"。一旦可用,macOS 的屏幕/窗口采集就路由到原生 helper,使可编辑光标录屏不把系统光标烧进视频;光标位置本身仍在 Electron 侧采样(约 33ms 间隔,见 handlers.ts 中的 CURSOR_SAMPLE_INTERVAL_MS = 33)。当光标 helper 可用且系统授予了辅助功能(Accessibility)权限时,采样点还会被打上 link/text 光标提示(如 pointer),供编辑器渲染正确的指针形态。若权限缺失,request-native-mac-cursor-access 处理器会弹出原生对话框并深链到系统设置的辅助功能面板(electron/ipc/handlers.ts)。

完整的契约定义、分阶段推进计划与 SSOT 规则见 macOS 原生录屏路线图

2.5 macOS 的事件输出

Swift helper 的所有输出走同一个 emit 函数(main.swift):把字典序列化为单行 JSON 后 printfflush(stdout),保证 Electron 侧能逐行实时解析。事件流包括:

  • readyschemaVersion: 1,流配置完成、即将 startCapture);
  • recording-started(首个完整视频帧写入 writer 后,附带实际输出的 width/height);
  • recording-paused / recording-resumed(附 timestampMs,暂停期间丢弃所有采样缓冲,恢复后对 presentation time 做偏移对齐,见 retimedSampleBuffer);
  • warning(如 microphone-unavailable、停止采集失败);
  • error(附 codemessage,例如权限被拒、找不到显示器/窗口)。

Electron 侧还会在请求中强制 webcam.enabled = false 并附上 manifestPathrecording-<id>.session.json),与 README 中"Electron 持久化媒体/会话清单"的步骤对应。

三、Windows 侧:WGC Helper

3.1 二进制解析优先级

Windows 原生录屏从以下位置解析(electron/native/README.md):

  1. 环境变量 OPENSCREEN_WGC_CAPTURE_EXE,用于本地开发与诊断;
  2. electron/native/wgc-capture/build/wgc-capture.exe,本地 Ninja 构建的 helper;
  3. electron/native/wgc-capture/build/Release/wgc-capture.exe,本地多配置构建的 helper;
  4. electron/native/bin/win32-x64/wgc-capture.exeelectron/native/bin/win32-arm64/wgc-capture.exe,打包后的预构建 helper。

实现位于 electron/ipc/handlers.tsgetNativeWindowsCaptureHelperCandidates 按 env → build/Release → build → bin/ 生成候选列表(打包态额外检查 resources 下的同构路径),findNativeWindowsCaptureHelperPath 依次做可执行性检查。注意候选顺序与 README 略有实现差异:源码中 build/Releasebuild 之前被探测,两者都是"本地构建"路径,不影响可用性。

3.2 构建命令

npm run build:native:win

对应 package.jsonnode scripts/build-windows-wgc-helper.mjs。构建把 CMake 输出写到 electron/native/wgc-capture/build/wgc-capture.exe,并把可再分发的二进制拷贝到 electron/native/bin/win32-x64/wgc-capture.exe。CMake 工程入口是 electron/native/wgc-capture/CMakeLists.txt,源码模块划分清晰:

3.3 进程契约:单 JSON 参数 + stdin 命令 + 双通道事件

README 对契约的定义是:应用以"一个 JSON 参数 + stdin 命令"启动进程,stop\n 结束录制;迁移期间 helper 同时输出换行分隔的 JSON 事件和传统文本消息 Recording started / Recording stopped. Output path: <path>

源码逐条印证:

  • stdin 命令循环在 electron/native/wgc-capture/src/main.cpp 中按行读取,stopqquit 均触发停止;
  • 启动成功后打印 Recording started 文本(main.cpp),Electron 侧的 waitForNativeWindowsCaptureStartelectron/ipc/handlers.ts)正是监听这个文本(12 秒超时),并在开始时刻记录 cursorStartTimeMs 与采集启动的偏移量,保证后期叠加的光标采样与视频时间轴对齐;
  • 停止路径解析 Recording stopped. Output path: <path> 文本(15 秒超时,NATIVE_WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 15_000);
  • 同一 stdout 上还有 V2 JSON 事件:ready(schemaVersion 2)、webcam-formataudio-formatencoder-audio-formatrecording-paused / recording-resumedcursor-capture(来自 wgc_session.cpp,报告请求的光标捕获是否生效)以及最终带 screenPathrecording-stopped

3.4 V2 JSON 请求格式

README 给出的当前 V2 JSON 形态如下(这是契约的最小骨架):

{
  "schemaVersion": 2,
  "recordingId": 123,
  "sourceType": "display",
  "sourceId": "screen:0:0",
  "displayId": 1,
  "windowHandle": null,
  "outputPath": "C:\\path\\recording-123.mp4",
  "videoWidth": 1920,
  "videoHeight": 1080,
  "fps": 60,
  "captureSystemAudio": false,
  "captureMic": false,
  "microphoneDeviceId": "default",
  "microphoneDeviceName": "Microphone (NVIDIA Broadcast)",
  "microphoneGain": 1.4,
  "webcamEnabled": true,
  "webcamDeviceId": "default",
  "webcamDeviceName": "Camera (NVIDIA Broadcast)",
  "webcamWidth": 1280,
  "webcamHeight": 720,
  "webcamFps": 30,
  "outputs": {
    "screenPath": "C:\\path\\recording-123.mp4"
  }
}

结合 Electron 主进程实际组装的请求(electron/ipc/handlers.ts),真实流量中还额外携带一组字段,helper 端在 main.cppCaptureConfig 中对应解析:

字段 作用
displayX/displayY/displayW/displayH + hasDisplayBounds 显示器物理 bounds,用于 WGC 源定位与光标坐标换算
captureCursor 是否让 WGC 直接烧录系统光标(cursor.mode === "system" 时为 true)
cursorCaptureMode editable-overlay(默认,光标由 Electron 采样、后期叠加)或 system
outputs.webcamPath 摄像头 PiP 输出路径,固定为 recording-<id>-webcam.mp4
webcamDirectShowClsid Electron 预先解析好的 DirectShow 滤镜 CLSID,供虚拟摄像头兜底
source / video / audio / webcam / cursor 子对象 完整的结构化冗余描述,便于 helper 诊断日志与未来扩展

输出文件由 Electron 统一规划:recording-<recordingId>.mp4(屏幕主视频)、recording-<recordingId>-webcam.mp4(摄像头)、recording-<recordingId>.session.json(会话清单),全部落在用户数据目录的 RECORDINGS_DIR 下,并以 cwd 传给子进程。

3.5 设备解析策略:为什么同时传 deviceId 和 deviceName

这是 Windows helper 契约中最具工程细节的一点(README 原文展开):浏览器 deviceId 并不总能映射到 Media Foundation 符号链接或 WASAPI endpoint ID,因此渲染进程同时传递浏览器 ID 和用户可见设备名。

  • 麦克风:helper 先尝试请求的 WASAPI endpoint ID,再按 microphoneDeviceName 解析活动的采集 endpoint,最后回落到默认 endpoint;
  • 摄像头:Electron 侧先为选中标签解析匹配的 DirectShow 滤镜 CLSID——源码中 queryDirectShowVideoInputRegistryelectron/ipc/handlers.ts)通过 reg.exe 枚举 HKCR\CLSID\{860BB310-...}\Instance(VideoInputDevice 聚合点)拿到 FriendlyName/CLSID 对,再用 scoreNativeDeviceName 做归一化名称打分(精确匹配 1000 分、包含关系 900/800 分、关键词按词 100/50 分)选出最佳滤镜;helper 端则"Media Foundation 优先,请求的摄像头不在 MF 中时回退到该精确 DirectShow 滤镜",从而覆盖不通过 Media Foundation 暴露的虚拟摄像头;
  • 当前实现中,摄像头帧被合成为右下角 PiP 叠加进主 MP4。

四、冒烟测试与手动设备验证

Windows helper 提供一组脚本化的冒烟测试(package.json,脚本为 scripts/test-windows-wgc-helper.mjs):

npm run test:wgc-helper:win        # 基础屏幕录制冒烟
npm run test:wgc-window:win       # --window:窗口源
npm run test:wgc-audio:win        # --system-audio:系统音频环回
npm run test:wgc-mic:win          # --microphone:麦克风采集
npm run test:wgc-mixed-audio:win  # --system-audio --microphone:混合音频
npm run test:wgc-webcam:win       # --webcam:摄像头采集

(仓库中另有 test:wgc-full:win 组合 --webcam --system-audio --microphone 做全链路验证。)

要手动验证某台原生摄像头,通过环境变量指定设备名再运行测试,然后清理环境变量:

$env:OPENSCREEN_WGC_TEST_WEBCAM_DEVICE_NAME = "NVIDIA Broadcast"
npm run test:wgc-webcam:win
Remove-Item Env:OPENSCREEN_WGC_TEST_WEBCAM_DEVICE_NAME

验证某台原生麦克风同理:

$env:OPENSCREEN_WGC_TEST_MICROPHONE_DEVICE_NAME = "Microphone (NVIDIA Broadcast)"
npm run test:wgc-mic:win
Remove-Item Env:OPENSCREEN_WGC_TEST_MICROPHONE_DEVICE_NAME

这些变量让测试脚本跳过"选默认设备",直接锁定指定设备走 MF / WASAPI / DirectShow 的完整解析链,是排查"我的虚拟摄像头为何没被选中"这类问题的首选手段。

五、能力探测汇总与适用前提

前端通过两个 IPC 通道做能力门控,行为差异值得注意:

通道 不可用原因 实现位置
is-native-windows-capture-available unsupported-os(Windows 10 build 低于 19041,即 WGC 不支持的旧版本,见 handlers.ts);missing-helper(四路候选均无可用二进制) electron/ipc/handlers.ts
is-native-mac-capture-available unsupported-platform(非 darwin);missing-helper(未找到 Swift helper) electron/ipc/handlers.ts

适用前提与限制:

  • Windows 侧要求 Windows 10 build 19041 及以上,且 helper 二进制存在于上表任一位置(未打包开发时通常需要先执行 npm run build:native:win);
  • macOS 侧要求 macOS 13 及以上(Swift 包声明 .macOS(.v13),helper 对更低版本抛出 unsupportedMacOS),且需先获得屏幕录制权限(helper 内 CGPreflightScreenCaptureAccess 预检);麦克风采集另需麦克风权限;
  • 两个平台的"可编辑光标"都依赖 Electron 侧的光标采样与后期叠加,只有 system 光标模式才把光标交给 WGC/SCK 直接烧录;
  • macOS 摄像头仍是 Electron sidecar,Windows 摄像头则由 helper 直接采集并合成 PiP——这是两端当前最大的能力不对称,后续演进可参考 Windows 原生录屏路线图macOS 原生录屏路线图

六、契约设计的可借鉴之处

从这套实现可以提炼出跨平台 native helper 的几个通用模式:

  1. 单 JSON 请求参数:全部配置一次性传入,避免 IPC 结构随平台漂移;schemaVersion 字段为不兼容演进留出空间(macOS helper 目前用 schemaVersion 1,Windows helper 用 2);
  2. stdout 换行 JSON 事件 + 文本锚点并存:JSON 事件承载结构化信息(格式协商、输出路径),Recording started 等文本锚点承担"启动完成"的握手,迁移期双协议共存降低了断档风险;
  3. stdin 行命令stop 一行即完成收尾,pause/resume 同样走 stdin,控制面与数据面天然分离;
  4. env 变量 → 本地 build → 打包 bin 的三级解析:开发期指向本地编译产物,诊断期指向任意路径,生产期走 bin/<platform-arch>,三态互不干扰;
  5. 设备名与设备 ID 双传 + 名称打分解析:把"浏览器世界"与"OS 设备世界"的 ID 不一致问题显式化,用归一化打分(而非精确匹配)吸收厂商命名差异。

对需要自研 Electron 录屏工具的开发者而言,这套"Electron 编排 + 平台 helper 采集编码 + 进程契约"的结构,以及 electron/native/README.md 中逐条列出的解析优先级、构建命令与测试矩阵,是可以直接参照落地的工程范本。

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