fixed webcam disappearing

This commit is contained in:
webadderall
2026-03-29 10:31:50 +11:00
parent 94011f2772
commit ba5fac0f06
4 changed files with 160 additions and 15 deletions
+6
View File
@@ -85,6 +85,12 @@ interface Window {
message?: string;
error?: string;
}>;
recoverNativeScreenRecording: () => Promise<{
success: boolean;
path?: string;
message?: string;
error?: string;
}>;
getLastNativeCaptureDiagnostics: () => Promise<{
success: boolean;
diagnostics?: NativeCaptureDiagnostics | null;
+92
View File
@@ -2621,6 +2621,34 @@ async function finalizeStoredVideo(videoPath: string) {
}
}
async function recoverNativeMacCaptureOutput() {
const diagnosticsPath =
lastNativeCaptureDiagnostics?.backend === 'mac-screencapturekit'
? lastNativeCaptureDiagnostics.outputPath ?? null
: null
const candidatePath = nativeCaptureTargetPath ?? diagnosticsPath
if (!candidatePath) {
return null
}
try {
return await finalizeStoredVideo(candidatePath)
} catch (error) {
recordNativeCaptureDiagnostics({
backend: 'mac-screencapturekit',
phase: 'stop',
outputPath: candidatePath,
systemAudioPath: nativeCaptureSystemAudioPath,
microphonePath: nativeCaptureMicrophonePath,
processOutput: nativeCaptureOutputBuffer.trim() || undefined,
fileSizeBytes: await getFileSizeIfPresent(candidatePath),
error: String(error),
})
return null
}
}
async function startInteractionCapture() {
if (!isCursorCaptureActive) {
return
@@ -3370,9 +3398,34 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
await waitForNativeCaptureStart(nativeCaptureProcess)
nativeScreenRecordingActive = true
recordNativeCaptureDiagnostics({
backend: 'mac-screencapturekit',
phase: 'start',
sourceId: source?.id ?? null,
sourceType: source?.sourceType ?? 'unknown',
displayId: typeof config.displayId === 'number' ? config.displayId : null,
helperPath,
outputPath,
systemAudioPath: systemAudioOutputPath,
microphonePath: microphoneOutputPath,
processOutput: nativeCaptureOutputBuffer.trim() || undefined,
})
return { success: true }
} catch (error) {
console.error('Failed to start native ScreenCaptureKit recording:', error)
recordNativeCaptureDiagnostics({
backend: 'mac-screencapturekit',
phase: 'start',
sourceId: source?.id ?? null,
sourceType: source?.sourceType ?? 'unknown',
helperPath: getNativeCaptureHelperBinaryPath(),
outputPath: nativeCaptureTargetPath,
systemAudioPath: nativeCaptureSystemAudioPath,
microphonePath: nativeCaptureMicrophonePath,
processOutput: nativeCaptureOutputBuffer.trim() || undefined,
fileSizeBytes: await getFileSizeIfPresent(nativeCaptureTargetPath),
error: String(error),
})
try {
nativeCaptureProcess?.kill()
} catch {
@@ -3485,6 +3538,11 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
}
if (!nativeScreenRecordingActive) {
const recovered = await recoverNativeMacCaptureOutput()
if (recovered) {
return recovered
}
return { success: false, message: 'No native screen recording is active.' }
}
@@ -3525,6 +3583,8 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
} catch (error) {
console.error('Failed to stop native ScreenCaptureKit recording:', error)
const fallbackPath = nativeCaptureTargetPath
const fallbackSystemAudioPath = nativeCaptureSystemAudioPath
const fallbackMicrophonePath = nativeCaptureMicrophonePath
nativeScreenRecordingActive = false
nativeCaptureProcess = null
nativeCaptureTargetPath = null
@@ -3538,12 +3598,28 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
try {
await fs.access(fallbackPath)
console.log('[stop-native-screen-recording] Recovering with fallback path:', fallbackPath)
if (fallbackSystemAudioPath || fallbackMicrophonePath) {
try {
await muxNativeMacRecordingWithAudio(
fallbackPath,
fallbackSystemAudioPath,
fallbackMicrophonePath,
)
} catch (muxError) {
console.warn('Failed to mux recovered native macOS audio into capture:', muxError)
}
}
return await finalizeStoredVideo(fallbackPath)
} catch {
// File doesn't exist or isn't accessible
}
}
const recovered = await recoverNativeMacCaptureOutput()
if (recovered) {
return recovered
}
return {
success: false,
message: 'Failed to stop native ScreenCaptureKit recording',
@@ -3552,6 +3628,22 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
}
})
ipcMain.handle('recover-native-screen-recording', async () => {
if (process.platform !== 'darwin') {
return { success: false, message: 'Native screen recording recovery is only available on macOS.' }
}
const recovered = await recoverNativeMacCaptureOutput()
if (recovered) {
return recovered
}
return {
success: false,
message: 'No recoverable native macOS recording output was found.',
}
})
ipcMain.handle('pause-native-screen-recording', async () => {
if (process.platform === 'win32') {
if (!windowsNativeCaptureActive || !windowsCaptureProcess) {
+3
View File
@@ -68,6 +68,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
stopNativeScreenRecording: () => {
return ipcRenderer.invoke("stop-native-screen-recording");
},
recoverNativeScreenRecording: () => {
return ipcRenderer.invoke("recover-native-screen-recording");
},
getLastNativeCaptureDiagnostics: () => {
return ipcRenderer.invoke("get-last-native-capture-diagnostics");
},
+59 -15
View File
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useCallback } from "react";
import { fixWebmDuration } from "@fix-webm-duration/fix";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { getEffectiveRecordingDurationMs } from "@/lib/mediaTiming";
@@ -91,6 +91,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const pendingWebcamPathPromise = useRef<Promise<string | null> | null>(null);
const webcamStopPromise = useRef<Promise<string | null> | null>(null);
const webcamStopResolver = useRef<((path: string | null) => void) | null>(null);
const resolvedWebcamPath = useRef<string | null>(null);
const accumulatedPausedDurationMs = useRef(0);
const pauseStartedAtMs = useRef<number | null>(null);
@@ -312,12 +313,29 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
const result = pending ? await pending : null;
resolvedWebcamPath.current = result;
pendingWebcamPathPromise.current = null;
return result;
}, []);
const recoverNativeRecordingSession = useCallback(async () => {
if (typeof window.electronAPI?.recoverNativeScreenRecording !== "function") {
return null;
}
const result = await window.electronAPI.recoverNativeScreenRecording();
if (!result.success || !result.path) {
return null;
}
const webcamPath = await stopWebcamRecorder();
await finalizeRecordingSession(result.path, webcamPath);
return result.path;
}, [finalizeRecordingSession, stopWebcamRecorder]);
const startWebcamRecorder = useCallback(async () => {
if (!webcamEnabled) {
resolvedWebcamPath.current = null;
pendingWebcamPathPromise.current = Promise.resolve(null);
webcamStartTime.current = null;
webcamTimeOffsetMs.current = 0;
@@ -343,6 +361,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const mimeType = selectMimeType();
webcamChunks.current = [];
resolvedWebcamPath.current = null;
webcamStopPromise.current = new Promise((resolve) => {
webcamStopResolver.current = resolve;
});
@@ -401,6 +420,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
recorder.start(RECORDER_TIMESLICE_MS);
} catch (error) {
console.warn("Failed to start webcam recording; continuing without webcam layer:", error);
resolvedWebcamPath.current = null;
pendingWebcamPathPromise.current = Promise.resolve(null);
webcamStopPromise.current = Promise.resolve(null);
webcamRecorder.current = null;
@@ -430,6 +450,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
if (!result.success || !result.path) {
console.error("Failed to stop native screen recording:", result.error ?? result.message);
void logNativeCaptureDiagnostics("stop-native-screen-recording");
try {
const recoveredPath = await recoverNativeRecordingSession();
if (recoveredPath) {
return;
}
} catch (recoveryError) {
console.error("Failed to recover native screen recording:", recoveryError);
}
return;
}
@@ -501,19 +529,32 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
});
const removeRecordingInterruptedListener = window.electronAPI?.onRecordingInterrupted?.((state) => {
setRecording(false);
nativeScreenRecording.current = false;
cleanupCapturedMedia();
void window.electronAPI.setRecordingState(false);
void (async () => {
setRecording(false);
nativeScreenRecording.current = false;
cleanupCapturedMedia();
await window.electronAPI.setRecordingState(false);
if (state.reason === "window-unavailable" && !hasPromptedForReselect.current) {
hasPromptedForReselect.current = true;
alert(state.message);
void window.electronAPI.openSourceSelector();
} else {
console.error(state.message);
toast.error(state.message);
}
if (state.reason !== "window-unavailable") {
try {
const recoveredPath = await recoverNativeRecordingSession();
if (recoveredPath) {
return;
}
} catch (recoveryError) {
console.error("Failed to recover interrupted native screen recording:", recoveryError);
}
}
if (state.reason === "window-unavailable" && !hasPromptedForReselect.current) {
hasPromptedForReselect.current = true;
alert(state.message);
await window.electronAPI.openSourceSelector();
} else {
console.error(state.message);
toast.error(state.message);
}
})();
});
return () => {
@@ -532,7 +573,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
cleanupCapturedMedia();
};
}, [cleanupCapturedMedia]);
}, [cleanupCapturedMedia, recoverNativeRecordingSession]);
const startRecording = async () => {
if (startInFlight.current) {
@@ -853,7 +894,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
if (videoResult.path) {
const webcamPath = await (pendingWebcamPathPromise.current ?? Promise.resolve(null));
const webcamPath = pendingWebcamPathPromise.current
? await pendingWebcamPathPromise.current
: resolvedWebcamPath.current;
await finalizeRecordingSession(videoResult.path, webcamPath);
}
} catch (error) {
@@ -955,6 +998,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamStream.current?.getTracks().forEach((t) => t.stop());
webcamStream.current = null;
pendingWebcamPathPromise.current = null;
resolvedWebcamPath.current = null;
if (nativeScreenRecording.current) {
nativeScreenRecording.current = false;