Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import {
} from "electron";
import { mainT, setMainLocale } from "./i18n";
import { registerIpcHandlers } from "./ipc/handlers";
import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow } from "./windows";
import {
closeCameraPreviewWindow,
createCameraPreviewWindow,
createEditorWindow,
createHudOverlayWindow,
createSourceSelectorWindow,
} from "./windows";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -361,6 +367,14 @@ app.whenReady().then(async () => {
ipcMain.on("hud-overlay-close", () => {
app.quit();
});

// Camera preview window — shown during recording so user can see their face
ipcMain.on("show-camera-preview", (_, deviceId: string) => {
createCameraPreviewWindow(deviceId ?? "");
});
ipcMain.on("hide-camera-preview", () => {
closeCameraPreviewWindow();
});
Comment on lines +371 to +377
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the electron/main.ts file around the specified lines
cd /
find . -name "electron" -type d | head -5

Repository: siddharthvaddem/openscreen

Length of output: 160


🏁 Script executed:

# List files in electron directory
fd . electron --type f --max-depth 1

Repository: siddharthvaddem/openscreen

Length of output: 170


🏁 Script executed:

# Read the main.ts file around lines 371-377
cat -n electron/main.ts | sed -n '360,390p'

Repository: siddharthvaddem/openscreen

Length of output: 1135


🏁 Script executed:

# Check the full context of IPC handler registration
rg "show-camera-preview|hide-camera-preview" -B 5 -A 10

Repository: siddharthvaddem/openscreen

Length of output: 1883


🏁 Script executed:

# Search for createCameraPreviewWindow and closeCameraPreviewWindow implementation
rg "createCameraPreviewWindow|closeCameraPreviewWindow" -B 3 -A 8

Repository: siddharthvaddem/openscreen

Length of output: 2689


🏁 Script executed:

# Check the createCameraPreviewWindow implementation details
cat -n electron/windows.ts | sed -n '1,150p'

Repository: siddharthvaddem/openscreen

Length of output: 5193


🏁 Script executed:

# Look at createHudOverlayWindow and related window creation
rg "createHudOverlayWindow|createKalturaBrowseWindow" -B 2 -A 20

Repository: siddharthvaddem/openscreen

Length of output: 3287


🏁 Script executed:

# Check preload configuration for each window type
rg "preload:" electron/windows.ts -B 5 -A 2

Repository: siddharthvaddem/openscreen

Length of output: 920


🏁 Script executed:

# Check if there are any other references to showCameraPreview or hideCameraPreview
rg "showCameraPreview|hideCameraPreview" -B 3 -A 3

Repository: siddharthvaddem/openscreen

Length of output: 1851


🏁 Script executed:

# Check which windows load preload and what their purposes are
rg "loadURL|loadFile" electron/windows.ts | head -20

Repository: siddharthvaddem/openscreen

Length of output: 583


🏁 Script executed:

# Verify preload.mjs exports and what electronAPI contains
cat -n electron/preload.ts | head -100

Repository: siddharthvaddem/openscreen

Length of output: 4381


🏁 Script executed:

# Check for createKalturaBrowseWindow to verify if it has preload attached
rg "createKalturaBrowseWindow" -B 5 -A 30

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

# Also check the KalturaBrowseDialog mentioned in learnings
rg "KalturaBrowseDialog" --type tsx -B 3 -A 15

Repository: siddharthvaddem/openscreen

Length of output: 97


🏁 Script executed:

# Search for Kaltura-related files
fd Kaltura . --type f

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

# Try searching for KalturaBrowseDialog without type specification
rg "KalturaBrowseDialog" -B 3 -A 15

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

# Check if there are any window creation functions that load remote URLs
rg "loadURL.*https" electron/

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

# List all TypeScript files to understand the structure
fd . src --type f --extension ts --extension tsx | head -20

Repository: siddharthvaddem/openscreen

Length of output: 700


🏁 Script executed:

# Check what windows can call these IPC handlers - verify all windows have preload
rg "windowType=" electron/windows.ts

Repository: siddharthvaddem/openscreen

Length of output: 355


🏁 Script executed:

# Check how LaunchWindow is loaded and in which window
rg "LaunchWindow" src/ -B 3 -A 3

Repository: siddharthvaddem/openscreen

Length of output: 1892


🏁 Script executed:

# Look at all IPC listeners registered and check which ones lack sender validation
rg "ipcMain.on\(|ipcMain.handle\(" electron/main.ts -A 2 | head -50

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?.url to 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Camera preview window — shown during recording so user can see their face
ipcMain.on("show-camera-preview", (_, deviceId: string) => {
createCameraPreviewWindow(deviceId ?? "");
});
ipcMain.on("hide-camera-preview", () => {
closeCameraPreviewWindow();
});
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", (event, deviceId: string) => {
if (!isHudOverlaySender(event)) return;
createCameraPreviewWindow(deviceId ?? "");
});
ipcMain.on("hide-camera-preview", (event) => {
if (!isHudOverlaySender(event)) return;
closeCameraPreviewWindow();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.ts` around lines 371 - 377, The IPC handlers for
"show-camera-preview" and "hide-camera-preview" must validate the sender before
toggling the camera preview: in the ipcMain.on callbacks for
"show-camera-preview" and "hide-camera-preview" check event.senderFrame?.url
(the same HUD overlay check used elsewhere) and only call
createCameraPreviewWindow(deviceId ?? "") or closeCameraPreviewWindow() when the
URL matches the HUD overlay window; otherwise return early and do not perform
any action. Ensure you reference the existing handlers for
"show-camera-preview"/"hide-camera-preview" and reuse the same URL matching
logic used for other HUD-gated IPCs to keep consistent validation.

ipcMain.handle("set-locale", (_, locale: string) => {
setMainLocale(locale);
setupApplicationMenu();
Expand Down
6 changes: 6 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
setLocale: (locale: string) => {
return ipcRenderer.invoke("set-locale", locale);
},
showCameraPreview: (deviceId: string) => {
ipcRenderer.send("show-camera-preview", deviceId);
},
hideCameraPreview: () => {
ipcRenderer.send("hide-camera-preview");
},
setMicrophoneExpanded: (expanded: boolean) => {
ipcRenderer.send("hud:setMicrophoneExpanded", expanded);
},
Expand Down
72 changes: 72 additions & 0 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Window position math can place preview off-screen on small displays.

Line 101 uses a fixed -170 offset with no clamp, so the preview can become partially inaccessible on low-height work areas.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/windows.ts` around lines 100 - 101, The current y computation (using
workArea, windowSize, margin and a hardcoded -170) can push the preview
off-screen on short displays; change the logic around the y variable so you
compute the desired Y (workArea.y + workArea.height - windowSize - margin - 170)
and then clamp it within safe bounds (e.g., at least workArea.y + margin and at
most workArea.y + workArea.height - windowSize - margin) so the preview never
extends above or below the visible workArea; update the y assignment in
electron/windows.ts (the y variable that uses workArea, windowSize, margin and
the -170 offset) to use this clamped value.


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.
Expand Down
5 changes: 4 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { CameraPreviewWindow } from "./components/launch/CameraPreviewWindow";
import { LaunchWindow } from "./components/launch/LaunchWindow";
import { SourceSelector } from "./components/launch/SourceSelector";
import { Toaster } from "./components/ui/sonner";
Expand All @@ -15,7 +16,7 @@ export default function App() {
const params = new URLSearchParams(window.location.search);
const type = params.get("windowType") || "";
setWindowType(type);
if (type === "hud-overlay" || type === "source-selector") {
if (type === "hud-overlay" || type === "source-selector" || type === "camera-preview") {
document.body.style.background = "transparent";
document.documentElement.style.background = "transparent";
document.getElementById("root")?.style.setProperty("background", "transparent");
Expand All @@ -29,6 +30,8 @@ export default function App() {

const content = (() => {
switch (windowType) {
case "camera-preview":
return <CameraPreviewWindow />;
case "hud-overlay":
return <LaunchWindow />;
case "source-selector":
Expand Down
166 changes: 166 additions & 0 deletions src/components/launch/CameraPreviewWindow.tsx
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/launch/CameraPreviewWindow.tsx | head -60

Repository: siddharthvaddem/openscreen

Length of output: 2097


getUserMedia cleanup has an async unmount race — camera won't release.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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());
};
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());
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/launch/CameraPreviewWindow.tsx` around lines 27 - 39, The
getUserMedia promise can resolve after the component's cleanup and create tracks
that are never stopped; add a disposed guard: declare a local let disposed =
false in the effect, set disposed = true inside the cleanup, and in the
getUserMedia .then handler check if disposed before assigning stream or
videoRef.current.srcObject — if disposed is true immediately stop any obtained
tracks instead of attaching them; keep the existing cleanup that stops tracks
from the outer-scoped stream variable to cover the normal path.

}, []);

if (collapsed) {
return (
<div
style={{
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
Comment on lines +44 to +52
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "CameraPreviewWindow.tsx" --type f

Repository: siddharthvaddem/openscreen

Length of output: 115


🏁 Script executed:

cat -n src/components/launch/CameraPreviewWindow.tsx | head -120

Repository: siddharthvaddem/openscreen

Length of output: 4139


Collapsed state loses drag behavior — add WebkitAppRegion: "drag" to outer container.

Expanded mode sets WebkitAppRegion: "drag" on the outer div (line 96), but collapsed mode's outer container (lines 44-52) is missing this property. The button correctly has WebkitAppRegion: "no-drag" (line 72) to stay clickable, but the surrounding area won't be draggable after collapse. Users won't be able to reposition the window until expanding it again — kinda cursed UX. Add WebkitAppRegion: "drag" to the collapsed outer div to match the expanded state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/launch/CameraPreviewWindow.tsx` around lines 44 - 52,
Collapsed outer container in CameraPreviewWindow lacks the CSS WebkitAppRegion:
"drag", so the window becomes non-draggable when collapsed; update the collapsed
outer div (the top-level <div> rendered in collapsed mode inside the
CameraPreviewWindow component) to include WebkitAppRegion: "drag" in its style
object, keeping the existing button style that uses WebkitAppRegion: "no-drag"
intact so the button remains clickable and behavior matches the expanded state.

>
{/* 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>
);
}
13 changes: 13 additions & 0 deletions src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "LaunchWindow.tsx" --type f

Repository: siddharthvaddem/openscreen

Length of output: 108


🏁 Script executed:

if [ -f "src/components/launch/LaunchWindow.tsx" ]; then
  wc -l "src/components/launch/LaunchWindow.tsx"
fi

Repository: 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"
fi

Repository: 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 webcamDeviceId and selectedCameraId but only have [webcamEnabled] in the dependency array. so if someone picks a different camera while the preview is live, they won't see it update until they toggle the webcam off/on. lowkey breaks the device-selection flow.

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
Verify each finding against the current code and only fix it if needed.

In `@src/components/launch/LaunchWindow.tsx` around lines 157 - 166, The effect
that calls window.electronAPI.showCameraPreview/hideCameraPreview only lists
webcamEnabled in its dependency array, so changes to webcamDeviceId or
selectedCameraId won't trigger an update; update the useEffect dependencies to
include webcamDeviceId and selectedCameraId (or the single chosen deviceId
variable) so that when webcamEnabled is true and the selected device changes the
effect re-runs and calls showCameraPreview with the new deviceId; keep the early
return on missing window.electronAPI and preserve calling hideCameraPreview when
webcamEnabled is false.


useEffect(() => {
if (!import.meta.env.DEV) {
return;
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useScreenRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
setElapsedSeconds(0);
window.electronAPI?.setRecordingState(true);


const activeScreenRecorder = screenRecorder.current;
const activeWebcamRecorder = webcamRecorder.current;
const activeRecordingId = recordingId.current;
Expand Down
2 changes: 2 additions & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ interface Window {
onMenuLoadProject: (callback: () => void) => () => void;
onMenuSaveProject: (callback: () => void) => () => void;
onMenuSaveProjectAs: (callback: () => void) => () => void;
showCameraPreview: (deviceId: string) => void;
hideCameraPreview: () => void;
setMicrophoneExpanded: (expanded: boolean) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
Expand Down