Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ interface Window {
recordingId: number;
webcam: import("../src/lib/recordingSession").RecordedVideoAssetInput;
cursorCaptureMode?: import("../src/lib/recordingSession").CursorCaptureMode;
durationMs?: number;
webcamOffsetMs?: number;
}) => Promise<{
success: boolean;
path?: string;
Expand Down Expand Up @@ -233,6 +235,7 @@ interface Window {
recordingId: number;
webcam: import("../src/lib/recordingSession").RecordedVideoAssetInput;
cursorCaptureMode?: import("../src/lib/recordingSession").CursorCaptureMode;
durationMs?: number;
webcamOffsetMs?: number;
}) => Promise<{
success: boolean;
Expand Down
46 changes: 39 additions & 7 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,12 @@ type AttachNativeMacWebcamRecordingInput = {
recordingId?: number;
webcam?: RecordedVideoAssetInput;
cursorCaptureMode?: CursorCaptureMode;
/**
* Webcam clip duration (ms), head start included. A streamed webcam file carries
* no Duration header and the renderer no longer holds the blob to patch, so the
* main process repairs the container on disk with this value.
*/
durationMs?: number;
/** See {@link ProjectMedia.webcamOffsetMs}. */
webcamOffsetMs?: number;
};
Expand Down Expand Up @@ -2788,6 +2794,13 @@ export function registerIpcHandlers(
}
});

// On-disk write streams for in-progress recordings, keyed by output file name.
// Chunks append as they arrive so the renderer never buffers the full video (#616).
// Declared here because both the webcam attach below and store-recorded-session
// finalize through the same registry.
const recordingStreams = new RecordingStreamRegistry();
registerRecordingStreamHandlers(ipcMain, recordingStreams, resolveRecordingOutputPath);

/**
* Writes a browser-recorded webcam clip next to a natively-recorded screen
* video and rewrites the session manifest to include both.
Expand Down Expand Up @@ -2817,15 +2830,39 @@ export function registerIpcHandlers(

await fs.access(screenVideoPath, fsConstants.R_OK);

if (!payload.webcam?.fileName || !payload.webcam.videoData) {
if (!payload.webcam?.fileName) {
return {
success: false,
error: `Native ${platformLabel} webcam attachment is missing video data.`,
};
}

const webcamVideoPath = resolveRecordingOutputPath(payload.webcam.fileName);
await fs.writeFile(webcamVideoPath, Buffer.from(payload.webcam.videoData));
// A streamed webcam arrives with an empty buffer: its bytes are already on
// disk, so close the stream and keep the file rather than writing it here.
// Nothing multi-gigabyte crosses IPC or gets flattened into one Buffer (#253).
const webcamStreamed = await finalizeRecordingFile(
recordingStreams,
payload.webcam.fileName,
webcamVideoPath,
payload.webcam.videoData,
);
// Mirrors finalizeRecordingFile's own condition, so this fires exactly when
// it wrote nothing and the session would point at a file that isn't there.
if (
!webcamStreamed &&
!(payload.webcam.videoData && payload.webcam.videoData.byteLength > 0)
) {
return {
success: false,
error: `Native ${platformLabel} webcam attachment is missing video data.`,
};
}
// Streamed files lack the WebM Duration header, which the editor needs to
// scale its timeline. Best-effort: a failed repair leaves the clip intact.
if (webcamStreamed && isValidDurationMs(payload.durationMs)) {
await repairRecordingContainer(webcamVideoPath, payload.durationMs);
}

const createdAt =
typeof payload.recordingId === "number" && Number.isFinite(payload.recordingId)
Expand Down Expand Up @@ -2892,11 +2929,6 @@ export function registerIpcHandlers(
},
);

// On-disk write streams for in-progress recordings, keyed by output file name.
// Chunks append as they arrive so the renderer never buffers the full video (#616).
const recordingStreams = new RecordingStreamRegistry();
registerRecordingStreamHandlers(ipcMain, recordingStreams, resolveRecordingOutputPath);

ipcMain.handle("store-recorded-session", async (_, payload: StoreRecordedSessionInput) => {
try {
return await storeRecordedSessionFiles(payload);
Expand Down
2 changes: 2 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
recordingId: number;
webcam: { fileName: string; videoData: ArrayBuffer };
cursorCaptureMode?: import("../src/lib/recordingSession").CursorCaptureMode;
durationMs?: number;
webcamOffsetMs?: number;
}) => {
return ipcRenderer.invoke("attach-native-linux-webcam-recording", payload);
Expand Down Expand Up @@ -242,6 +243,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
recordingId: number;
webcam: { fileName: string; videoData: ArrayBuffer };
cursorCaptureMode?: import("../src/lib/recordingSession").CursorCaptureMode;
durationMs?: number;
webcamOffsetMs?: number;
}) => {
return ipcRenderer.invoke("attach-native-mac-webcam-recording", payload);
Expand Down
Loading
Loading