fix: warm start native capture before countdown

This commit is contained in:
webadderall
2026-08-23 19:31:37 +10:00
parent 72e9724505
commit 9be52f37f2
6 changed files with 191 additions and 100 deletions
Binary file not shown.
@@ -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",
Binary file not shown.
+3 -2
View File
@@ -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 (
<HudInteractionContext.Provider
@@ -464,7 +465,7 @@ function LaunchWindowContent() {
>
<motion.div
ref={hudBarRef}
layout={!showRecordingWebcamPreview && !isHudDragging}
layout={shouldAnimateHudLayout}
transition={hudStateTransition}
className={`${styles.bar} launch-theme mb-2`}
>
@@ -487,7 +488,7 @@ function LaunchWindowContent() {
<AnimatePresence initial={false} mode="wait">
<motion.div
key={hudMode}
layout={!showRecordingWebcamPreview && !isHudDragging}
layout={shouldAnimateHudLayout}
className={styles.barState}
initial={{
opacity: 0,
+179 -89
View File
@@ -213,10 +213,7 @@ export function resolveBrowserCaptureCursorPolicy({
export function shouldUseNativeWindowsCaptureForSource(
source: Pick<ProcessedDesktopSource, "id"> | 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();
};