mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 07:23:14 +00:00
Fix HUD capture protection across recording backends
This commit is contained in:
1 parent
b019824a29
commit
58faea68f1
9 files changed
+102
-17
No files matched your search
@@ -0,0 +1,29 @@
|
||||
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",
|
||||
);
|
||||
|
||||
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("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\(\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
systemPreferences,
|
||||
} from "electron";
|
||||
import { showCursor } from "../../cursorHider";
|
||||
import { getMonitorHandles } from "../monitorResolver";
|
||||
import {
|
||||
getHudOverlayCaptureProtectionEnabled,
|
||||
reassertHudOverlayCaptureProtection,
|
||||
} from "../../windows";
|
||||
import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants";
|
||||
import { startWindowBoundsCapture, stopWindowBoundsCapture } from "../cursor/bounds";
|
||||
import { startInteractionCapture, stopInteractionCapture } from "../cursor/interaction";
|
||||
@@ -31,6 +34,7 @@ import {
|
||||
writeCursorTelemetry,
|
||||
} from "../cursor/telemetry";
|
||||
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
|
||||
import { getMonitorHandles } from "../monitorResolver";
|
||||
import {
|
||||
ensureNativeCaptureHelperBinary,
|
||||
ensureSwiftHelperBinary,
|
||||
@@ -398,6 +402,10 @@ export function registerRecordingHandlers(
|
||||
ipcMain.handle(
|
||||
"start-native-screen-recording",
|
||||
async (_, source: SelectedSource, options?: NativeMacRecordingOptions) => {
|
||||
// Capture starts before the renderer publishes its recording-state
|
||||
// transition, so protect the HUD at the actual capture boundary.
|
||||
reassertHudOverlayCaptureProtection();
|
||||
|
||||
// Windows native capture path
|
||||
if (process.platform === "win32") {
|
||||
const windowsCaptureAvailable = await isNativeWindowsCaptureAvailable();
|
||||
@@ -726,6 +734,10 @@ export function registerRecordingHandlers(
|
||||
capturesMicrophone,
|
||||
};
|
||||
|
||||
if (getHudOverlayCaptureProtectionEnabled()) {
|
||||
config.excludedProcessIds = [process.pid];
|
||||
}
|
||||
|
||||
if (options?.microphoneDeviceId) {
|
||||
config.microphoneDeviceId = options.microphoneDeviceId;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
getUpdateToastWindow,
|
||||
hideUpdateToastWindow,
|
||||
isHudOverlayMousePassthroughSupported,
|
||||
reassertHudOverlayCaptureProtection,
|
||||
reassertHudOverlayMousePassthrough as reassertHudOverlayMouseState,
|
||||
setHudOverlayRecordingActive,
|
||||
showUpdateToastWindow,
|
||||
@@ -1075,6 +1076,10 @@ app.whenReady().then(async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Browser and Linux portal capture starts as soon as this callback
|
||||
// resolves, before recording-state-changed is emitted.
|
||||
reassertHudOverlayCaptureProtection();
|
||||
|
||||
const sourceId = getSelectedSourceId();
|
||||
// On Linux/Wayland, calling desktopCapturer.getSources() itself
|
||||
// invokes the xdg-desktop-portal picker. If we then return one of
|
||||
|
||||
@@ -14,6 +14,7 @@ struct CaptureConfig: Codable {
|
||||
let microphoneDeviceId: String?
|
||||
let microphoneLabel: String?
|
||||
let microphoneOutputPath: String?
|
||||
let excludedProcessIds: [Int32]?
|
||||
}
|
||||
|
||||
let targetCaptureFPS = 60
|
||||
@@ -140,7 +141,15 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
throw NSError(domain: "RecordlyCapture", code: 4, userInfo: [NSLocalizedDescriptionKey: "Display not found"])
|
||||
}
|
||||
|
||||
filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: [])
|
||||
let excludedProcessIds = Set(config.excludedProcessIds ?? [])
|
||||
let excludedApplications = availableContent.applications.filter {
|
||||
excludedProcessIds.contains($0.processID)
|
||||
}
|
||||
filter = SCContentFilter(
|
||||
display: display,
|
||||
excludingApplications: excludedApplications,
|
||||
exceptingWindows: []
|
||||
)
|
||||
let displayBounds = CGDisplayBounds(display.displayID)
|
||||
let scaleFactor = ScreenCaptureRecorder.scaleFactor(for: display.displayID)
|
||||
outputWidth = max(2, Int(displayBounds.width) * scaleFactor)
|
||||
|
||||
@@ -64,3 +64,11 @@ 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");
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
+36
-14
@@ -114,10 +114,6 @@ function getEditorWindowQuery(): Record<string, string> {
|
||||
return query;
|
||||
}
|
||||
|
||||
function isHudOverlayCaptureProtectionSupported(): boolean {
|
||||
return process.platform !== "linux";
|
||||
}
|
||||
|
||||
export function isHudOverlayMousePassthroughSupported(): boolean {
|
||||
return process.platform !== "linux";
|
||||
}
|
||||
@@ -146,6 +142,30 @@ function loadHudOverlayCaptureProtectionSetting(): boolean {
|
||||
return hudOverlayHiddenFromCapture;
|
||||
}
|
||||
|
||||
export function getHudOverlayCaptureProtectionEnabled(): boolean {
|
||||
return loadHudOverlayCaptureProtectionSetting();
|
||||
}
|
||||
|
||||
function applyHudOverlayCaptureProtectionToWindow(hud: BrowserWindow, enabled: boolean): void {
|
||||
try {
|
||||
hud.setContentProtection(enabled);
|
||||
} catch (error) {
|
||||
console.warn("Failed to apply HUD capture protection:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function reassertHudOverlayCaptureProtection(): boolean {
|
||||
const enabled = loadHudOverlayCaptureProtectionSetting();
|
||||
const hud = getHudOverlayWindow();
|
||||
if (!hud) {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
applyHudOverlayCaptureProtectionToWindow(hud, enabled);
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function persistHudOverlayCaptureProtectionSetting(enabled: boolean): void {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
@@ -413,13 +433,7 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
|
||||
hudOverlayHiddenFromCapture = Boolean(enabled);
|
||||
persistHudOverlayCaptureProtectionSetting(hudOverlayHiddenFromCapture);
|
||||
|
||||
if (
|
||||
isHudOverlayCaptureProtectionSupported() &&
|
||||
hudOverlayWindow &&
|
||||
!hudOverlayWindow.isDestroyed()
|
||||
) {
|
||||
hudOverlayWindow.setContentProtection(hudOverlayHiddenFromCapture);
|
||||
}
|
||||
reassertHudOverlayCaptureProtection();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -479,6 +493,9 @@ export function createHudOverlayWindow(): BrowserWindow {
|
||||
return;
|
||||
}
|
||||
hasShownHudWindow = true;
|
||||
// Showing or changing native window state can recreate platform window
|
||||
// flags. Reassert capture protection on both sides of the transition.
|
||||
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
|
||||
if (process.platform === "win32") {
|
||||
// A focusable window is required for a Windows taskbar entry, but the
|
||||
// always-on-top HUD must not steal focus when Recordly starts.
|
||||
@@ -487,6 +504,7 @@ export function createHudOverlayWindow(): BrowserWindow {
|
||||
win.show();
|
||||
}
|
||||
win.moveTop();
|
||||
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
|
||||
if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) {
|
||||
win.setIgnoreMouseEvents(false);
|
||||
setTimeout(() => {
|
||||
@@ -497,9 +515,12 @@ export function createHudOverlayWindow(): BrowserWindow {
|
||||
}
|
||||
};
|
||||
|
||||
if (isHudOverlayCaptureProtectionSupported()) {
|
||||
win.setContentProtection(hudOverlayHiddenFromCapture);
|
||||
}
|
||||
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
|
||||
win.on("show", () => {
|
||||
if (!win.isDestroyed()) {
|
||||
applyHudOverlayCaptureProtectionToWindow(win, hudOverlayHiddenFromCapture);
|
||||
}
|
||||
});
|
||||
|
||||
if (isHudOverlayMousePassthroughSupported()) {
|
||||
if (hudOverlayRecordingActive) {
|
||||
@@ -654,6 +675,7 @@ export function setHudOverlayRecordingActive(recording: boolean): void {
|
||||
hudOverlayRecordingActive = Boolean(recording);
|
||||
hudOverlayFallbackExpanded = false;
|
||||
applyHudOverlayBounds();
|
||||
reassertHudOverlayCaptureProtection();
|
||||
// Start in passthrough mode. Forwarded pointer movement lets the renderer
|
||||
// make the visible HUD controls interactive when the pointer reaches them,
|
||||
// while transparent parts never block the recorded application.
|
||||
|
||||
@@ -115,7 +115,7 @@ function LaunchWindowContent() {
|
||||
toggleHudCaptureProtection,
|
||||
} = useLaunchWindowSystemState(preparePermissions);
|
||||
|
||||
const supportsHudCaptureProtection = platform !== "linux";
|
||||
const supportsHudCaptureProtection = true;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDeviceId) {
|
||||
|
||||
Reference in new issue
Block a user