diff --git a/electron/native/bin/win32-x64/cursor-monitor.exe b/electron/native/bin/win32-x64/cursor-monitor.exe index ef71282d..70738d11 100644 Binary files a/electron/native/bin/win32-x64/cursor-monitor.exe and b/electron/native/bin/win32-x64/cursor-monitor.exe differ diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index c5080b66..c4755869 100644 --- a/electron/native/bin/win32-x64/helpers-manifest.json +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -5,24 +5,24 @@ "helpers": { "wgc-capture": { "binaryName": "wgc-capture.exe", - "binarySha256": "298b41f371c3881046061048b466e12ed70dd93fa761bade2fd57d1ccddf3cb9", + "binarySha256": "4d89fdff8e3343998c7a3b4d75d964c1f75f93594aa097c6681e477d25ec9f01", "sourceDir": "electron/native/wgc-capture", - "sourceFingerprint": "6ee457080c27dc939ff4b61965f86b6d73995e40200440a1dc44865f9708d39f", - "updatedAt": "2026-05-24T19:49:15.077Z" + "sourceFingerprint": "c6dac250c9d16f7aa881998353441b4ae3fb59731b802e3b8491a136e79726b0", + "updatedAt": "2026-07-11T11:58:45.856Z" }, "cursor-monitor": { "binaryName": "cursor-monitor.exe", - "binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12", + "binarySha256": "f1d8f30e8d7bee19ecea91c9a90a95ea4824138b8336b18030fa0602a641d70d", "sourceDir": "electron/native/cursor-monitor", - "sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e", - "updatedAt": "2026-05-07T15:22:18.173Z" + "sourceFingerprint": "45bb72e4039e061a354af87317d7c42ec3d2118369b3f4379046ff497b701e72", + "updatedAt": "2026-07-11T11:58:56.534Z" }, "recordly-gpu-export": { "binaryName": "recordly-gpu-export.exe", - "binarySha256": "4cb3a293fd36f718af55906820d9b3fd78babc855888c2e248f0b918ec1aff3c", + "binarySha256": "49a2ac588206305d0129e6263ce4be49780c50a9dc4efc7df63aad09178a919f", "sourceDir": "electron/native/gpu-export-probe", - "sourceFingerprint": "743b386a5f1bbcc99cec5465c3de228d2b045061dead31dfcbf25cf6a1e61de5", - "updatedAt": "2026-05-07T20:13:48.585Z" + "sourceFingerprint": "37a5842eba63cdeccfddd02cc278a98207248fb0aa6b56153fb85ed152c908c1", + "updatedAt": "2026-07-11T11:58:51.659Z" }, "recordly-nvidia-cuda-compositor": { "binaryName": "recordly-nvidia-cuda-compositor.exe", diff --git a/electron/native/bin/win32-x64/recordly-gpu-export.exe b/electron/native/bin/win32-x64/recordly-gpu-export.exe index 3d0f57f5..b9d1efc9 100644 Binary files a/electron/native/bin/win32-x64/recordly-gpu-export.exe and b/electron/native/bin/win32-x64/recordly-gpu-export.exe differ diff --git a/electron/native/bin/win32-x64/wgc-capture.exe b/electron/native/bin/win32-x64/wgc-capture.exe index 8180edd0..a882ee87 100644 Binary files a/electron/native/bin/win32-x64/wgc-capture.exe and b/electron/native/bin/win32-x64/wgc-capture.exe differ diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09cca63e..9bc34c85 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -438,6 +438,7 @@ function LaunchWindowContent() { const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle"; const useNativeHudBarDrag = platform === "linux" || hudOverlayMousePassthroughSupported === false; + const shouldAnimateHudLayout = !recording && !showRecordingWebcamPreview && !isHudDragging; return ( @@ -487,7 +488,7 @@ function LaunchWindowContent() { | null | undefined, ): boolean { - return ( - source?.id?.startsWith("screen:") === true || - source?.id?.startsWith("window:") === true - ); + return source?.id?.startsWith("screen:") === true || source?.id?.startsWith("window:") === true; } export function createProcessedMicrophoneConstraints( @@ -1066,6 +1063,97 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }, []); + const prepareRecordingStart = useCallback(async () => { + const platform = await window.electronAPI.getPlatform(); + hideEditorOverlayCursorByDefault.current = false; + const existingSource = await window.electronAPI.getSelectedSource(); + const selectedSource = + existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); + if (!selectedSource) { + alert("Please select a source to record"); + return null; + } + + if (!existingSource && selectedSource.id === "screen:linux-portal") { + try { + await window.electronAPI.selectSource(selectedSource); + } catch (err) { + console.warn("Failed to persist Linux portal sentinel source:", err); + } + } + + const permissionsReady = await preparePermissions(); + if (!permissionsReady) { + return null; + } + + recordingSessionTimestamp.current = Date.now(); + resetRecordingClock(recordingSessionTimestamp.current); + await prepareWebcamRecorder(); + + const useNativeMacScreenCapture = + platform === "darwin" && + (selectedSource.id?.startsWith("screen:") || + selectedSource.id?.startsWith("window:")) && + typeof window.electronAPI.startNativeScreenRecording === "function"; + + let useNativeWindowsCapture = false; + if ( + platform === "win32" && + shouldUseNativeWindowsCaptureForSource(selectedSource) && + typeof window.electronAPI.isNativeWindowsCaptureAvailable === "function" + ) { + try { + const nativeWindowsResult = + await window.electronAPI.isNativeWindowsCaptureAvailable(); + useNativeWindowsCapture = nativeWindowsResult.available; + if (!useNativeWindowsCapture && !hasShownNativeWindowsFallbackToast.current) { + void logNativeCaptureDiagnostics("is-native-windows-capture-available"); + hasShownNativeWindowsFallbackToast.current = true; + toast.info( + "Native Windows capture is unavailable. Falling back to browser capture.", + ); + } + } catch { + useNativeWindowsCapture = false; + if (!hasShownNativeWindowsFallbackToast.current) { + hasShownNativeWindowsFallbackToast.current = true; + toast.info( + "Unable to check native Windows capture. Falling back to browser capture.", + ); + } + } + } + + let micLabel: string | undefined; + if ((useNativeMacScreenCapture || useNativeWindowsCapture) && microphoneEnabled) { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const mic = devices.find( + (d) => d.deviceId === microphoneDeviceId && d.kind === "audioinput", + ); + micLabel = mic?.label || undefined; + } catch { + // Fall through - native process will use the default mic. + } + } + + return { + platform, + selectedSource, + useNativeMacScreenCapture, + useNativeWindowsCapture, + micLabel, + }; + }, [ + logNativeCaptureDiagnostics, + microphoneDeviceId, + microphoneEnabled, + preparePermissions, + prepareWebcamRecorder, + resetRecordingClock, + ]); + const stopRecording = useRef(() => { setPaused(false); if (nativeScreenRecording.current) { @@ -1375,84 +1463,33 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setStarting(true); try { - const platform = await window.electronAPI.getPlatform(); - hideEditorOverlayCursorByDefault.current = false; - const existingSource = await window.electronAPI.getSelectedSource(); - const selectedSource = - existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); - if (!selectedSource) { - alert("Please select a source to record"); + const preparedStart = await prepareRecordingStart(); + if (!preparedStart) { return; } - // Persist the synthetic Linux portal sentinel to main so that the - // setDisplayMediaRequestHandler can short-circuit getSources() and - // avoid triggering an extra portal dialog. - if (!existingSource && selectedSource.id === "screen:linux-portal") { + + const { selectedSource, useNativeMacScreenCapture, useNativeWindowsCapture, micLabel } = + preparedStart; + const useNativeCapture = + useNativeMacScreenCapture || useNativeWindowsCapture; + const shouldWarmStartNativeCapture = useNativeCapture && countdownDelay > 0; + if (countdownDelay > 0 && !shouldWarmStartNativeCapture) { + setCountdownActive(true); try { - await window.electronAPI.selectSource(selectedSource); - } catch (err) { - console.warn("Failed to persist Linux portal sentinel source:", err); + const result = await window.electronAPI.startCountdown(countdownDelay); + if (!result.success || result.cancelled) { + return; + } + } finally { + setCountdownActive(false); } + recordingSessionTimestamp.current = Date.now(); + resetRecordingClock(recordingSessionTimestamp.current); } - const permissionsReady = await preparePermissions(); - if (!permissionsReady) { - return; - } - - recordingSessionTimestamp.current = Date.now(); - resetRecordingClock(recordingSessionTimestamp.current); - await prepareWebcamRecorder(); - const useNativeMacScreenCapture = - platform === "darwin" && - (selectedSource.id?.startsWith("screen:") || - selectedSource.id?.startsWith("window:")) && - typeof window.electronAPI.startNativeScreenRecording === "function"; - - let useNativeWindowsCapture = false; let nativeWindowsCaptureStartFailed = false; - if ( - platform === "win32" && - shouldUseNativeWindowsCaptureForSource(selectedSource) && - typeof window.electronAPI.isNativeWindowsCaptureAvailable === "function" - ) { - try { - const nativeWindowsResult = - await window.electronAPI.isNativeWindowsCaptureAvailable(); - useNativeWindowsCapture = nativeWindowsResult.available; - if (!useNativeWindowsCapture && !hasShownNativeWindowsFallbackToast.current) { - void logNativeCaptureDiagnostics("is-native-windows-capture-available"); - hasShownNativeWindowsFallbackToast.current = true; - toast.info( - "Native Windows capture is unavailable. Falling back to browser capture.", - ); - } - } catch { - useNativeWindowsCapture = false; - if (!hasShownNativeWindowsFallbackToast.current) { - hasShownNativeWindowsFallbackToast.current = true; - toast.info( - "Unable to check native Windows capture. Falling back to browser capture.", - ); - } - } - } - - if (useNativeMacScreenCapture || useNativeWindowsCapture) { - // Resolve the selected mic label for native capture backends. - let micLabel: string | undefined; - if (microphoneEnabled) { - try { - const devices = await navigator.mediaDevices.enumerateDevices(); - const mic = devices.find( - (d) => d.deviceId === microphoneDeviceId && d.kind === "audioinput", - ); - micLabel = mic?.label || undefined; - } catch { - // Fall through — native process will use the default mic - } - } + if (useNativeCapture) { const nativeResult = await window.electronAPI.startNativeScreenRecording( selectedSource, { @@ -1491,11 +1528,53 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } if (nativeResult.success) { + nativeScreenRecording.current = true; + nativeWindowsRecording.current = useNativeWindowsCapture; + if (shouldWarmStartNativeCapture) { + const pauseResult = await window.electronAPI.pauseNativeScreenRecording(); + if (!pauseResult.success) { + throw new Error( + pauseResult.error ?? + pauseResult.message ?? + "Failed to pause native capture before countdown", + ); + } + + setCountdownActive(true); + try { + const countdownResult = + await window.electronAPI.startCountdown(countdownDelay); + if (!countdownResult.success || countdownResult.cancelled) { + const stoppedResult = + await window.electronAPI.stopNativeScreenRecording(); + if (stoppedResult.path) { + await window.electronAPI.deleteRecordingFile( + stoppedResult.path, + ); + } + nativeScreenRecording.current = false; + nativeWindowsRecording.current = false; + cleanupCapturedMedia(); + await stopWebcamRecorder(); + return; + } + } finally { + setCountdownActive(false); + } + + const resumeResult = await window.electronAPI.resumeNativeScreenRecording(); + if (!resumeResult.success) { + throw new Error( + resumeResult.error ?? + resumeResult.message ?? + "Failed to resume native capture after countdown", + ); + } + } + const mainStartedAt = Date.now(); micFallbackStartDelayMs.current = null; beginWebcamCapture(); - nativeScreenRecording.current = true; - nativeWindowsRecording.current = useNativeWindowsCapture; resetRecordingClock(mainStartedAt); webcamTimeOffsetMs.current = webcamStartTime.current === null @@ -1581,6 +1660,22 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } + if (nativeWindowsCaptureStartFailed && countdownDelay > 0) { + setCountdownActive(true); + try { + const result = await window.electronAPI.startCountdown(countdownDelay); + if (!result.success || result.cancelled) { + cleanupCapturedMedia(); + await stopWebcamRecorder(); + return; + } + } finally { + setCountdownActive(false); + } + recordingSessionTimestamp.current = Date.now(); + resetRecordingClock(recordingSessionTimestamp.current); + } + const browserCursorPolicy = resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed, }); @@ -1922,6 +2017,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); setRecording(false); try { + if (nativeScreenRecording.current) { + nativeScreenRecording.current = false; + nativeWindowsRecording.current = false; + const stoppedResult = await window.electronAPI.stopNativeScreenRecording(); + if (stoppedResult.path) { + await window.electronAPI.deleteRecordingFile(stoppedResult.path); + } + } await window.electronAPI?.setRecordingState(false); } catch (stateError) { console.warn("Failed to reset main-process recording state:", stateError); @@ -2085,19 +2188,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } - // Start recording with optional countdown - if (countdownDelay > 0) { - setCountdownActive(true); - try { - const result = await window.electronAPI.startCountdown(countdownDelay); - if (!result.success || result.cancelled) { - return; - } - } finally { - setCountdownActive(false); - } - } - startRecording(); };