diff --git a/electron/main.ts b/electron/main.ts index cd609292..913ca05d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -817,12 +817,15 @@ function createEditorWindowWrapper() { if (choice === 0) { editorWindow.webContents.send("request-save-before-close"); ipcMain.once("save-before-close-done", (_event, saved: boolean) => { - if (saved) { - if (process.platform === "win32" && !isAppQuitting) { - closeEditorWindowToHud(editorWindow); - } else { - closeEditorWindowBypassingUnsavedPrompt(editorWindow); - } + if (!saved) { + isAppQuitting = false; + return; + } + + if (process.platform === "win32" && !isAppQuitting) { + closeEditorWindowToHud(editorWindow); + } else { + closeEditorWindowBypassingUnsavedPrompt(editorWindow); } }); } else if (choice === 1) { diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 473551b3..c1853bad 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1745,6 +1745,8 @@ const VideoPlayback = forwardRef( // biome-ignore lint/correctness/useExhaustiveDependencies: The media path intentionally triggers source-specific state reset. useEffect(() => { + webcamSynchronizedPathRef.current = null; + setWebcamSynchronizedPath(null); setWebcamVideoDimensions(null); lastWebcamSyncTimeRef.current = null; }, [webcamVideoPath]); diff --git a/src/components/video-editor/WebcamCropControl.tsx b/src/components/video-editor/WebcamCropControl.tsx index 4468434f..122601f2 100644 --- a/src/components/video-editor/WebcamCropControl.tsx +++ b/src/components/video-editor/WebcamCropControl.tsx @@ -249,6 +249,13 @@ export function WebcamCropControl({ syncPreviewMedia(); }, [syncPreviewMedia]); + // biome-ignore lint/correctness/useExhaustiveDependencies: The source identity intentionally resets cached synchronization state. + useEffect(() => { + synchronizedPreviewSrcRef.current = null; + setSynchronizedPreviewSrc(null); + setPreviewFrame(null); + }, [previewSrc]); + const commitVisualCrop = (nextVisualCrop: CropRegion, immediate = false) => { const nextCrop = mirrored ? flipCropHorizontally(nextVisualCrop) diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 0a04b72f..6d17f0a4 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -1375,13 +1375,20 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); if (webcamPath) { - await window.electronAPI.setCurrentRecordingSession({ - videoPath: finalPath, - webcamPath, - timeOffsetMs: webcamTimeOffsetMs.current, - hideOverlayCursorByDefault: - hideEditorOverlayCursorByDefault.current, - }); + try { + await window.electronAPI.setCurrentRecordingSession({ + videoPath: finalPath, + webcamPath, + timeOffsetMs: webcamTimeOffsetMs.current, + hideOverlayCursorByDefault: + hideEditorOverlayCursorByDefault.current, + }); + } catch (sessionError) { + console.error( + "Failed to publish the asynchronously finalized webcam:", + sessionError, + ); + } } return webcamPath; @@ -1395,11 +1402,26 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const muxReadyPromise = isNativeWindows ? window.electronAPI.muxNativeWindowsRecording(expectedDurationMs) : Promise.resolve(null); - const [webcamPath] = await Promise.all([ - webcamReadyPromise, - microphoneReadyPromise, - muxReadyPromise, - ]); + const [webcamResult, microphoneResult, muxResult] = + await Promise.allSettled([ + webcamReadyPromise, + microphoneReadyPromise, + muxReadyPromise, + ]); + const webcamPath = + webcamResult.status === "fulfilled" ? webcamResult.value : null; + for (const [taskName, result] of [ + ["webcam", webcamResult], + ["microphone sidecar", microphoneResult], + ["Windows mux", muxResult], + ] as const) { + if (result.status === "rejected") { + console.error( + `[useScreenRecorder] ${taskName} finalization failed:`, + result.reason, + ); + } + } console.log( "[useScreenRecorder] Emitting setCurrentRecordingSession with:",