OpenScreen 原生录屏 Helper:Windows WGC 与 macOS ScreenCaptureKit 的进程契约实现详解
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):
- Electron 负责解析选中的录屏源(显示器或窗口)、输出路径,以及用户选择的麦克风/摄像头设备;
- 启动 helper 子进程,传入一个结构化的 JSON 请求;
- Helper 完整拥有采集、计时(含暂停/恢复的时间对齐)、编码和混流(muxing);
- Electron 持久化最终的媒体文件/会话清单(session manifest),并显式上报 helper 的错误。
这个边界在源码中可以直接对应。以 macOS 为例,主进程在 electron/ipc/handlers.ts 的 start-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):
- 环境变量
OPENSCREEN_SCK_CAPTURE_EXE,用于本地开发与诊断; electron/native/screencapturekit/build/openscreen-screencapturekit-helper,本地构建的 Swift 输出;electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper或electron/native/bin/darwin-x64/openscreen-screencapturekit-helper,打包后的预构建 helper。
源码中这一优先级在 electron/ipc/handlers.ts 的 getNativeMacCaptureHelperCandidates / 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 的实现吻合:makeStreamConfiguration 中 configuration.showsCursor = !request.video.hideSystemCursor(即可编辑光标模式下由 Electron 采样、后期叠加,而不是烧进画面)、minimumFrameInterval 由请求的 fps 换算、queueDepth = 6、像素格式 kCVPixelFormatType_32BGRA、音频 sampleRate = 48000、channelCount = 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 后 print 并 fflush(stdout),保证 Electron 侧能逐行实时解析。事件流包括:
ready(schemaVersion: 1,流配置完成、即将startCapture);recording-started(首个完整视频帧写入 writer 后,附带实际输出的 width/height);recording-paused/recording-resumed(附timestampMs,暂停期间丢弃所有采样缓冲,恢复后对 presentation time 做偏移对齐,见retimedSampleBuffer);warning(如microphone-unavailable、停止采集失败);error(附code与message,例如权限被拒、找不到显示器/窗口)。
Electron 侧还会在请求中强制 webcam.enabled = false 并附上 manifestPath(recording-<id>.session.json),与 README 中"Electron 持久化媒体/会话清单"的步骤对应。
三、Windows 侧:WGC Helper
3.1 二进制解析优先级
Windows 原生录屏从以下位置解析(electron/native/README.md):
- 环境变量
OPENSCREEN_WGC_CAPTURE_EXE,用于本地开发与诊断; electron/native/wgc-capture/build/wgc-capture.exe,本地 Ninja 构建的 helper;electron/native/wgc-capture/build/Release/wgc-capture.exe,本地多配置构建的 helper;electron/native/bin/win32-x64/wgc-capture.exe或electron/native/bin/win32-arm64/wgc-capture.exe,打包后的预构建 helper。
实现位于 electron/ipc/handlers.ts:getNativeWindowsCaptureHelperCandidates 按 env → build/Release → build → bin/ 生成候选列表(打包态额外检查 resources 下的同构路径),findNativeWindowsCaptureHelperPath 依次做可执行性检查。注意候选顺序与 README 略有实现差异:源码中 build/Release 在 build 之前被探测,两者都是"本地构建"路径,不影响可用性。
3.2 构建命令
npm run build:native:win
对应 package.json 的 node 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,源码模块划分清晰:
- wgc_session.cpp:Windows.Graphics.Capture 会话与帧源;
- wasapi_loopback_capture.cpp:系统音频环回采集;
- mf_encoder.cpp:Media Foundation 编码与 MP4 封装;
- webcam_capture.cpp / dshow_webcam_capture.cpp:摄像头采集(MF 优先,DirectShow 兜底);
- monitor_utils.cpp:显示器枚举与 bounds 解析。
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 中按行读取,
stop、q、quit均触发停止; - 启动成功后打印
Recording started文本(main.cpp),Electron 侧的waitForNativeWindowsCaptureStart(electron/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-format、audio-format、encoder-audio-format、recording-paused/recording-resumed、cursor-capture(来自 wgc_session.cpp,报告请求的光标捕获是否生效)以及最终带screenPath的recording-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.cpp 的 CaptureConfig 中对应解析:
| 字段 | 作用 |
|---|---|
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——源码中
queryDirectShowVideoInputRegistry(electron/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 的几个通用模式:
- 单 JSON 请求参数:全部配置一次性传入,避免 IPC 结构随平台漂移;
schemaVersion字段为不兼容演进留出空间(macOS helper 目前用 schemaVersion 1,Windows helper 用 2); - stdout 换行 JSON 事件 + 文本锚点并存:JSON 事件承载结构化信息(格式协商、输出路径),
Recording started等文本锚点承担"启动完成"的握手,迁移期双协议共存降低了断档风险; - stdin 行命令:
stop一行即完成收尾,pause/resume 同样走 stdin,控制面与数据面天然分离; - env 变量 → 本地 build → 打包 bin 的三级解析:开发期指向本地编译产物,诊断期指向任意路径,生产期走
bin/<platform-arch>,三态互不干扰; - 设备名与设备 ID 双传 + 名称打分解析:把"浏览器世界"与"OS 设备世界"的 ID 不一致问题显式化,用归一化打分(而非精确匹配)吸收厂商命名差异。
对需要自研 Electron 录屏工具的开发者而言,这套"Electron 编排 + 平台 helper 采集编码 + 进程契约"的结构,以及 electron/native/README.md 中逐条列出的解析优先级、构建命令与测试矩阵,是可以直接参照落地的工程范本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00