From a8494177ab5399554d22d46864674fd4595f4619 Mon Sep 17 00:00:00 2001 From: reedaaz Date: Sat, 16 May 2026 15:07:17 +0200 Subject: [PATCH 1/3] @ feat(recording): persist webcam device + enabled across sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing per-user recording-preferences store (userData recordings-settings.json, never bundled) to also remember the selected webcam device id and webcam-enabled state, mirroring the proven mic persistence pattern: - get/set-recording-preferences handlers + types widened (webcamEnabled, webcamDeviceId, selectedSourceId/Name reserved for screen follow-up). - persistWebcamEnabled / persistWebcamDeviceId in useScreenRecorder, exposed as the public setters so all call sites persist automatically. - Hydrate webcam prefs on startup alongside mic. Screen-source persistence intentionally deferred (main-process, ephemeral desktopCapturer ids — separate follow-up). Co-Authored-By: Claude Opus 4.7 @ --- electron/electron-env.d.ts | 8 ++++++++ electron/ipc/register/settings.ts | 17 +++++++++++++++-- electron/preload.ts | 4 ++++ src/hooks/useScreenRecorder.ts | 20 ++++++++++++++++++-- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index f4c391e06..ad7ee11d0 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -826,6 +826,10 @@ interface Window { microphoneEnabled: boolean; microphoneDeviceId?: string; systemAudioEnabled: boolean; + webcamEnabled?: boolean; + webcamDeviceId?: string; + selectedSourceId?: string; + selectedSourceName?: string; }>; getRecordingAudioLabConfig: () => Promise<{ browserMicrophoneProfile: string; @@ -835,6 +839,10 @@ interface Window { microphoneEnabled?: boolean; microphoneDeviceId?: string; systemAudioEnabled?: boolean; + webcamEnabled?: boolean; + webcamDeviceId?: string; + selectedSourceId?: string; + selectedSourceName?: string; }) => Promise<{ success: boolean; error?: string }>; /** Countdown timer before recording */ getCountdownDelay: () => Promise<{ success: boolean; delay: number }>; diff --git a/electron/ipc/register/settings.ts b/electron/ipc/register/settings.ts index cecabb5f5..6e16c7041 100644 --- a/electron/ipc/register/settings.ts +++ b/electron/ipc/register/settings.ts @@ -147,9 +147,22 @@ export function registerSettingsHandlers() { microphoneEnabled: parsed.microphoneEnabled === true, microphoneDeviceId: typeof parsed.microphoneDeviceId === 'string' ? parsed.microphoneDeviceId : undefined, systemAudioEnabled: parsed.systemAudioEnabled === true, + webcamEnabled: parsed.webcamEnabled === true, + webcamDeviceId: typeof parsed.webcamDeviceId === 'string' ? parsed.webcamDeviceId : undefined, + selectedSourceId: typeof parsed.selectedSourceId === 'string' ? parsed.selectedSourceId : undefined, + selectedSourceName: typeof parsed.selectedSourceName === 'string' ? parsed.selectedSourceName : undefined, } } catch { - return { success: true, microphoneEnabled: false, microphoneDeviceId: undefined, systemAudioEnabled: false } + return { + success: true, + microphoneEnabled: false, + microphoneDeviceId: undefined, + systemAudioEnabled: false, + webcamEnabled: false, + webcamDeviceId: undefined, + selectedSourceId: undefined, + selectedSourceName: undefined, + } } }) @@ -157,7 +170,7 @@ export function registerSettingsHandlers() { return getBrowserMicrophoneProfileFromEnv() }) - ipcMain.handle('set-recording-preferences', async (_, prefs: { microphoneEnabled?: boolean; microphoneDeviceId?: string; systemAudioEnabled?: boolean }) => { + ipcMain.handle('set-recording-preferences', async (_, prefs: { microphoneEnabled?: boolean; microphoneDeviceId?: string; systemAudioEnabled?: boolean; webcamEnabled?: boolean; webcamDeviceId?: string; selectedSourceId?: string; selectedSourceName?: string }) => { try { let existing: Record = {} try { diff --git a/electron/preload.ts b/electron/preload.ts index 8ef19765e..f5486f500 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -907,6 +907,10 @@ contextBridge.exposeInMainWorld("electronAPI", { microphoneEnabled?: boolean; microphoneDeviceId?: string; systemAudioEnabled?: boolean; + webcamEnabled?: boolean; + webcamDeviceId?: string; + selectedSourceId?: string; + selectedSourceName?: string; }) => ipcRenderer.invoke("set-recording-preferences", prefs), getCountdownDelay: () => ipcRenderer.invoke("get-countdown-delay"), setCountdownDelay: (delay: number) => ipcRenderer.invoke("set-countdown-delay", delay), diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 6f021761a..83130f9b2 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -1198,6 +1198,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setMicrophoneDeviceId(result.microphoneDeviceId); } setSystemAudioEnabled(result.systemAudioEnabled); + if (result.webcamEnabled) { + setWebcamEnabled(true); + } + if (result.webcamDeviceId) { + setWebcamDeviceId(result.webcamDeviceId); + } } })(); }, []); @@ -1217,6 +1223,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn { void window.electronAPI.setRecordingPreferences({ systemAudioEnabled: enabled }); }, []); + const persistWebcamEnabled = useCallback((enabled: boolean) => { + setWebcamEnabled(enabled); + void window.electronAPI.setRecordingPreferences({ webcamEnabled: enabled }); + }, []); + + const persistWebcamDeviceId = useCallback((deviceId: string | undefined) => { + setWebcamDeviceId(deviceId); + void window.electronAPI.setRecordingPreferences({ webcamDeviceId: deviceId }); + }, []); + useEffect(() => { let cleanup: (() => void) | undefined; @@ -2009,9 +2025,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { systemAudioEnabled, setSystemAudioEnabled: persistSystemAudioEnabled, webcamEnabled, - setWebcamEnabled, + setWebcamEnabled: persistWebcamEnabled, webcamDeviceId, - setWebcamDeviceId, + setWebcamDeviceId: persistWebcamDeviceId, countdownDelay, setCountdownDelay, }; From 302f837b47db39a93fda17dddcb6fc7a096855c9 Mon Sep 17 00:00:00 2001 From: reedaaz Date: Mon, 18 May 2026 10:43:48 +0200 Subject: [PATCH 2/3] feat(launch): persist & restore screen/mic/webcam selection across sessions Save selected screen source (id, name, display_id, thumbnail, appIcon, type) to disk and restore it on startup. Fix mic device race where the init sync overwrote the saved deviceId with undefined before real devices enumerated. Co-Authored-By: Claude Opus 4.7 --- electron/electron-env.d.ts | 8 ++++++++ src/components/launch/LaunchWindow.tsx | 4 ++-- src/components/launch/hooks/useLaunchWindowActions.ts | 8 ++++++++ src/hooks/useScreenRecorder.ts | 10 ++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index ad7ee11d0..c9216b90b 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -830,6 +830,10 @@ interface Window { webcamDeviceId?: string; selectedSourceId?: string; selectedSourceName?: string; + selectedSourceDisplayId?: string; + selectedSourceThumbnail?: string | null; + selectedSourceAppIcon?: string | null; + selectedSourceType?: "screen" | "window"; }>; getRecordingAudioLabConfig: () => Promise<{ browserMicrophoneProfile: string; @@ -843,6 +847,10 @@ interface Window { webcamDeviceId?: string; selectedSourceId?: string; selectedSourceName?: string; + selectedSourceDisplayId?: string; + selectedSourceThumbnail?: string | null; + selectedSourceAppIcon?: string | null; + selectedSourceType?: "screen" | "window"; }) => Promise<{ success: boolean; error?: string }>; /** Countdown timer before recording */ getCountdownDelay: () => Promise<{ success: boolean; delay: number }>; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 49547069d..e67a81644 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -119,11 +119,11 @@ function LaunchWindowContent() { const supportsHudCaptureProtection = platform !== "linux"; useEffect(() => { - if (!selectedDeviceId) { + if (!selectedDeviceId || selectedDeviceId === "default") { return; } - setMicrophoneDeviceId(selectedDeviceId === "default" ? undefined : selectedDeviceId); + setMicrophoneDeviceId(selectedDeviceId); }, [selectedDeviceId, setMicrophoneDeviceId]); useEffect(() => { diff --git a/src/components/launch/hooks/useLaunchWindowActions.ts b/src/components/launch/hooks/useLaunchWindowActions.ts index 90acfe577..87c8b4e46 100644 --- a/src/components/launch/hooks/useLaunchWindowActions.ts +++ b/src/components/launch/hooks/useLaunchWindowActions.ts @@ -9,6 +9,14 @@ export function useLaunchWindowActions() { const handleSourceSelect = useCallback(async (source: DesktopSource) => { await window.electronAPI.selectSource(source); + void window.electronAPI.setRecordingPreferences({ + selectedSourceId: source.id, + selectedSourceName: source.name, + selectedSourceDisplayId: source.display_id, + selectedSourceThumbnail: source.thumbnail, + selectedSourceAppIcon: source.appIcon, + selectedSourceType: source.sourceType, + }); setSelectedSource(source.name); setHasSelectedSource(true); window.electronAPI.showSourceHighlight?.({ diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 83130f9b2..e232a69d1 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -1204,6 +1204,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (result.webcamDeviceId) { setWebcamDeviceId(result.webcamDeviceId); } + if (result.selectedSourceId && result.selectedSourceName) { + void window.electronAPI.selectSource({ + id: result.selectedSourceId, + name: result.selectedSourceName, + display_id: result.selectedSourceDisplayId || "", + thumbnail: result.selectedSourceThumbnail || null, + appIcon: result.selectedSourceAppIcon || null, + sourceType: result.selectedSourceType || "screen", + }); + } } })(); }, []); From b7f39783ec131696098696d55b02417eb5a08ab6 Mon Sep 17 00:00:00 2001 From: reedaaz Date: Mon, 18 May 2026 15:11:57 +0200 Subject: [PATCH 3/3] fix(launch): validate persisted screen source against live capture list Electron desktopCapturer ids are not stable across reboots / display changes. Restore the saved source only if it still exists in the current source list (re-enumerated live), otherwise the app would silently capture a non-existent target. Drops the redundant persisted display_id/thumbnail/appIcon/type fields since the live source object carries them. Co-Authored-By: Claude Opus 4.7 --- electron/electron-env.d.ts | 8 ------ .../launch/hooks/useLaunchWindowActions.ts | 4 --- src/hooks/useScreenRecorder.ts | 25 +++++++++++++------ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index c9216b90b..ad7ee11d0 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -830,10 +830,6 @@ interface Window { webcamDeviceId?: string; selectedSourceId?: string; selectedSourceName?: string; - selectedSourceDisplayId?: string; - selectedSourceThumbnail?: string | null; - selectedSourceAppIcon?: string | null; - selectedSourceType?: "screen" | "window"; }>; getRecordingAudioLabConfig: () => Promise<{ browserMicrophoneProfile: string; @@ -847,10 +843,6 @@ interface Window { webcamDeviceId?: string; selectedSourceId?: string; selectedSourceName?: string; - selectedSourceDisplayId?: string; - selectedSourceThumbnail?: string | null; - selectedSourceAppIcon?: string | null; - selectedSourceType?: "screen" | "window"; }) => Promise<{ success: boolean; error?: string }>; /** Countdown timer before recording */ getCountdownDelay: () => Promise<{ success: boolean; delay: number }>; diff --git a/src/components/launch/hooks/useLaunchWindowActions.ts b/src/components/launch/hooks/useLaunchWindowActions.ts index 87c8b4e46..cdfb8e57e 100644 --- a/src/components/launch/hooks/useLaunchWindowActions.ts +++ b/src/components/launch/hooks/useLaunchWindowActions.ts @@ -12,10 +12,6 @@ export function useLaunchWindowActions() { void window.electronAPI.setRecordingPreferences({ selectedSourceId: source.id, selectedSourceName: source.name, - selectedSourceDisplayId: source.display_id, - selectedSourceThumbnail: source.thumbnail, - selectedSourceAppIcon: source.appIcon, - selectedSourceType: source.sourceType, }); setSelectedSource(source.name); setHasSelectedSource(true); diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index e232a69d1..c718cef2a 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -1205,14 +1205,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setWebcamDeviceId(result.webcamDeviceId); } if (result.selectedSourceId && result.selectedSourceName) { - void window.electronAPI.selectSource({ - id: result.selectedSourceId, - name: result.selectedSourceName, - display_id: result.selectedSourceDisplayId || "", - thumbnail: result.selectedSourceThumbnail || null, - appIcon: result.selectedSourceAppIcon || null, - sourceType: result.selectedSourceType || "screen", - }); + // Electron desktopCapturer ids are not stable across + // reboots / display changes. Only restore the source if it + // still exists in the current capture list, otherwise the + // app would silently capture a non-existent target. + try { + const available = await window.electronAPI.getSources({ + types: ["screen", "window"], + }); + const match = available.find( + (source) => source.id === result.selectedSourceId, + ); + if (match) { + void window.electronAPI.selectSource(match); + } + } catch { + // Source enumeration failed — skip restore silently. + } } } })();