fix(capture): stabilize platform source handling

This commit is contained in:
wiiiii123
2026-05-26 23:12:01 +07:00
parent 73bd4f11f5
commit 31d1022fef
6 changed files with 167 additions and 19 deletions
+21 -15
View File
@@ -147,7 +147,7 @@ import {
parseJsonWithByteOrderMark,
parseWindowId,
} from "../utils";
import { resolveWindowsCaptureDisplay } from "../windowsCaptureSelection";
import { resolveWindowsCaptureTarget } from "../windowsCaptureSelection";
const execFileAsync = promisify(execFile);
@@ -442,15 +442,13 @@ export function registerRecordingHandlers(
const browserMicFallbackRequested =
shouldStartWindowsBrowserMicrophoneFallback(options);
const windowId = parseWindowId(source?.id);
const isWindowCapture = Boolean(windowId && source?.id?.startsWith("window:"));
const resolvedDisplay = resolveWindowsCaptureDisplay(
const captureTarget = resolveWindowsCaptureTarget(
source,
getScreen().getAllDisplays(),
getScreen().getPrimaryDisplay(),
);
const displayBounds = resolvedDisplay.bounds;
const displayBounds =
captureTarget.kind === "display" ? captureTarget.bounds : null;
setWindowsOrphanedMicAudioPath(null);
const config: Record<string, unknown> = {
@@ -458,29 +456,37 @@ export function registerRecordingHandlers(
fps: 60,
};
if (isWindowCapture) {
config.windowHandle = windowId;
if (captureTarget.kind === "invalid-window") {
return {
success: false,
message:
"Selected window is no longer available. Please choose the window again.",
};
}
if (captureTarget.kind === "window") {
config.windowHandle = captureTarget.windowHandle;
} else {
// Windows Graphics Capture (WGC) requires a raw HMONITOR handle.
// We attempt to resolve the handle by matching the physical coordinates of the target display.
const monitors = getMonitorHandles();
const matchedMonitor = monitors.find(
(monitor) =>
monitor.x === Math.round(displayBounds.x) &&
monitor.y === Math.round(displayBounds.y),
monitor.x === Math.round(captureTarget.bounds.x) &&
monitor.y === Math.round(captureTarget.bounds.y),
);
if (matchedMonitor) {
config.displayId = matchedMonitor.handle;
} else {
// Fallback to coordinate-based matching if handle resolution fails
config.displayId = resolvedDisplay.displayId;
config.displayId = captureTarget.displayId;
}
config.displayX = Math.round(resolvedDisplay.bounds.x);
config.displayY = Math.round(resolvedDisplay.bounds.y);
config.displayW = Math.round(resolvedDisplay.bounds.width);
config.displayH = Math.round(resolvedDisplay.bounds.height);
config.displayX = Math.round(captureTarget.bounds.x);
config.displayY = Math.round(captureTarget.bounds.y);
config.displayW = Math.round(captureTarget.bounds.width);
config.displayH = Math.round(captureTarget.bounds.height);
}
if (options?.capturesSystemAudio) {
+65 -1
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { resolveWindowsCaptureDisplay } from "./windowsCaptureSelection";
import {
resolveWindowsCaptureDisplay,
resolveWindowsCaptureTarget,
} from "./windowsCaptureSelection";
describe("resolveWindowsCaptureDisplay", () => {
const primaryDisplay = {
@@ -62,3 +65,64 @@ describe("resolveWindowsCaptureDisplay", () => {
});
});
});
describe("resolveWindowsCaptureTarget", () => {
const primaryDisplay = {
id: 101,
bounds: {
x: 0,
y: 0,
width: 1920,
height: 1080,
},
};
const secondaryDisplay = {
id: 202,
bounds: {
x: 1920,
y: -40,
width: 2560,
height: 1440,
},
};
it("uses a window handle when a Windows window source is selected", () => {
const resolved = resolveWindowsCaptureTarget(
{ id: "window:123456:0", sourceType: "window" },
[primaryDisplay, secondaryDisplay],
primaryDisplay,
);
expect(resolved).toEqual({
kind: "window",
windowHandle: 123456,
});
});
it("does not silently turn an invalid window source into display capture", () => {
const resolved = resolveWindowsCaptureTarget(
{ id: "window:0:0", sourceType: "window", display_id: String(secondaryDisplay.id) },
[primaryDisplay, secondaryDisplay],
primaryDisplay,
);
expect(resolved).toEqual({
kind: "invalid-window",
});
});
it("keeps display capture behavior for selected screens", () => {
const resolved = resolveWindowsCaptureTarget(
{ id: "screen:202:0", sourceType: "screen", display_id: String(secondaryDisplay.id) },
[primaryDisplay, secondaryDisplay],
primaryDisplay,
);
expect(resolved).toEqual({
kind: "display",
displayId: secondaryDisplay.id,
bounds: secondaryDisplay.bounds,
});
});
});
+60
View File
@@ -1,5 +1,7 @@
export type WindowsCaptureSourceLike = {
id?: string;
display_id?: string;
sourceType?: string;
};
export type WindowsCaptureDisplayBounds = {
@@ -19,6 +21,38 @@ export type ResolvedWindowsCaptureDisplay = {
bounds: WindowsCaptureDisplayBounds;
};
export type ResolvedWindowsCaptureTarget =
| {
kind: "window";
windowHandle: number;
}
| {
kind: "display";
displayId: number;
bounds: WindowsCaptureDisplayBounds;
}
| {
kind: "invalid-window";
};
function parseDesktopCapturerWindowHandle(sourceId?: string) {
if (!sourceId) {
return null;
}
const match = sourceId.match(/^window:(\d+)/);
if (!match) {
return null;
}
const handle = Number.parseInt(match[1], 10);
return Number.isFinite(handle) && handle > 0 ? handle : null;
}
function isWindowCaptureSource(source: WindowsCaptureSourceLike | null | undefined) {
return source?.sourceType === "window" || source?.id?.startsWith("window:") === true;
}
export function resolveWindowsCaptureDisplay(
source: WindowsCaptureSourceLike | null | undefined,
allDisplays: WindowsCaptureDisplayLike[],
@@ -40,3 +74,29 @@ export function resolveWindowsCaptureDisplay(
bounds: matchedDisplay.bounds,
};
}
export function resolveWindowsCaptureTarget(
source: WindowsCaptureSourceLike | null | undefined,
allDisplays: WindowsCaptureDisplayLike[],
primaryDisplay: WindowsCaptureDisplayLike,
): ResolvedWindowsCaptureTarget {
if (isWindowCaptureSource(source)) {
const windowHandle = parseDesktopCapturerWindowHandle(source?.id);
if (windowHandle !== null) {
return {
kind: "window",
windowHandle,
};
}
return {
kind: "invalid-window",
};
}
const resolvedDisplay = resolveWindowsCaptureDisplay(source, allDisplays, primaryDisplay);
return {
kind: "display",
...resolvedDisplay,
};
}
+3 -1
View File
@@ -272,7 +272,9 @@ function setHudOverlayFallbackExpanded(expanded: boolean) {
ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => {
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
if (!isHudOverlayMousePassthroughSupported()) {
setHudOverlayFallbackExpanded(!ignore);
if (process.platform !== "linux") {
setHudOverlayFallbackExpanded(!ignore);
}
hudOverlayWindow.setIgnoreMouseEvents(false);
return;
}
+11
View File
@@ -4,6 +4,7 @@ import {
createProcessedMicrophoneConstraints,
normalizeBrowserMicrophoneProfile,
resolveBrowserCaptureCursorPolicy,
shouldUseNativeWindowsCaptureForSource,
} from "./useScreenRecorder";
type RecordingState = "inactive" | "recording" | "paused";
@@ -129,6 +130,16 @@ describe("resolveBrowserCaptureCursorPolicy", () => {
});
});
describe("shouldUseNativeWindowsCaptureForSource", () => {
it("keeps native Windows capture on screen sources", () => {
expect(shouldUseNativeWindowsCaptureForSource({ id: "screen:101:0" })).toBe(true);
});
it("routes window sources through browser capture", () => {
expect(shouldUseNativeWindowsCaptureForSource({ id: "window:123456:0" })).toBe(false);
});
});
function stopRecording(
recorder: ReturnType<typeof createMockMediaRecorder>,
isNativeRecording: boolean,
+7 -2
View File
@@ -211,6 +211,12 @@ export function resolveBrowserCaptureCursorPolicy({
};
}
export function shouldUseNativeWindowsCaptureForSource(
source: Pick<ProcessedDesktopSource, "id"> | null | undefined,
): boolean {
return source?.id?.startsWith("screen:") === true;
}
export function createProcessedMicrophoneConstraints(
microphoneDeviceId?: string,
profile: BrowserMicrophoneProfile = DEFAULT_BROWSER_MICROPHONE_PROFILE,
@@ -1362,8 +1368,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
let nativeWindowsCaptureStartFailed = false;
if (
platform === "win32" &&
(selectedSource.id?.startsWith("screen:") ||
selectedSource.id?.startsWith("window:")) &&
shouldUseNativeWindowsCaptureForSource(selectedSource) &&
typeof window.electronAPI.isNativeWindowsCaptureAvailable === "function"
) {
try {