mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
fix(capture): stabilize platform source handling
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user