feat: compose mac native capture with media

This commit is contained in:
Etienne
2026-05-12 09:32:14 +02:00
committed by Etienne Lescot
parent b9e2134749
commit 6a4ddc5dad
7 changed files with 453 additions and 81 deletions
+125 -49
View File
@@ -11,7 +11,7 @@ import {
type NativeWindowsRecordingRequest,
parseWindowHandleFromSourceId,
} from "@/lib/nativeWindowsRecording";
import type { CursorCaptureMode } from "@/lib/recordingSession";
import type { CursorCaptureMode, RecordedVideoAssetInput } from "@/lib/recordingSession";
import { requestCameraAccess } from "@/lib/requestCameraAccess";
const TARGET_FRAME_RATE = 60;
@@ -466,56 +466,105 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
}, []);
const finalizeNativeMacRecording = useCallback(async (discard = false) => {
const activeNativeRecording = nativeMacRecording.current;
if (!activeNativeRecording || activeNativeRecording.finalizing) {
return false;
}
activeNativeRecording.finalizing = true;
const clearNativeRecordingState = () => {
nativeMacRecording.current = null;
setRecording(false);
setPaused(false);
setElapsedSeconds(0);
accumulatedDurationMs.current = 0;
segmentStartedAt.current = null;
};
try {
const result = await window.electronAPI.stopNativeMacRecording(discard);
if (discard || result.discarded) {
clearNativeRecordingState();
return true;
const finalizeNativeMacRecording = useCallback(
async (discard = false) => {
const activeNativeRecording = nativeMacRecording.current;
if (!activeNativeRecording || activeNativeRecording.finalizing) {
return false;
}
if (!result.success) {
console.error("Failed to stop native macOS recording:", result.error);
toast.error(result.error ?? "Failed to stop native macOS recording");
activeNativeRecording.finalizing = true;
const duration = Math.max(0, getRecordingDurationMs());
const activeWebcamRecorder = webcamRecorder.current;
if (activeWebcamRecorder && webcamRecorder.current === activeWebcamRecorder) {
webcamRecorder.current = null;
}
const webcamAssetPromise = (async (): Promise<RecordedVideoAssetInput | undefined> => {
if (!activeWebcamRecorder) {
return undefined;
}
try {
if (activeWebcamRecorder.recorder.state !== "inactive") {
activeWebcamRecorder.recorder.stop();
}
const webcamBlob = await activeWebcamRecorder.recordedBlobPromise;
if (!webcamBlob || webcamBlob.size === 0) {
return undefined;
}
const fixedWebcamBlob = await fixWebmDuration(webcamBlob, duration);
return {
videoData: await fixedWebcamBlob.arrayBuffer(),
fileName: `${RECORDING_FILE_PREFIX}${activeNativeRecording.recordingId}${WEBCAM_FILE_SUFFIX}${VIDEO_FILE_EXTENSION}`,
};
} catch (error) {
console.error("Failed to finalize native macOS webcam recording:", error);
return undefined;
}
})();
const clearNativeRecordingState = () => {
nativeMacRecording.current = null;
setRecording(false);
setPaused(false);
setElapsedSeconds(0);
accumulatedDurationMs.current = 0;
segmentStartedAt.current = null;
};
try {
const result = await window.electronAPI.stopNativeMacRecording(discard);
const webcamAsset = await webcamAssetPromise;
if (discard || result.discarded) {
clearNativeRecordingState();
return true;
}
if (!result.success) {
console.error("Failed to stop native macOS recording:", result.error);
toast.error(result.error ?? "Failed to stop native macOS recording");
activeNativeRecording.finalizing = false;
return true;
}
if (webcamAsset && result.path) {
const attachResult = await window.electronAPI.attachNativeMacWebcamRecording({
screenVideoPath: result.path,
recordingId: activeNativeRecording.recordingId,
webcam: webcamAsset,
cursorCaptureMode,
});
if (attachResult.success) {
result.session = attachResult.session;
} else {
console.error("Failed to attach native macOS webcam recording:", attachResult.error);
toast.error(attachResult.error ?? "Failed to store webcam recording");
}
}
clearNativeRecordingState();
if (result.session) {
await window.electronAPI.setCurrentRecordingSession(result.session);
} else if (result.path) {
await window.electronAPI.setCurrentVideoPath(result.path);
}
await window.electronAPI.switchToEditor();
return true;
} catch (error) {
console.error("Error saving native macOS recording:", error);
toast.error(
error instanceof Error ? error.message : "Failed to save native macOS recording",
);
activeNativeRecording.finalizing = false;
return true;
} finally {
if (discardRecordingId.current === activeNativeRecording.recordingId) {
discardRecordingId.current = null;
}
}
clearNativeRecordingState();
if (result.session) {
await window.electronAPI.setCurrentRecordingSession(result.session);
} else if (result.path) {
await window.electronAPI.setCurrentVideoPath(result.path);
}
await window.electronAPI.switchToEditor();
return true;
} catch (error) {
console.error("Error saving native macOS recording:", error);
toast.error(error instanceof Error ? error.message : "Failed to save native macOS recording");
activeNativeRecording.finalizing = false;
return true;
} finally {
if (discardRecordingId.current === activeNativeRecording.recordingId) {
discardRecordingId.current = null;
}
}
}, []);
},
[cursorCaptureMode, getRecordingDurationMs],
);
const stopRecording = useRef(() => {
if (nativeWindowsRecording.current) {
@@ -717,7 +766,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
},
},
webcam: {
enabled: webcamEnabled,
enabled: false,
deviceId: webcamDeviceId,
deviceName: webcamDeviceName,
width: WEBCAM_TARGET_WIDTH,
@@ -783,8 +832,31 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const displayId =
Number(selectedSource.display_id) || parseMacDisplayIdFromSourceId(selectedSource.id);
const windowId = parseMacWindowIdFromSourceId(selectedSource.id);
let nativeWebcamRecorder: RecorderHandle | null = null;
if (webcamEnabled) {
stopWebcamPreviewStream();
if (!webcamReady.current) {
await new Promise<void>((resolve) => {
const interval = setInterval(() => {
if (webcamReady.current) {
clearInterval(interval);
resolve();
}
}, 50);
setTimeout(() => {
clearInterval(interval);
resolve();
}, 5000);
});
}
if (webcamStream.current) {
nativeWebcamRecorder = createRecorderHandle(webcamStream.current, {
mimeType: selectMimeType(),
videoBitsPerSecond: BITRATE_BASE,
});
} else {
webcamAcquireId.current++;
setWebcamEnabledState(false);
}
}
const request: NativeMacRecordingRequest = {
schemaVersion: 1,
@@ -830,6 +902,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
};
const result = await window.electronAPI.startNativeMacRecording(request);
if (!result.success || !result.recordingId) {
if (nativeWebcamRecorder && nativeWebcamRecorder.recorder.state !== "inactive") {
nativeWebcamRecorder.recorder.stop();
}
throw new Error(result.error ?? "Native macOS capture failed.");
}
@@ -838,6 +913,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
recordingId: result.recordingId,
finalizing: false,
};
webcamRecorder.current = nativeWebcamRecorder;
accumulatedDurationMs.current = 0;
segmentStartedAt.current = Date.now();
allowAutoFinalize.current = true;