-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: live camera preview overlay (visible before & during recording) #441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ const RENDERER_DIST = path.join(APP_ROOT, "dist"); | |||||||||||||
| const HEADLESS = process.env["HEADLESS"] === "true"; | ||||||||||||||
|
|
||||||||||||||
| let hudOverlayWindow: BrowserWindow | null = null; | ||||||||||||||
| let cameraPreviewWindow: BrowserWindow | null = null; | ||||||||||||||
|
|
||||||||||||||
| ipcMain.on("hud-overlay-hide", () => { | ||||||||||||||
| if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { | ||||||||||||||
|
|
@@ -85,6 +86,77 @@ export function createHudOverlayWindow(): BrowserWindow { | |||||||||||||
| return win; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Creates a small always-on-top floating window that shows the live webcam | ||||||||||||||
| * feed during recording so the user can see their face overlay in real time. | ||||||||||||||
| */ | ||||||||||||||
| export function createCameraPreviewWindow(deviceId: string): BrowserWindow { | ||||||||||||||
| const primaryDisplay = screen.getPrimaryDisplay(); | ||||||||||||||
| const { workArea } = primaryDisplay; | ||||||||||||||
|
|
||||||||||||||
| const windowSize = 220; | ||||||||||||||
| const margin = 24; | ||||||||||||||
|
|
||||||||||||||
| const x = Math.floor(workArea.x + workArea.width - windowSize - margin); | ||||||||||||||
| const y = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD | ||||||||||||||
|
Comment on lines
+100
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Window position math can place preview off-screen on small displays. Line 101 uses a fixed nit: cleaner bounds-safe positioning-const x = Math.floor(workArea.x + workArea.width - windowSize - margin);
-const y = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD
+const desiredX = Math.floor(workArea.x + workArea.width - windowSize - margin);
+const desiredY = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD
+const x = Math.max(workArea.x, Math.min(desiredX, workArea.x + workArea.width - windowSize));
+const y = Math.max(workArea.y, Math.min(desiredY, workArea.y + workArea.height - windowSize));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| if (cameraPreviewWindow && !cameraPreviewWindow.isDestroyed()) { | ||||||||||||||
| cameraPreviewWindow.close(); | ||||||||||||||
| cameraPreviewWindow = null; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const win = new BrowserWindow({ | ||||||||||||||
| width: windowSize, | ||||||||||||||
| height: windowSize, | ||||||||||||||
| x, | ||||||||||||||
| y, | ||||||||||||||
| frame: false, | ||||||||||||||
| transparent: true, | ||||||||||||||
| resizable: false, | ||||||||||||||
| alwaysOnTop: true, | ||||||||||||||
| skipTaskbar: true, | ||||||||||||||
| hasShadow: false, | ||||||||||||||
| show: !HEADLESS, | ||||||||||||||
| webPreferences: { | ||||||||||||||
| preload: path.join(__dirname, "preload.mjs"), | ||||||||||||||
| nodeIntegration: false, | ||||||||||||||
| contextIsolation: true, | ||||||||||||||
| backgroundThrottling: false, | ||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| if (process.platform === "darwin") { | ||||||||||||||
| win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| cameraPreviewWindow = win; | ||||||||||||||
|
|
||||||||||||||
| win.on("closed", () => { | ||||||||||||||
| if (cameraPreviewWindow === win) { | ||||||||||||||
| cameraPreviewWindow = null; | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| const encodedDeviceId = encodeURIComponent(deviceId); | ||||||||||||||
|
|
||||||||||||||
| if (VITE_DEV_SERVER_URL) { | ||||||||||||||
| win.loadURL(`${VITE_DEV_SERVER_URL}?windowType=camera-preview&deviceId=${encodedDeviceId}`); | ||||||||||||||
| } else { | ||||||||||||||
| win.loadFile(path.join(RENDERER_DIST, "index.html"), { | ||||||||||||||
| query: { windowType: "camera-preview", deviceId }, | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return win; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export function closeCameraPreviewWindow(): void { | ||||||||||||||
| if (cameraPreviewWindow && !cameraPreviewWindow.isDestroyed()) { | ||||||||||||||
| cameraPreviewWindow.close(); | ||||||||||||||
| cameraPreviewWindow = null; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Creates the main editor window. Starts maximised with a hidden title bar on | ||||||||||||||
| * macOS. This window is not always-on-top and appears in the taskbar/dock. | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,166 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useRef, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Floating camera preview window — shown whenever the webcam is enabled, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * both before and during recording. Renders the live feed in a draggable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * circle. On hover, a collapse button appears so the user can tuck it away | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * without turning off the webcam. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function CameraPreviewWindow() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const videoRef = useRef<HTMLVideoElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [collapsed, setCollapsed] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [hovered, setHovered] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const params = new URLSearchParams(window.location.search); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const deviceId = params.get("deviceId") ?? ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const constraints: MediaStreamConstraints = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| video: deviceId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 720 } } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : { width: { ideal: 1280 }, height: { ideal: 720 } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| audio: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let stream: MediaStream | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| navigator.mediaDevices | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .getUserMedia(constraints) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((s) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stream = s; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (videoRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| videoRef.current.srcObject = s; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch(console.error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stream?.getTracks().forEach((t) => t.stop()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n src/components/launch/CameraPreviewWindow.tsx | head -60Repository: siddharthvaddem/openscreen Length of output: 2097
If the component unmounts before the promise resolves, the .then() handler runs after cleanup and creates tracks that never get stopped. lowkey cursed for resource lifecycle. Fix with disposed guard let stream: MediaStream | null = null;
+ let disposed = false;
navigator.mediaDevices
.getUserMedia(constraints)
.then((s) => {
+ if (disposed) {
+ s.getTracks().forEach((t) => t.stop());
+ return;
+ }
stream = s;
if (videoRef.current) {
videoRef.current.srcObject = s;
}
})
.catch(console.error);
return () => {
+ disposed = true;
stream?.getTracks().forEach((t) => t.stop());
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (collapsed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: "100vw", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: "100vh", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: "flex", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alignItems: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justifyContent: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: "transparent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd "CameraPreviewWindow.tsx" --type fRepository: siddharthvaddem/openscreen Length of output: 115 🏁 Script executed: cat -n src/components/launch/CameraPreviewWindow.tsx | head -120Repository: siddharthvaddem/openscreen Length of output: 4139 Collapsed state loses drag behavior — add Expanded mode sets 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Collapsed pill — click to expand */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setCollapsed(false)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Show camera preview" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: "flex", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alignItems: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gap: 6, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: "6px 12px", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| borderRadius: 999, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: "rgba(20,20,28,0.88)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: "1.5px solid rgba(255,255,255,0.15)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boxShadow: "0 4px 16px rgba(0,0,0,0.5)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: "rgba(255,255,255,0.75)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fontSize: 12, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fontFamily: "system-ui, sans-serif", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor: "pointer", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-expect-error Electron drag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebkitAppRegion: "no-drag", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Camera icon */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path d="M23 7l-7 5 7 5V7z"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Show | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: "100vw", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: "100vh", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: "flex", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alignItems: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justifyContent: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: "transparent", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-expect-error Electron drag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebkitAppRegion: "drag", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor: "grab", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMouseEnter={() => setHovered(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onMouseLeave={() => setHovered(false)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Circle video frame */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div style={{ position: "relative", width: 196, height: 196, flexShrink: 0 }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 196, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 196, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| borderRadius: "50%", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overflow: "hidden", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: "3px solid rgba(255,255,255,0.22)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boxShadow: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "0 8px 32px rgba(0,0,0,0.65), 0 0 0 1px rgba(255,255,255,0.08), inset 0 0 0 1px rgba(0,0,0,0.3)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: "#111", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <video | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={videoRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| autoPlay | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| muted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| playsInline | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: "100%", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: "100%", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| objectFit: "cover", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: "scaleX(-1)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: "block", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Hover controls */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {hovered && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => setCollapsed(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title="Hide camera preview" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| position: "absolute", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| top: 8, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| right: 8, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 28, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 28, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| borderRadius: "50%", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| background: "rgba(0,0,0,0.65)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| border: "1px solid rgba(255,255,255,0.2)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: "rgba(255,255,255,0.85)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: "flex", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alignItems: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| justifyContent: "center", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor: "pointer", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-expect-error Electron drag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebkitAppRegion: "no-drag", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| backdropFilter: "blur(6px)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Eye-off icon */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <line x1="1" y1="1" x2="23" y2="23"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -152,6 +152,19 @@ export function LaunchWindow() { | |
| } | ||
| }, [selectedCameraId, setWebcamDeviceId]); | ||
|
|
||
| // Show live camera preview as soon as webcam is enabled (before and during recording). | ||
| // Only retrigger when webcamEnabled flips — not on every device-list load. | ||
| useEffect(() => { | ||
| if (!window.electronAPI) return; | ||
| if (webcamEnabled) { | ||
| const deviceId = webcamDeviceId || selectedCameraId || ""; | ||
| window.electronAPI.showCameraPreview(deviceId); | ||
| } else { | ||
| window.electronAPI.hideCameraPreview(); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [webcamEnabled]); | ||
|
Comment on lines
+157
to
+166
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd "LaunchWindow.tsx" --type fRepository: siddharthvaddem/openscreen Length of output: 108 🏁 Script executed: if [ -f "src/components/launch/LaunchWindow.tsx" ]; then
wc -l "src/components/launch/LaunchWindow.tsx"
fiRepository: siddharthvaddem/openscreen Length of output: 112 🏁 Script executed: if [ -f "src/components/launch/LaunchWindow.tsx" ]; then
sed -n '150,175p' "src/components/launch/LaunchWindow.tsx"
fiRepository: siddharthvaddem/openscreen Length of output: 885 Camera preview stays stale when you switch devices while recording (missing deps at line 165). Rn the eslint-disable-next-line silences a legit warning — you're using Proposed fix-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
@@
+const lastPreviewDeviceIdRef = useRef<string | null>(null);
+
useEffect(() => {
if (!window.electronAPI) return;
- if (webcamEnabled) {
- const deviceId = webcamDeviceId || selectedCameraId || "";
- window.electronAPI.showCameraPreview(deviceId);
- } else {
+ if (!webcamEnabled) {
window.electronAPI.hideCameraPreview();
+ lastPreviewDeviceIdRef.current = null;
+ return;
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
-}, [webcamEnabled]);
+ const deviceId = webcamDeviceId || selectedCameraId || "";
+ if (lastPreviewDeviceIdRef.current === deviceId) return;
+ window.electronAPI.showCameraPreview(deviceId);
+ lastPreviewDeviceIdRef.current = deviceId;
+}, [webcamEnabled, webcamDeviceId, selectedCameraId]);🤖 Prompt for AI Agents |
||
|
|
||
| useEffect(() => { | ||
| if (!import.meta.env.DEV) { | ||
| return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 160
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 170
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 1135
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 1883
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 2689
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 5193
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 3287
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 920
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 1851
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 583
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 4381
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 97
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 700
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 355
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 1892
🏁 Script executed:
Repository: siddharthvaddem/openscreen
Length of output: 542
IPC handlers should validate sender before toggling camera preview window.
These listeners currently accept calls from any renderer with access to
window.electronAPI. Since all windows share the same preload bridge, an attacker who compromises any window could spam open/close on the camera preview—kinda cursed to have that persist across the app without validation.The hardening is straightforward: check
event.senderFrame?.urlto gate these to the HUD overlay window only, same pattern already established elsewhere.Hardening patch (recommended)
import { app, BrowserWindow, dialog, + IpcMainEvent, ipcMain, @@ +function isHudOverlaySender(event: IpcMainEvent): boolean { + const url = event.senderFrame?.url ?? ""; + return url.includes("windowType=hud-overlay"); +} + // Camera preview window — shown during recording so user can see their face - ipcMain.on("show-camera-preview", (_, deviceId: string) => { + ipcMain.on("show-camera-preview", (event, deviceId: string) => { + if (!isHudOverlaySender(event)) return; createCameraPreviewWindow(deviceId ?? ""); }); - ipcMain.on("hide-camera-preview", () => { + ipcMain.on("hide-camera-preview", (event) => { + if (!isHudOverlaySender(event)) return; closeCameraPreviewWindow(); });📝 Committable suggestion
🤖 Prompt for AI Agents