首页
/ OpenScreen Windows 原生光标捕获诊断流水线:从 GetCursorInfo 采样器到 WGC Helper 与真实编辑器预览校验

OpenScreen Windows 原生光标捕获诊断流水线:从 GetCursorInfo 采样器到 WGC Helper 与真实编辑器预览校验

2026-09-05 15:04:37作者:幸俭卉

本文基于 OpenScreen 仓库的 Windows native cursor test pipeline 文档展开,讲解两条专为 Windows 原生光标捕获设计的本地诊断流水线:一条是不启动应用、直接用 GetCursorInfo 采样真实系统指针并产出规范化 CursorRecordingData 与预览视频的独立诊断脚本;另一条是启动真实 Electron 应用、注入 fixture 视频与光标 sidecar 数据、从 OpenScreen 编辑器 UI 逐帧截屏并编码为 WebM 的预览校验脚本。读完本篇,你将掌握这两条流水线的完整命令、环境变量、产物结构与内置断言逻辑,并理解它们背后的 Windows 原生捕获后端(WGC helper)契约、可用性与 Click Bounce 已知缺口,从而在不走完整“录制→编辑→导出”人工循环的前提下快速迭代光标渲染代码。

1. 为什么需要这条诊断流水线

OpenScreen 在 Windows 上把光标从“烧录进视频”改为“独立采样 + 后期重建”:录制时系统指针被排除在画面之外,光标位置、形状、hotspot 与点击事件以旁路数据(sidecar)形式记录,再由编辑器预览和导出管线重新合成。这种架构让 SizeSmoothingMotion BlurClick Bounce 等光标效果可以在不重新录制的前提下反复调参,但也引入了新的验证难题——任何采样、坐标归一化或 hotspot 计算上的偏差,都会表现为预览/导出中光标漂移或形状错位。

因此 docs/testing/windows-native-cursor.md 定义了两条刻意保持“本地化、快速迭代”性质的开发者工具:它们只产出短视频与 JSON 报告,让光标改动可以在几分钟内被肉眼与数据双重检查,而不必每次都走一遍完整的人工 record/edit/export 循环。需要强调的是,文档明确指出这两条诊断不能完全替代端到端的人工录制校验——在合入光标相关改动之前,仍应在打包后的应用里做一次真实录制与导出。

