From f5ccd5b7e040dafb1b99ea3d965c312d18862264 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Thu, 19 Mar 2026 00:05:58 +1100
Subject: [PATCH] Fix Windows DXGI capture helper flow
---
CLAUDE.md | 2 +-
README.md | 4 +-
README.zh-CN.md | 4 +-
electron/ipc/handlers.ts | 2 +-
.../CMakeLists.txt | 8 +-
.../src/dxgi_session.cpp | 0
.../src/dxgi_session.h | 0
.../src/main.cpp | 0
.../src/mf_encoder.cpp | 0
.../src/mf_encoder.h | 0
.../src/monitor_utils.cpp | 0
.../src/monitor_utils.h | 0
.../src/wasapi_loopback.cpp | 0
.../src/wasapi_loopback.h | 0
package.json | 13 +--
...-capture.mjs => build-windows-capture.mjs} | 24 +++---
src/components/launch/LaunchWindow.tsx | 4 +
src/components/video-editor/VideoEditor.tsx | 80 -------------------
src/hooks/useScreenRecorder.ts | 29 +++----
19 files changed, 48 insertions(+), 122 deletions(-)
rename electron/native/{wgc-capture => windows-capture}/CMakeLists.txt (61%)
rename electron/native/{wgc-capture => windows-capture}/src/dxgi_session.cpp (100%)
rename electron/native/{wgc-capture => windows-capture}/src/dxgi_session.h (100%)
rename electron/native/{wgc-capture => windows-capture}/src/main.cpp (100%)
rename electron/native/{wgc-capture => windows-capture}/src/mf_encoder.cpp (100%)
rename electron/native/{wgc-capture => windows-capture}/src/mf_encoder.h (100%)
rename electron/native/{wgc-capture => windows-capture}/src/monitor_utils.cpp (100%)
rename electron/native/{wgc-capture => windows-capture}/src/monitor_utils.h (100%)
rename electron/native/{wgc-capture => windows-capture}/src/wasapi_loopback.cpp (100%)
rename electron/native/{wgc-capture => windows-capture}/src/wasapi_loopback.h (100%)
rename scripts/{build-wgc-capture.mjs => build-windows-capture.mjs} (63%)
diff --git a/CLAUDE.md b/CLAUDE.md
index cc0f9400..2080d1ea 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -42,7 +42,7 @@ Run a single test file: `npx vitest run src/lib/exporter/gifExporter.test.ts`
### Recording Pipeline (Platform-Specific)
- **macOS**: ScreenCaptureKit via compiled Swift helpers (`scripts/build-native-helpers.mjs`)
-- **Windows**: DXGI Desktop Duplication via C++ CMake build (`scripts/build-wgc-capture.mjs`), fallback to FFmpeg
+- **Windows**: DXGI Desktop Duplication via C++ CMake build (`scripts/build-windows-capture.mjs`), fallback to FFmpeg
- **Linux**: Chromium `getDisplayMedia`
- Core recording logic: `src/hooks/useScreenRecorder.ts` (60 FPS target, adaptive bitrate)
diff --git a/README.md b/README.md
index 2fb3e8bf..1ec18731 100644
--- a/README.md
+++ b/README.md
@@ -160,7 +160,7 @@ xattr -rd com.apple.quarantine /Applications/Recordly.app
| Platform | Minimum version | Notes |
|---|---|---|
| **macOS** | macOS 12.3 (Monterey) | Required for ScreenCaptureKit. Recording and cursor hiding will not work on older versions. |
-| **Windows** | Windows 10 20H1 (Build 19041, May 2020) | Required for Windows Graphics Capture (`IsCursorCaptureEnabled`). Older builds fall back to Electron browser capture — cursor will be visible in recordings. |
+| **Windows** | Windows 10 20H1 (Build 19041, May 2020) | Required for the native DXGI Desktop Duplication helper. Older builds fall back to Electron browser capture — cursor will be visible in recordings. |
| **Linux** | Any modern distro | Recording works via Electron capture. Cursor is always visible in recordings. System audio requires PipeWire (Ubuntu 22.04+, Fedora 34+). |
> [!IMPORTANT]
@@ -217,7 +217,7 @@ Adjust:
**macOS**: Cursor is excluded from the recording at the ScreenCaptureKit level — always clean.
-**Windows**: Cursor is excluded via Windows Graphics Capture (`IsCursorCaptureEnabled(false)`) — requires **Windows 10 Build 19041+**. On older builds the app falls back to Electron’s browser capture and the real cursor will be visible in the recording.
+**Windows**: Cursor exclusion depends on the native DXGI Desktop Duplication helper plus OS cursor hide/show, and requires **Windows 10 Build 19041+**. On older builds the app falls back to Electron’s browser capture and the real cursor will be visible in the recording.
**Linux**: Electron’s desktop capture API does not support cursor hiding. The real OS cursor will always be visible in recordings. If you also enable the animated cursor overlay in the editor, you may see **two cursors** in the output.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index c956c27e..6c4a8890 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -156,7 +156,7 @@ xattr -rd com.apple.quarantine /Applications/Recordly.app
| 平台 | 最低版本 | 说明 |
|---|---|---|
| **macOS** | macOS 12.3 (Monterey) | ScreenCaptureKit 的最低要求。更旧版本无法正常进行录制和隐藏光标。 |
-| **Windows** | Windows 10 20H1(Build 19041,2020 年 5 月) | Windows Graphics Capture (`IsCursorCaptureEnabled`) 的最低要求。更旧版本会回退到 Electron 浏览器捕获,录制中会显示真实光标。 |
+| **Windows** | Windows 10 20H1(Build 19041,2020 年 5 月) | 原生 DXGI Desktop Duplication 录制助手的最低要求。更旧版本会回退到 Electron 浏览器捕获,录制中会显示真实光标。 |
| **Linux** | 任意现代发行版 | 通过 Electron 捕获实现录制。录制中光标始终可见。系统音频需要 PipeWire(Ubuntu 22.04+、Fedora 34+)。 |
> [!IMPORTANT]
@@ -213,7 +213,7 @@ xattr -rd com.apple.quarantine /Applications/Recordly.app
**macOS**:在 ScreenCaptureKit 层级就会将光标排除,因此始终干净。
-**Windows**:通过 Windows Graphics Capture 的 `IsCursorCaptureEnabled(false)` 排除光标,需要 **Windows 10 Build 19041+**。在更旧版本中,应用会回退到 Electron 浏览器捕获,真实光标会出现在录制里。
+**Windows**:光标排除依赖原生 DXGI Desktop Duplication 录制助手以及系统光标隐藏/恢复逻辑,需要 **Windows 10 Build 19041+**。在更旧版本中,应用会回退到 Electron 浏览器捕获,真实光标会出现在录制里。
**Linux**:Electron 的桌面捕获 API 不支持隐藏光标。真实系统光标将始终出现在录制中。如果你同时在编辑器中启用动画光标叠加,导出结果中可能会出现**两个光标**。
diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts
index 61f1c191..9a8060b9 100644
--- a/electron/ipc/handlers.ts
+++ b/electron/ipc/handlers.ts
@@ -848,7 +848,7 @@ async function buildFfmpegCaptureArgs(source: SelectedSource, outputPath: string
}
function getWindowsCaptureExePath() {
- return resolveUnpackedAppPath('electron', 'native', 'wgc-capture', 'build', 'Release', 'wgc-capture.exe')
+ return resolveUnpackedAppPath('electron', 'native', 'windows-capture', 'build', 'Release', 'windows-capture.exe')
}
function getCursorMonitorExePath() {
diff --git a/electron/native/wgc-capture/CMakeLists.txt b/electron/native/windows-capture/CMakeLists.txt
similarity index 61%
rename from electron/native/wgc-capture/CMakeLists.txt
rename to electron/native/windows-capture/CMakeLists.txt
index 59e156e2..773452c4 100644
--- a/electron/native/wgc-capture/CMakeLists.txt
+++ b/electron/native/windows-capture/CMakeLists.txt
@@ -1,10 +1,10 @@
cmake_minimum_required(VERSION 3.20)
-project(wgc-capture LANGUAGES CXX)
+project(windows-capture LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
-add_executable(wgc-capture
+add_executable(windows-capture
src/main.cpp
src/dxgi_session.cpp
src/mf_encoder.cpp
@@ -12,9 +12,9 @@ add_executable(wgc-capture
src/wasapi_loopback.cpp
)
-target_compile_options(wgc-capture PRIVATE /EHsc /W3 /utf-8)
+target_compile_options(windows-capture PRIVATE /EHsc /W3 /utf-8)
-target_link_libraries(wgc-capture PRIVATE
+target_link_libraries(windows-capture PRIVATE
d3d11
dxgi
dwmapi
diff --git a/electron/native/wgc-capture/src/dxgi_session.cpp b/electron/native/windows-capture/src/dxgi_session.cpp
similarity index 100%
rename from electron/native/wgc-capture/src/dxgi_session.cpp
rename to electron/native/windows-capture/src/dxgi_session.cpp
diff --git a/electron/native/wgc-capture/src/dxgi_session.h b/electron/native/windows-capture/src/dxgi_session.h
similarity index 100%
rename from electron/native/wgc-capture/src/dxgi_session.h
rename to electron/native/windows-capture/src/dxgi_session.h
diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/windows-capture/src/main.cpp
similarity index 100%
rename from electron/native/wgc-capture/src/main.cpp
rename to electron/native/windows-capture/src/main.cpp
diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/windows-capture/src/mf_encoder.cpp
similarity index 100%
rename from electron/native/wgc-capture/src/mf_encoder.cpp
rename to electron/native/windows-capture/src/mf_encoder.cpp
diff --git a/electron/native/wgc-capture/src/mf_encoder.h b/electron/native/windows-capture/src/mf_encoder.h
similarity index 100%
rename from electron/native/wgc-capture/src/mf_encoder.h
rename to electron/native/windows-capture/src/mf_encoder.h
diff --git a/electron/native/wgc-capture/src/monitor_utils.cpp b/electron/native/windows-capture/src/monitor_utils.cpp
similarity index 100%
rename from electron/native/wgc-capture/src/monitor_utils.cpp
rename to electron/native/windows-capture/src/monitor_utils.cpp
diff --git a/electron/native/wgc-capture/src/monitor_utils.h b/electron/native/windows-capture/src/monitor_utils.h
similarity index 100%
rename from electron/native/wgc-capture/src/monitor_utils.h
rename to electron/native/windows-capture/src/monitor_utils.h
diff --git a/electron/native/wgc-capture/src/wasapi_loopback.cpp b/electron/native/windows-capture/src/wasapi_loopback.cpp
similarity index 100%
rename from electron/native/wgc-capture/src/wasapi_loopback.cpp
rename to electron/native/windows-capture/src/wasapi_loopback.cpp
diff --git a/electron/native/wgc-capture/src/wasapi_loopback.h b/electron/native/windows-capture/src/wasapi_loopback.h
similarity index 100%
rename from electron/native/wgc-capture/src/wasapi_loopback.h
rename to electron/native/windows-capture/src/wasapi_loopback.h
diff --git a/package.json b/package.json
index d42e0416..a00e9fc5 100644
--- a/package.json
+++ b/package.json
@@ -16,19 +16,20 @@
"type": "module",
"scripts": {
"dev": "vite",
- "postinstall": "npm run rebuild:native",
- "build": "npm run build:native-helpers && tsc && vite build && electron-builder",
+ "postinstall": "npm run rebuild:native && npm run build:platform-native-helpers",
+ "build": "npm run build:platform-native-helpers && tsc && vite build && electron-builder",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"preview": "vite preview",
"rebuild:native": "electron-rebuild --force --only uiohook-napi",
"build:native-helpers": "node scripts/build-native-helpers.mjs",
- "build:wgc-capture": "node scripts/build-wgc-capture.mjs",
+ "build:platform-native-helpers": "npm run build:native-helpers && npm run build:windows-capture && npm run build:cursor-monitor",
+ "build:windows-capture": "node scripts/build-windows-capture.mjs",
"build:cursor-monitor": "node scripts/build-cursor-monitor.mjs",
- "build:mac": "npm run build:native-helpers && tsc && vite build && electron-builder --mac",
- "build:win": "npm run build:wgc-capture && npm run build:cursor-monitor && tsc && vite build && electron-builder --win",
- "build:linux": "tsc && vite build && electron-builder --linux",
+ "build:mac": "npm run build:platform-native-helpers && tsc && vite build && electron-builder --mac",
+ "build:win": "npm run build:platform-native-helpers && tsc && vite build && electron-builder --win",
+ "build:linux": "npm run build:platform-native-helpers && tsc && vite build && electron-builder --linux",
"i18n:check": "node scripts/i18n-check.mjs",
"test": "vitest --run",
"test:watch": "vitest"
diff --git a/scripts/build-wgc-capture.mjs b/scripts/build-windows-capture.mjs
similarity index 63%
rename from scripts/build-wgc-capture.mjs
rename to scripts/build-windows-capture.mjs
index 49a6a7a7..bfdfa4dd 100644
--- a/scripts/build-wgc-capture.mjs
+++ b/scripts/build-windows-capture.mjs
@@ -3,16 +3,16 @@ import { mkdirSync, existsSync } from 'node:fs';
import path from 'node:path';
const projectRoot = process.cwd();
-const sourceDir = path.join(projectRoot, 'electron', 'native', 'wgc-capture');
+const sourceDir = path.join(projectRoot, 'electron', 'native', 'windows-capture');
const buildDir = path.join(sourceDir, 'build');
if (process.platform !== 'win32') {
- console.log('[build-wgc-capture] Skipping native Windows capture build: host platform is not Windows.');
+ console.log('[build-windows-capture] Skipping native Windows capture build: host platform is not Windows.');
process.exit(0);
}
if (!existsSync(path.join(sourceDir, 'CMakeLists.txt'))) {
- console.error('[build-wgc-capture] CMakeLists.txt not found at', sourceDir);
+ console.error('[build-windows-capture] CMakeLists.txt not found at', sourceDir);
process.exit(1);
}
@@ -42,13 +42,13 @@ function findCmake() {
const cmake = findCmake();
if (!cmake) {
- console.error('[build-wgc-capture] CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.');
+ console.error('[build-windows-capture] CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.');
process.exit(1);
}
mkdirSync(buildDir, { recursive: true });
-console.log('[build-wgc-capture] Configuring CMake...');
+console.log('[build-windows-capture] Configuring CMake...');
try {
execSync(`${cmake} .. -G "Visual Studio 17 2022" -A x64`, {
cwd: buildDir,
@@ -56,7 +56,7 @@ try {
timeout: 120000,
});
} catch {
- console.log('[build-wgc-capture] VS 2022 generator not found, trying VS 2019...');
+ console.log('[build-windows-capture] VS 2022 generator not found, trying VS 2019...');
try {
execSync(`${cmake} .. -G "Visual Studio 16 2019" -A x64`, {
cwd: buildDir,
@@ -64,12 +64,12 @@ try {
timeout: 120000,
});
} catch (innerError) {
- console.error('[build-wgc-capture] CMake configure failed:', innerError.message);
+ console.error('[build-windows-capture] CMake configure failed:', innerError.message);
process.exit(1);
}
}
-console.log('[build-wgc-capture] Building native Windows capture helper...');
+console.log('[build-windows-capture] Building native Windows capture helper...');
try {
execSync(`${cmake} --build . --config Release`, {
cwd: buildDir,
@@ -77,14 +77,14 @@ try {
timeout: 300000,
});
} catch (error) {
- console.error('[build-wgc-capture] Build failed:', error.message);
+ console.error('[build-windows-capture] Build failed:', error.message);
process.exit(1);
}
-const exePath = path.join(buildDir, 'Release', 'wgc-capture.exe');
+const exePath = path.join(buildDir, 'Release', 'windows-capture.exe');
if (existsSync(exePath)) {
- console.log(`[build-wgc-capture] Built successfully: ${exePath}`);
+ console.log(`[build-windows-capture] Built successfully: ${exePath}`);
} else {
- console.error('[build-wgc-capture] Expected exe not found at', exePath);
+ console.error('[build-windows-capture] Expected exe not found at', exePath);
process.exit(1);
}
diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx
index 12590e9d..601a5f06 100644
--- a/src/components/launch/LaunchWindow.tsx
+++ b/src/components/launch/LaunchWindow.tsx
@@ -731,6 +731,10 @@ export function LaunchWindow() {
+ window.electronAPI?.hudOverlayHide?.()} title={t("recording.hideHud")}>
+
+
+
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index 1627c2e8..214b78da 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -38,7 +38,6 @@ import {
import { SettingsPanel } from "./SettingsPanel";
import TimelineEditor from "./timeline/TimelineEditor";
import {
- detectInteractionCandidates,
normalizeCursorTelemetry,
} from "./timeline/zoomSuggestionUtils";
import {
@@ -189,7 +188,6 @@ export default function VideoEditor() {
const nextAnnotationIdRef = useRef(1);
const nextAnnotationZIndexRef = useRef(1); // Track z-index for stacking order
const exporterRef = useRef(null);
- const autoSuggestedVideoPathRef = useRef(null);
const historyPastRef = useRef([]);
const historyFutureRef = useRef([]);
const historyCurrentRef = useRef(null);
@@ -910,84 +908,6 @@ export default function VideoEditor() {
return [...zoomRegions.filter((region) => region.id !== loopEndRegion.id), loopEndRegion];
}, [loopCursor, zoomRegions, displayedTimelineWindow, connectZooms]);
- useEffect(() => {
- if (
- !videoPath ||
- duration <= 0 ||
- zoomRegions.length > 0 ||
- normalizedCursorTelemetry.length < 2
- ) {
- return;
- }
-
- if (autoSuggestedVideoPathRef.current === videoPath) {
- return;
- }
-
- const totalMs = Math.max(0, Math.round(duration * 1000));
- if (totalMs <= 0) {
- return;
- }
-
- const candidates = detectInteractionCandidates(normalizedCursorTelemetry);
- if (candidates.length === 0) {
- autoSuggestedVideoPathRef.current = videoPath;
- return;
- }
-
- const DEFAULT_DURATION_MS = 1100;
- const MIN_SPACING_MS = 1800;
- const sortedCandidates = [...candidates].sort((a, b) => b.strength - a.strength);
- const acceptedCenters: number[] = [];
-
- setZoomRegions((prev) => {
- if (prev.length > 0) {
- return prev;
- }
-
- const reservedSpans: Array<{ start: number; end: number }> = [];
- const additions: ZoomRegion[] = [];
- let nextId = nextZoomIdRef.current;
-
- sortedCandidates.forEach((candidate) => {
- const tooCloseToAccepted = acceptedCenters.some(
- (center) => Math.abs(center - candidate.centerTimeMs) < MIN_SPACING_MS,
- );
- if (tooCloseToAccepted) {
- return;
- }
-
- const centeredStart = Math.round(candidate.centerTimeMs - DEFAULT_DURATION_MS / 2);
- const startMs = Math.max(0, Math.min(centeredStart, totalMs - DEFAULT_DURATION_MS));
- const endMs = Math.min(totalMs, startMs + DEFAULT_DURATION_MS);
-
- const hasOverlap = reservedSpans.some((span) => endMs > span.start && startMs < span.end);
- if (hasOverlap) {
- return;
- }
-
- additions.push({
- id: `zoom-${nextId++}`,
- startMs,
- endMs,
- depth: DEFAULT_ZOOM_DEPTH,
- focus: clampFocusToDepth(candidate.focus, DEFAULT_ZOOM_DEPTH),
- });
- reservedSpans.push({ start: startMs, end: endMs });
- acceptedCenters.push(candidate.centerTimeMs);
- });
-
- if (additions.length === 0) {
- return prev;
- }
-
- nextZoomIdRef.current = nextId;
- return [...prev, ...additions];
- });
-
- autoSuggestedVideoPathRef.current = videoPath;
- }, [videoPath, duration, normalizedCursorTelemetry, zoomRegions.length]);
-
// Initialize default wallpaper with resolved asset path
useEffect(() => {
let mounted = true;
diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts
index 8d0f1e38..2fab8d5d 100644
--- a/src/hooks/useScreenRecorder.ts
+++ b/src/hooks/useScreenRecorder.ts
@@ -534,19 +534,20 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
let videoTrack: MediaStreamTrack | undefined;
let systemAudioIncluded = false;
+ const browserScreenVideoConstraints = {
+ mandatory: {
+ chromeMediaSource: CHROME_MEDIA_SOURCE,
+ chromeMediaSourceId: selectedSource.id,
+ maxWidth: TARGET_WIDTH,
+ maxHeight: TARGET_HEIGHT,
+ maxFrameRate: TARGET_FRAME_RATE,
+ minFrameRate: MIN_FRAME_RATE,
+ googCaptureCursor: false,
+ },
+ cursor: "never" as const,
+ };
if (wantsAudioCapture) {
- const videoConstraints = {
- mandatory: {
- chromeMediaSource: CHROME_MEDIA_SOURCE,
- chromeMediaSourceId: selectedSource.id,
- maxWidth: TARGET_WIDTH,
- maxHeight: TARGET_HEIGHT,
- maxFrameRate: TARGET_FRAME_RATE,
- minFrameRate: MIN_FRAME_RATE,
- },
- };
-
let screenMediaStream: MediaStream;
if (systemAudioEnabled) {
@@ -558,20 +559,20 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
chromeMediaSourceId: selectedSource.id,
},
},
- video: videoConstraints,
+ video: browserScreenVideoConstraints,
});
} catch (audioError) {
console.warn("System audio capture failed, falling back to video-only:", audioError);
alert("System audio is not available for this source. Recording will continue without system audio.");
screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({
audio: false,
- video: videoConstraints,
+ video: browserScreenVideoConstraints,
});
}
} else {
screenMediaStream = await (navigator.mediaDevices as any).getUserMedia({
audio: false,
- video: videoConstraints,
+ video: browserScreenVideoConstraints,
});
}