mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 06:46:09 +00:00
fix: stop live camera access while in editor mode
This commit is contained in:
Vendored
+2
@@ -223,6 +223,8 @@ interface Window {
|
||||
hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => void;
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
getEditorMode: () => Promise<boolean>;
|
||||
onEditorModeChanged: (callback: (inEditor: boolean) => void) => () => void;
|
||||
hudOverlayRendererReady: () => void;
|
||||
hudOverlaySetWebcamPreviewVisible: (visible: boolean) => void;
|
||||
getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>;
|
||||
|
||||
@@ -199,6 +199,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
hudOverlayClose: () => {
|
||||
ipcRenderer.send("hud-overlay-close");
|
||||
},
|
||||
getEditorMode: () => ipcRenderer.invoke("get-editor-mode"),
|
||||
onEditorModeChanged: (callback: (inEditor: boolean) => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, inEditor: boolean) => callback(inEditor);
|
||||
ipcRenderer.on("editor-mode-changed", listener);
|
||||
return () => ipcRenderer.removeListener("editor-mode-changed", listener);
|
||||
},
|
||||
hudOverlayRendererReady: () => {
|
||||
ipcRenderer.send("hud-overlay-renderer-ready");
|
||||
},
|
||||
|
||||
@@ -446,6 +446,14 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
|
||||
};
|
||||
});
|
||||
|
||||
const editorWindows = new Set<BrowserWindow>();
|
||||
function notifyEditorMode() {
|
||||
if (hudOverlayWindow && !hudOverlayWindow.webContents.isDestroyed()) {
|
||||
hudOverlayWindow.webContents.send("editor-mode-changed", editorWindows.size > 0);
|
||||
}
|
||||
}
|
||||
ipcMain.handle("get-editor-mode", () => editorWindows.size > 0);
|
||||
|
||||
export function createHudOverlayWindow(): BrowserWindow {
|
||||
const perfStart = Date.now();
|
||||
loadHudOverlayCaptureProtectionSetting();
|
||||
@@ -938,6 +946,13 @@ export function createEditorWindow(): BrowserWindow {
|
||||
},
|
||||
});
|
||||
|
||||
editorWindows.add(win);
|
||||
notifyEditorMode();
|
||||
win.once("closed", () => {
|
||||
editorWindows.delete(win);
|
||||
notifyEditorMode();
|
||||
});
|
||||
|
||||
const publishWindowChrome = () => {
|
||||
if (!win.isDestroyed())
|
||||
win.webContents.send("window-chrome-changed", {
|
||||
|
||||
@@ -17,6 +17,27 @@ export function useWebcamPreviewOverlay({
|
||||
webcamPopoverOpen: boolean;
|
||||
hudOverlayMousePassthroughSupported: boolean | null;
|
||||
}) {
|
||||
const [editorMode, setEditorMode] = useState(true);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let receivedEvent = false;
|
||||
const unsubscribe = window.electronAPI.onEditorModeChanged((inEditor) => {
|
||||
receivedEvent = true;
|
||||
if (active) setEditorMode(inEditor);
|
||||
});
|
||||
void window.electronAPI
|
||||
.getEditorMode()
|
||||
.then((inEditor) => {
|
||||
if (active && !receivedEvent) setEditorMode(inEditor);
|
||||
})
|
||||
.catch(() => {
|
||||
/* Keep the camera off if editor mode is unknown. */
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
const [showFloatingWebcamPreview, setShowFloatingWebcamPreview] = useState(true);
|
||||
const [webcamPreviewOffset, setWebcamPreviewOffset] = useState(DEFAULT_WEBCAM_PREVIEW_OFFSET);
|
||||
const webcamPreviewOffsetRef = useRef(DEFAULT_WEBCAM_PREVIEW_OFFSET);
|
||||
@@ -46,7 +67,9 @@ export function useWebcamPreviewOverlay({
|
||||
hudOverlayMousePassthroughSupported,
|
||||
);
|
||||
const shouldStreamWebcamPreview =
|
||||
webcamEnabled && (showRecordingWebcamPreview || (showWebcamControls && webcamPopoverOpen));
|
||||
!editorMode &&
|
||||
webcamEnabled &&
|
||||
(showRecordingWebcamPreview || (showWebcamControls && webcamPopoverOpen));
|
||||
|
||||
useEffect(() => {
|
||||
if (!webcamEnabled) {
|
||||
|
||||
@@ -6,8 +6,6 @@ export interface VideoDevice {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
let hasRequestedVideoLabels = false;
|
||||
|
||||
export function useVideoDevices(enabled: boolean = true) {
|
||||
const [devices, setDevices] = useState<VideoDevice[]>([]);
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string>("default");
|
||||
@@ -24,7 +22,6 @@ export function useVideoDevices(enabled: boolean = true) {
|
||||
|
||||
const loadDevices = async () => {
|
||||
const loadId = ++activeLoadId;
|
||||
let permissionStream: MediaStream | null = null;
|
||||
|
||||
try {
|
||||
if (mounted && loadId === activeLoadId) {
|
||||
@@ -32,8 +29,8 @@ export function useVideoDevices(enabled: boolean = true) {
|
||||
setError(null);
|
||||
}
|
||||
|
||||
let allDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
let videoInputs = allDevices
|
||||
const allDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
const videoInputs = allDevices
|
||||
.filter((device) => device.kind === "videoinput")
|
||||
.map((device, index) => ({
|
||||
deviceId: device.deviceId,
|
||||
@@ -41,25 +38,6 @@ export function useVideoDevices(enabled: boolean = true) {
|
||||
groupId: device.groupId,
|
||||
}));
|
||||
|
||||
const needsLabelPermission =
|
||||
videoInputs.length > 0 && videoInputs.every((device) => !device.label.trim());
|
||||
|
||||
if (needsLabelPermission && !hasRequestedVideoLabels) {
|
||||
permissionStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false,
|
||||
});
|
||||
allDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
videoInputs = allDevices
|
||||
.filter((device) => device.kind === "videoinput")
|
||||
.map((device, index) => ({
|
||||
deviceId: device.deviceId,
|
||||
label: device.label || `Camera ${index + 1}`,
|
||||
groupId: device.groupId,
|
||||
}));
|
||||
hasRequestedVideoLabels = true;
|
||||
}
|
||||
|
||||
if (mounted && loadId === activeLoadId) {
|
||||
setDevices(videoInputs);
|
||||
setSelectedDeviceId((currentDeviceId) => {
|
||||
@@ -87,7 +65,6 @@ export function useVideoDevices(enabled: boolean = true) {
|
||||
console.error("Error loading video devices:", error);
|
||||
}
|
||||
} finally {
|
||||
permissionStream?.getTracks().forEach((track) => track.stop());
|
||||
if (mounted && loadId === activeLoadId) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ export async function installDesktopBridge(page: Page, videoFixture = "preview.m
|
||||
hudOverlayRendererReady: success,
|
||||
hudOverlaySetIgnoreMouse: success,
|
||||
hudOverlaySetWebcamPreviewVisible: success,
|
||||
getEditorMode: async () => false,
|
||||
onEditorModeChanged: subscribe,
|
||||
getActiveCountdown: async () => ({ success: true, seconds: 3 }),
|
||||
onCountdownTick: subscribe,
|
||||
cancelCountdown: async () => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { installDesktopBridge, installDesktopBridgeOverrides } from "./bridge";
|
||||
|
||||
test("editor mode never opens the camera and releases an active recorder preview", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installDesktopBridge(page);
|
||||
await installDesktopBridgeOverrides(page, () => {
|
||||
let visible: ((value: boolean) => void) | undefined;
|
||||
window.electronAPI.getRecordingPreferences = async () => ({
|
||||
success: true,
|
||||
webcamEnabled: true,
|
||||
microphoneEnabled: false,
|
||||
systemAudioEnabled: false,
|
||||
});
|
||||
window.electronAPI.getEditorMode = async () => true;
|
||||
window.electronAPI.onEditorModeChanged = (callback) => {
|
||||
visible = callback;
|
||||
return () => {
|
||||
visible = undefined;
|
||||
};
|
||||
};
|
||||
window.addEventListener("test-leave-editor", () => visible?.(false));
|
||||
window.addEventListener("test-enter-editor", () => visible?.(true));
|
||||
let requests = 0;
|
||||
Object.defineProperty(navigator.mediaDevices, "getUserMedia", {
|
||||
value: async () => {
|
||||
document.documentElement.dataset.cameraRequests = String(++requests);
|
||||
const canvas = document.createElement("canvas");
|
||||
const stream = canvas.captureStream();
|
||||
for (const track of stream.getTracks()) {
|
||||
const stop = track.stop.bind(track);
|
||||
track.stop = () => {
|
||||
document.documentElement.dataset.cameraStopped = "true";
|
||||
stop();
|
||||
};
|
||||
}
|
||||
return stream;
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.goto("/?windowType=hud-overlay");
|
||||
await expect(page.getByRole("button", { name: "Disable webcam" })).toBeVisible();
|
||||
expect(await page.locator("html").getAttribute("data-camera-requests")).toBeNull();
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("test-leave-editor")));
|
||||
await expect(page.locator("html")).toHaveAttribute("data-camera-requests", "1");
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("test-enter-editor")));
|
||||
await expect(page.locator("html")).toHaveAttribute("data-camera-stopped", "true");
|
||||
await page.evaluate(() => navigator.mediaDevices.dispatchEvent(new Event("devicechange")));
|
||||
await expect(page.locator("html")).toHaveAttribute("data-camera-requests", "1");
|
||||
});
|
||||
Reference in New Issue
Block a user