2. 诊断一:原生光标采样器(test:cursor-native:win

2.1 命令与行为概览

npm run test:cursor-native:win

该脚本对应 scripts/test-windows-native-cursor.mjs,在 package.json 中注册为 test:cursor-native:win。它不启动 OpenScreen 应用,而是完全独立地运行一次受控的光标捕获实验。脚本启动后会:

  • 启动一个基于 Windows GetCursorInfo 的采样器(PowerShell 内注入 C# P/Invoke 类型);
  • SetCursorPos 驱动真实系统指针沿一条正弦波形路径移动(步长 120ms,在路径中点还会注入一次真实的左键按下/释放,用于验证点击事件捕获);
  • 捕获原生光标句柄(handle)、hotspot、光标位图资产,并把标准 IDC_* 光标类型与句柄做映射识别;
  • 将样本写成规范化的 CursorRecordingData(sidecar 兼容格式);
  • 生成一个抽象预览视频(路径 / 资产 / hotspot 叠加渲染);
  • 生成一个真实屏幕预览视频:以当前桌面截图为背景,把重建的光标叠加上去。

输出目录会打印在命令结果中,形如 C:\Users\<user>\AppData\Local\Temp\openscreen-cursor-native-...(代码中实际是 os.tmpdir() 下以 openscreen-cursor-native-${Date.now()} 命名的目录)。脚本在启动时强制检查 process.platform === "win32",非 Windows 平台直接报错退出。

2.2 采样器的 Windows API 实现细节

scripts/test-windows-native-cursor.mjs 的源码看,采样脚本通过 Add-Type 动态编译一个 OpenScreenCursorDiagnosticInterop 类,核心 P/Invoke 包括:

  • GetCursorInfo(ref CURSORINFO):每轮采样读取光标可见性标志(flags & 1)、hCursor 句柄和 ptScreenPos 屏幕坐标;
  • GetIconInfo / CopyIcon / DestroyIcon / DeleteObject:把光标句柄转成 Icon,再绘制进 32bpp ARGB 位图并导出 PNG(base64 data URL),同时读取 xHotspot/yHotspot;采样器对每个新句柄只抓一次资产($lastCursorId 去重),并在 finally 中释放 GDI 对象;
  • GetAsyncKeyState(0x01):轮询左键状态(高 16 位为“当前是否按下”,低 1 位为“两个采样之间是否发生过切换”);
  • SetWindowsHookEx(WH_MOUSE_LL, ...):低级鼠标钩子统计 WM_LBUTTONDOWN/WM_LBUTTONUP,用于捕获采样间隔之间的快速点击。

标准光标识别使用一张 IDC_* 常量表:arrow = 32512text = 32513wait = 32514crosshair = 32515up-arrow = 32516resize-nwse = 32642resize-nesw = 32643resize-ew = 32644resize-ns = 32645move = 32646not-allowed = 32648pointer = 32649app-starting = 32650help = 32651,通过 LoadCursor(NULL, IDC_*) 加载后与采样到的句柄比对。对于无法映射到标准类型的光标,还有一段启发式分类(尺寸 24–64px、hotspot 位置区间、不透明像素占比等)尝试区分 closed-hand / open-hand

真实指针驱动脚本 buildMousePathScript 会把主屏 bounds 内缩 80px,在垂直方向叠加 Sin(t * 2π) 波形(幅度 min(180, height/4)),在路径中点调用 mouse_event(MOUSEEVENTF_LEFTDOWN) + 12ms + MOUSEEVENTF_LEFTUP 制造一次合成点击;桌面截屏脚本则按 CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS 间隔把主屏 CopyFromScreen 到 960 宽的 PNG 帧序列,并输出带时间戳的 frames.json

2.3 产物结构与内置断言

输出目录中的关键文件:

文件 内容
report.json 采样数、资产数、唯一光标句柄数、唯一位置数、左键按下/点击样本数、错误数、首尾样本、生成产物路径
cursor-recording-data.json sidecar 兼容的光标数据(下文详述)
preview.webm 抽象路径/资产/hotspot 预览(1x/2x/3x 位图重建 + 矢量替换光标)
real-capture-preview.webm 真实桌面截图背景 + 重建光标叠加
assets/*.png 从 Windows 捕获的原始光标位图
events.jsonreport.htmlreal-capture-report.html 原始事件流与可视化诊断页(Playwright 无头 Chromium 打开后 captureStream 录制为 WebM)

cursor-recording-data.json 的结构由 toRecordingData 生成:version: 2provider"native"(无资产时为 "none");每个样本包含相对首个样本的 timeMs、按主屏 bounds 归一化的 cx/cyassetId(即光标句柄)、visiblecursorType,以及由左键状态转移推导的 interactionTypeclick / mouseup / move);每个资产包含 idplatform: "win32"、base64 PNG、宽高、hotspotX/YscaleFactor: 1

脚本最后会执行 assertReport,即一组硬性验收条件,任何一条不满足都会以非零退出:

  • sampleCount >= floor(DURATION_MS / SAMPLE_INTERVAL_MS / 3)(采样连续性底线);
  • 至少存在可见光标样本、至少捕获到 1 个光标资产 PNG;
  • uniquePositionCount >= 4(指针确实发生了可观察的移动);
  • errorCount === 0
  • 左键点击交互必须被观察到(leftButtonPressedSampleCount > 0clickSampleCount > 0)。

2.4 环境变量覆盖

$env:CURSOR_TEST_DURATION_MS = "3000"
$env:CURSOR_TEST_SAMPLE_INTERVAL_MS = "16"
$env:CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS = "80"
$env:CURSOR_TEST_OUTPUT_DIR = "C:\temp\openscreen-cursor-test"
npm run test:cursor-native:win

结合源码,完整的取值与默认值如下(非法或非正值会被警告并回退到默认值):

环境变量 默认值 含义
CURSOR_TEST_DURATION_MS 1800 指针驱动与桌面截屏的总时长(ms)
CURSOR_TEST_SAMPLE_INTERVAL_MS 25 GetCursorInfo 采样间隔(ms)
CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS 100 桌面截图帧间隔(ms)
CURSOR_TEST_READY_TIMEOUT_MS 5000 等待采样器 ready 事件的超时(ms)
CURSOR_TEST_OUTPUT_DIR 系统临时目录下 openscreen-cursor-native-<时间戳> 产物输出目录

3. 诊断二:OpenScreen 真实编辑器预览捕获(capture:openscreen-preview

npm run capture:openscreen-preview

该脚本对应 scripts/capture-openscreen-preview.mjs,与上一条诊断形成互补:它启动真实的 Electron 应用,注入 fixture 视频与光标 sidecar 数据,打开编辑器,从真实的 OpenScreen 预览 UI 逐帧截图,再编码成 WebM。它回答的问题是:诊断流水线里重建出来的光标行为,在真实编辑器里是否渲染得一模一样。

从源码看,其完整流程是:

  1. Sidecar 发现:默认扫描系统临时目录中所有 openscreen-cursor-native-* 子目录里的 cursor-recording-data.json,取 mtime 最新者——即最近一次 npm run test:cursor-native:win 的产物;找不到时直接报错提示先运行采样诊断。可用 CURSOR_RECORDING_DATA_PATH 强制指定具体文件(不存在会抛错):
$env:CURSOR_RECORDING_DATA_PATH = "C:\path\to\cursor-recording-data.json"
npm run capture:openscreen-preview
  1. 构建检查:要求 dist-electron/main.jsdist/index.html 存在(即先跑过 npm run build-vite);设置 OPENSCREEN_PREVIEW_SKIP_BUILD=true 会跳过构建并只做存在性检查,否则脚本会自动通过 cmd.exe 触发 npm run build-vite
  2. 注入 fixture:把 tests/fixtures/sample.webm 复制为 openscreen-preview-fixture.webm,并把 sidecar 复制为同名的 .cursor.json,再通过 window.electronAPI.setCurrentVideoPath + switchToEditor 让应用直接进入编辑器。
  3. 逐帧截图:等待编辑器窗口(URL 含 windowType=editor)中的 videocanvas 选择器就绪,设置 1280×800 视口,静音并把 currentTime 依次置为 index / FPS,每帧等待 40ms 后对页面截图保存为 frames/frame-XXXX.png
  4. 编码 WebM:启动无头 Chromium,把全部 PNG 帧按设定 FPS 绘到 canvas 上,用 captureStream + MediaRecorder(VP9 WebM)编码出 openscreen-preview.webm
$env:OPENSCREEN_PREVIEW_SKIP_BUILD = "true"
$env:OPENSCREEN_PREVIEW_FRAME_COUNT = "120"
$env:OPENSCREEN_PREVIEW_FPS = "30"
$env:OPENSCREEN_PREVIEW_OUTPUT_DIR = "C:\temp\openscreen-preview"
npm run capture:openscreen-preview
环境变量 默认值 含义
OPENSCREEN_PREVIEW_SKIP_BUILD 未设置(即自动执行 build-vite 置为 "true" 时跳过构建,仅校验产物存在
OPENSCREEN_PREVIEW_FRAME_COUNT 90 从编辑器捕获的帧数
OPENSCREEN_PREVIEW_FPS 30 逐帧定位间隔(index / FPS 秒)与输出视频帧率
OPENSCREEN_PREVIEW_OUTPUT_DIR 系统临时目录下 openscreen-real-preview-<时间戳> 输出目录
CURSOR_RECORDING_DATA_PATH 最新一次采样诊断产物 强制指定 sidecar 文件

输出目录中的文件:openscreen-preview.webm(真实 OpenScreen 编辑器预览视频)、frames/*.png(逐帧截图)、report.json(含 outputDirsourceCursorRecordingDataPathfixtureVideoPathoutputVideoPathframeCountfps)。

4. 两条诊断共同验证的能力

按照原文档,两条脚本组合起来可以快速检查:

  • Windows 光标样本是否可见且连续visibleSampleCountsampleCount 底线断言);
  • 原生 hotspot 在缩放到 3x 时是否仍然锚定正确(预览页同时绘制 1x/2x/3x 位图重建与红色十字 hotspot 标记);
  • 标准 Windows 光标是否通过 IDC_* 被正确识别(采样器句柄比对表);
  • 高质量 SVG 光标替换是否跟随原生 hotspot(诊断页中的 “pretty 3x” 矢量箭头即以同一 hotspot 为锚点绘制);
  • 真实 OpenScreen 预览是否与诊断流水线渲染出一致的光标行为(capture:openscreen-previewpreview.webm 的对比依据)。

再次强调文档给出的边界:这两条诊断不是完整端到端人工录制校验的替代品。光标改动在发布前,还需要在打包应用里完成一次真实 capture session 与导出。

5. 已知缺口:Click Bounce 尚未可见生效

原文档明确将 Windows 原生光标的 Click Bounce 标记为 backlog:SizeSmoothingMotion Blur 可以通过预览/导出验证,但 Click Bounce 在打包应用的人工测试中没有表现出可见效果。当前诊断只能观察到合成的点击元数据(leftButtonPressed/clickSampleCount),这不足以验证真实 OpenScreen record → preview → export 路径上的弹跳动画。

该问题的完整跟踪项位于 docs/engineering/windows-native-recorder-roadmap.md 的 “Native Cursor Click Bounce Is Not Visibly Applied” 小节(Backlog 部分),其中记录了已尝试的手段(向样本加入 interactionType: "click" | "mouseup" | "move"GetAsyncKeyState 轮询与低比特位快速点击捕获、WH_MOUSE_LL 鼠标钩子实验、把采样器改为临时 .ps1 文件以规避 Windows 命令行长度限制)以及当前诊断结论:采样元数据可以被记录,但尚未转化为真实打包应用中可见的 bounce 效果;后续排查方向包括检查真实 .cursor.json sidecar 中点击的 timeMs 是否准确、对比原生 DOM 光标路径与旧版 PixiCursorOverlay 的点击视觉状态,以及在必要时把点击事件下沉到小型原生光标 helper。

6. 背景:Windows 原生捕获后端(WGC Helper)

理解上述诊断的价值,需要知道它们所处的捕获架构。应用现已把 Windows 录制从 Electron getDisplayMedia 切换到外部 WGC helper 进程,目的正是消除此前导致重建光标在预览/导出路径中漂移的“坐标与时钟分裂”。

6.1 可用性规则

  • Windows 10 build 19041 或更新版本;
  • 存在可用的 helper 可执行文件。

前者在 electron/ipc/handlers.ts 中由 isWindowsGraphicsCaptureOsSupported 实现:解析 process.getSystemVersion() 的第三段作为 build 号,判断 build >= 19041,不满足时 IPC 侧会返回 “Windows Graphics Capture requires Windows 10 build 19041 or newer.” 错误。

helper 的可执行文件按以下顺序解析(见 electron/native/README.md):

  1. OPENSCREEN_WGC_CAPTURE_EXE(本地开发与诊断);
  2. electron/native/wgc-capture/build/wgc-capture.exe(本地 Ninja 构建产物);
  3. electron/native/wgc-capture/build/Release/wgc-capture.exe(多配置构建产物);
  4. electron/native/bin/win32-x64/wgc-capture.exeelectron/native/bin/win32-arm64/wgc-capture.exe(打包预构建)。

helper 目前实现:显示器/窗口视频捕获、系统音频环回、默认麦克风捕获、Media Foundation 网络摄像头捕获,以及针对 NVIDIA Broadcast 等特定虚拟摄像头的 DirectShow 回退。摄像头画面以右下角画中画叠加进主 MP4,黑色预热帧会在首个可见帧出现前被忽略。光标采样的原生实现位于 electron/native/wgc-capture/src/cursor-sampler.cpp

6.2 构建与 smoke 测试

本地构建 OpenScreen 的 helper:

npm run build:native:win

构建会把 CMake 产物写到 electron/native/wgc-capture/build/wgc-capture.exe,并复制到 electron/native/bin/win32-x64/wgc-capture.exe。直接对 helper 做 smoke 测试:

npm run test:wgc-helper:win
npm run test:wgc-helper:win -- --capture-cursor
npm run test:wgc-window:win
npm run test:wgc-audio:win
npm run test:wgc-mic:win
npm run test:wgc-mixed-audio:win
npm run test:wgc-webcam:win

这些脚本名在 package.json 中统一映射到 scripts/test-windows-wgc-helper.mjs 及其不同的 CLI 参数(--window--system-audio--microphone--webcam 等)。如需对某个特定摄像头/麦克风做手动验证,还可分别设置 OPENSCREEN_WGC_TEST_WEBCAM_DEVICE_NAME / OPENSCREEN_WGC_TEST_MICROPHONE_DEVICE_NAME(见 electron/native/README.md)。

6.3 指向自定义 helper 与进程契约

若要在本地诊断中使用另一个兼容 helper,把环境变量指向该可执行文件再启动开发态应用即可:

$env:OPENSCREEN_WGC_CAPTURE_EXE = "C:\path\to\wgc-capture.exe"
npm run build-vite
npm run dev

helper 的进程契约:应用以一个 JSON 配置参数启动该进程;helper 输出 JSON 生命周期事件,并打印遗留的 Recording started 标记;应用通过 stdin 发送 stopstop\n)来结束录制;helper 最终打印 Recording stopped. Output path: <path>。V2 JSON 配置形态(含 schemaVersionsourceType/sourceIdoutputPathfpscaptureSystemAudio/captureMic、麦克风/摄像头设备与增益参数、outputs.screenPath 等字段)的完整示例与字段说明,见 electron/native/README.md

7. 实操建议:推荐的验证顺序

结合两条诊断与 helper smoke 测试,一个典型的光标改动验证流程是:

  1. 修改光标采样/渲染相关代码后,先跑 npm run test:cursor-native:win,让 assertReport 的连续性、可见性、资产、移动、点击五类断言自动把关,并打开 real-capture-preview.webm 目测 1x/2x/3x 与矢量替换光标的 hotspot 锚定;
  2. 用同一份最新的 cursor-recording-data.jsonnpm run capture:openscreen-preview(可配 OPENSCREEN_PREVIEW_FRAME_COUNT=120),对比 openscreen-preview.webm 与诊断预览是否行为一致;
  3. 涉及捕获链路时,依次执行 test:wgc-helper:win 系列命令确认 helper 各通道(含 --capture-cursor)正常;
  4. 合入前,在打包应用中完成一次真实录制会话与导出的人工端到端校验;Click Bounce 在未关闭 roadmap 中的 backlog 项之前,不应被视为已交付能力。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
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.83 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
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384