From 7899d8fce0cdaa7b09715baeb4eec6018e0f8115 Mon Sep 17 00:00:00 2001 From: young Date: Thu, 3 Sep 2026 11:51:48 +1000 Subject: [PATCH] Address HUD protection review feedback --- electron/hudCaptureProtection.test.ts | 34 +++++++------------ electron/ipc/register/recording.ts | 10 ++++-- .../native/ScreenCaptureKitRecorder.test.ts | 8 ----- electron/windows.ts | 5 +++ src/components/launch/LaunchWindow.tsx | 5 +-- src/lib/hudCaptureProtection.test.ts | 33 ++++++++++++++++++ src/lib/hudCaptureProtection.ts | 15 ++++++++ 7 files changed, 77 insertions(+), 33 deletions(-) create mode 100644 src/lib/hudCaptureProtection.test.ts create mode 100644 src/lib/hudCaptureProtection.ts diff --git a/electron/hudCaptureProtection.test.ts b/electron/hudCaptureProtection.test.ts index 27bdd2cd..30b0e2b9 100644 --- a/electron/hudCaptureProtection.test.ts +++ b/electron/hudCaptureProtection.test.ts @@ -1,29 +1,21 @@ -import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; -const windowsSource = readFileSync(fileURLToPath(new URL("./windows.ts", import.meta.url)), "utf8"); -const mainSource = readFileSync(fileURLToPath(new URL("./main.ts", import.meta.url)), "utf8"); -const recordingSource = readFileSync( - fileURLToPath(new URL("./ipc/register/recording.ts", import.meta.url)), - "utf8", -); +import { + getHudCaptureExcludedProcessIds, + supportsHudCaptureProtection, +} from "../src/lib/hudCaptureProtection"; describe("HUD capture protection lifecycle", () => { - it("applies protection without disabling Linux", () => { - expect(windowsSource).not.toContain( - "function isHudOverlayCaptureProtectionSupported(): boolean", - ); - expect(windowsSource).toContain("hud.setContentProtection(enabled)"); - expect(windowsSource).toContain('win.on("show"'); + it("uses window protection on Windows and macOS only", () => { + expect(supportsHudCaptureProtection("win32")).toBe(true); + expect(supportsHudCaptureProtection("darwin")).toBe(true); + expect(supportsHudCaptureProtection("linux")).toBe(false); }); - it("reasserts protection at native and browser capture boundaries", () => { - expect(recordingSource).toMatch( - /"start-native-screen-recording"[\s\S]*?reassertHudOverlayCaptureProtection\(\)/, - ); - expect(mainSource).toMatch( - /setDisplayMediaRequestHandler[\s\S]*?reassertHudOverlayCaptureProtection\(\)/, - ); + it("only builds a macOS process exclusion when protection is enabled", () => { + expect(getHudCaptureExcludedProcessIds("darwin", true, 734)).toEqual([734]); + expect(getHudCaptureExcludedProcessIds("darwin", false, 734)).toEqual([]); + expect(getHudCaptureExcludedProcessIds("win32", true, 734)).toEqual([]); + expect(getHudCaptureExcludedProcessIds("linux", true, 734)).toEqual([]); }); }); diff --git a/electron/ipc/register/recording.ts b/electron/ipc/register/recording.ts index 372d7976..a1b62504 100644 --- a/electron/ipc/register/recording.ts +++ b/electron/ipc/register/recording.ts @@ -12,6 +12,7 @@ import { shell, systemPreferences, } from "electron"; +import { getHudCaptureExcludedProcessIds } from "../../../src/lib/hudCaptureProtection"; import { showCursor } from "../../cursorHider"; import { getHudOverlayCaptureProtectionEnabled, @@ -734,8 +735,13 @@ export function registerRecordingHandlers( capturesMicrophone, }; - if (getHudOverlayCaptureProtectionEnabled()) { - config.excludedProcessIds = [process.pid]; + const excludedProcessIds = getHudCaptureExcludedProcessIds( + process.platform, + getHudOverlayCaptureProtectionEnabled(), + process.pid, + ); + if (excludedProcessIds.length > 0) { + config.excludedProcessIds = excludedProcessIds; } if (options?.microphoneDeviceId) { diff --git a/electron/native/ScreenCaptureKitRecorder.test.ts b/electron/native/ScreenCaptureKitRecorder.test.ts index a309de97..0956bf6f 100644 --- a/electron/native/ScreenCaptureKitRecorder.test.ts +++ b/electron/native/ScreenCaptureKitRecorder.test.ts @@ -64,11 +64,3 @@ describe("ScreenCaptureKitRecorder colour metadata", () => { expect(recorderSource).toContain("AVVideoYCbCrMatrix_ITU_R_709_2"); }); }); - -describe("ScreenCaptureKitRecorder HUD capture protection", () => { - it("excludes protected Recordly applications from display capture", () => { - expect(recorderSource).toContain("let excludedProcessIds: [Int32]?"); - expect(recorderSource).toContain("excludedProcessIds.contains($0.processID)"); - expect(recorderSource).toContain("excludingApplications: excludedApplications"); - }); -}); diff --git a/electron/windows.ts b/electron/windows.ts index 420ea93f..47e90b8f 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -3,6 +3,7 @@ import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; +import { supportsHudCaptureProtection } from "../src/lib/hudCaptureProtection"; import { USER_DATA_PATH } from "./appPaths"; import { getHudOverlayWindowBounds, @@ -147,6 +148,10 @@ export function getHudOverlayCaptureProtectionEnabled(): boolean { } function applyHudOverlayCaptureProtectionToWindow(hud: BrowserWindow, enabled: boolean): void { + if (!supportsHudCaptureProtection(process.platform)) { + return; + } + try { hud.setContentProtection(enabled); } catch (error) { diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index c2e2a932..f7583b28 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -19,6 +19,7 @@ import { useScopedT } from "../../contexts/I18nContext"; import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; import { useScreenRecorder } from "../../hooks/useScreenRecorder"; import { useVideoDevices } from "../../hooks/useVideoDevices"; +import { supportsHudCaptureProtection } from "../../lib/hudCaptureProtection"; import { Button } from "../ui/button"; import { HudInteractionContext } from "./contexts/HudInteractionContext"; import { canToggleFloatingWebcamPreview } from "./floatingWebcamPreview"; @@ -115,7 +116,7 @@ function LaunchWindowContent() { toggleHudCaptureProtection, } = useLaunchWindowSystemState(preparePermissions); - const supportsHudCaptureProtection = true; + const hudCaptureProtectionSupported = supportsHudCaptureProtection(platform ?? ""); useEffect(() => { if (!selectedDeviceId) { @@ -372,7 +373,7 @@ function LaunchWindowContent() { { void toggleHudCaptureProtection(); diff --git a/src/lib/hudCaptureProtection.test.ts b/src/lib/hudCaptureProtection.test.ts new file mode 100644 index 00000000..912dd548 --- /dev/null +++ b/src/lib/hudCaptureProtection.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; + +import { + getHudCaptureExcludedProcessIds, + supportsHudCaptureProtection, +} from "./hudCaptureProtection"; + +describe("supportsHudCaptureProtection", () => { + it.each([ + ["win32", true], + ["darwin", true], + ["linux", false], + ["freebsd", false], + ])("reports support for %s as %s", (platform, expected) => { + expect(supportsHudCaptureProtection(platform)).toBe(expected); + }); +}); + +describe("getHudCaptureExcludedProcessIds", () => { + it("passes the current process to macOS capture when protection is enabled", () => { + expect(getHudCaptureExcludedProcessIds("darwin", true, 4512)).toEqual([4512]); + }); + + it.each([ + ["darwin", false, 4512], + ["win32", true, 4512], + ["linux", true, 4512], + ["darwin", true, 0], + ["darwin", true, Number.NaN], + ])("returns no native exclusions for %s, enabled=%s, pid=%s", (platform, enabled, pid) => { + expect(getHudCaptureExcludedProcessIds(platform, enabled, pid as number)).toEqual([]); + }); +}); diff --git a/src/lib/hudCaptureProtection.ts b/src/lib/hudCaptureProtection.ts new file mode 100644 index 00000000..980bcbc6 --- /dev/null +++ b/src/lib/hudCaptureProtection.ts @@ -0,0 +1,15 @@ +export function supportsHudCaptureProtection(platform: string): boolean { + return platform === "darwin" || platform === "win32"; +} + +export function getHudCaptureExcludedProcessIds( + platform: string, + enabled: boolean, + processId: number, +): number[] { + if (platform !== "darwin" || !enabled || !Number.isSafeInteger(processId) || processId <= 0) { + return []; + } + + return [processId]; +}