Skip to content
Closed
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
8 changes: 8 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ interface Window {
message?: string;
error?: string;
}>;
openRecordingStream: (
recordingId: number,
fileName: string,
) => Promise<{ success: boolean; error?: string }>;
appendRecordingChunk: (
recordingId: number,
chunk: ArrayBuffer,
) => Promise<{ success: boolean; error?: string }>;
getRecordedVideoPath: () => Promise<{
success: boolean;
path?: string;
Expand Down
66 changes: 63 additions & 3 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { constants as fsConstants } from "node:fs";
import { createWriteStream, constants as fsConstants, type WriteStream } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
Expand Down Expand Up @@ -2066,6 +2066,47 @@ export function registerIpcHandlers(
},
);

// Streaming chunk writers — keyed by recordingId. Chunks are appended directly
// to disk as they arrive from ondataavailable so the renderer never holds the
// full video in memory.
const activeWriteStreams = new Map<number, WriteStream>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] WriteStream lifecycle leaks.

activeWriteStreams is only .deleted inside storeRecordedSessionFiles. Any abandon path — onerror, user discard, early return, crash before stop — leaves the stream open (leaked fd) with a half-written file on disk. Opening a second stream for the same id would also orphan the first.

Fix: wrap this in one owner (RecordingStreams with open / append / close / abort) that closes on every terminal path, and guard against double-open.


ipcMain.handle(
"open-recording-stream",
async (
_,
recordingId: number,
fileName: string,
): Promise<{ success: boolean; error?: string }> => {
try {
const filePath = resolveRecordingOutputPath(fileName);
const ws = createWriteStream(filePath, { flags: "w" });
activeWriteStreams.set(recordingId, ws);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
},
Comment on lines +2081 to +2089
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing directory creation before opening write stream.

createWriteStream will throw ENOENT if RECORDINGS_DIR doesn't exist. Native recording flows call fs.mkdir(RECORDINGS_DIR, { recursive: true }) before starting, but the browser-based recording flow doesn't — it relies on this handler being called first.

🐛 Proposed fix
 	ipcMain.handle(
 		"open-recording-stream",
 		async (
 			_,
 			recordingId: number,
 			fileName: string,
 		): Promise<{ success: boolean; error?: string }> => {
 			try {
+				await fs.mkdir(RECORDINGS_DIR, { recursive: true });
 				const filePath = resolveRecordingOutputPath(fileName);
 				const ws = createWriteStream(filePath, { flags: "w" });
 				activeWriteStreams.set(recordingId, ws);
 				return { success: true };
 			} catch (error) {
 				return { success: false, error: String(error) };
 			}
 		},
 	);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const filePath = resolveRecordingOutputPath(fileName);
const ws = createWriteStream(filePath, { flags: "w" });
activeWriteStreams.set(recordingId, ws);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
},
try {
await fs.mkdir(RECORDINGS_DIR, { recursive: true });
const filePath = resolveRecordingOutputPath(fileName);
const ws = createWriteStream(filePath, { flags: "w" });
activeWriteStreams.set(recordingId, ws);
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 2081 - 2089, The handler that opens a
write stream (using resolveRecordingOutputPath, createWriteStream and
activeWriteStreams with recordingId) can fail with ENOENT if the recordings
directory doesn't exist; before creating the write stream call fs.mkdir (or
fs.promises.mkdir) on the parent directory of
resolveRecordingOutputPath(fileName) with { recursive: true } (await if using
promises) inside the try block, then create the stream and set
activeWriteStreams; keep the existing try/catch and return shape but ensure the
directory creation is awaited and errors propagate into the existing error
return.

);

ipcMain.handle(
"append-recording-chunk",
async (
_,
recordingId: number,
chunk: ArrayBuffer,
): Promise<{ success: boolean; error?: string }> => {
const ws = activeWriteStreams.get(recordingId);
if (!ws) return { success: false, error: "No active stream for recordingId " + recordingId };
return new Promise((resolve) => {
ws.write(Buffer.from(chunk), (err) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disk-write failures are swallowed → silent truncation.

This returns { success: false } on a ws.write error, but the renderer voids the call and never reads it. Disk-full mid-recording silently drops every later chunk and the recording "succeeds" truncated — the same silent-loss mode this PR is removing, relocated.

Fix: propagate the first append failure back to the recorder (stop + user-visible error, or fall back to in-memory if still viable). Also consider backpressure — ws.write's return value is ignored here, so a high-bitrate recording can balloon the main process buffer; respect write() === false / drain.

if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
});
});
},
);

ipcMain.handle("store-recorded-session", async (_, payload: StoreRecordedSessionInput) => {
try {
return await storeRecordedSessionFiles(payload);
Expand All @@ -2086,12 +2127,31 @@ export function registerIpcHandlers(
: Date.now();
const cursorCaptureMode = normalizeCursorCaptureMode(payload.cursorCaptureMode);
const screenVideoPath = resolveRecordingOutputPath(payload.screen.fileName);
await fs.writeFile(screenVideoPath, Buffer.from(payload.screen.videoData));

// Close the streaming write stream if one was used; otherwise fall back to
// writing the full buffer (short recordings that never opened a stream).
const screenWs = activeWriteStreams.get(createdAt);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] Open/close keys can diverge → empty file + leaked stream.

Streams are opened keyed by recordingId but closed here by createdAt, which falls back to Date.now() above. If payload.createdAt is ever falsy, createdAt becomes a fresh timestamp, this get misses, and you fall through to fs.writeFile(path, Buffer.from(emptyArrayBuffer)) — an empty file, with the real stream never closed. The renderer passes them equal today, but that's an undocumented cross-call invariant.

Fix: key open and close by the same single id and drop the Date.now() fallback for the streaming key.

if (screenWs) {
await new Promise<void>((resolve, reject) =>
screenWs.end((err?: Error | null) => (err ? reject(err) : resolve())),
);
activeWriteStreams.delete(createdAt);
} else if (payload.screen.videoData && payload.screen.videoData.byteLength > 0) {
await fs.writeFile(screenVideoPath, Buffer.from(payload.screen.videoData));
}
Comment on lines +2130 to +2141
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if discardCursorTelemetry is the only cleanup called on discard
rg -n "discardCursorTelemetry" --type ts -C 3

Repository: siddharthvaddem/openscreen

Length of output: 1541


🏁 Script executed:

#!/bin/bash
# Find the discard-cursor-telemetry handler in electron handlers
rg -n "discard-cursor-telemetry" --type ts -A 15 electron/

Repository: siddharthvaddem/openscreen

Length of output: 1152


🏁 Script executed:

#!/bin/bash
# Also check what happens with activeWriteStreams when recording is discarded
rg -n "activeWriteStreams" --type ts -B 3 -A 3

Repository: siddharthvaddem/openscreen

Length of output: 3504


🏁 Script executed:

#!/bin/bash
# Check if there's a cancel/cleanup handler for recordings
rg -n "cancelRecording\|abort.*[Rr]ecord" --type ts -A 5

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Search for the discard-cursor-telemetry handler definition
rg -n '"discard-cursor-telemetry"' electron/ipc/handlers.ts -A 20

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Also check the structure around the storeRecordedSessionFiles handler to understand the flow better
rg -n '"store-recorded-session"' electron/ipc/handlers.ts -B 5 -A 3

Repository: siddharthvaddem/openscreen

Length of output: 313


🏁 Script executed:

#!/bin/bash
# Search for discard-cursor-telemetry in all files
rg -n '"discard-cursor-telemetry"' . --type ts

Repository: siddharthvaddem/openscreen

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Check the useScreenRecorder hook to see full flow on discard
rg -n "discardRecordingId.current" src/hooks/useScreenRecorder.ts -B 5 -A 10

Repository: siddharthvaddem/openscreen

Length of output: 5815


Streams definitely leak when recording is cancelled/discarded.

When cancelRecording is called, it sets discardRecordingId.current and triggers finalization. In finalizeScreenRecording, if the recording is being discarded (line 416-418 in useScreenRecorder.ts), it bails out early without calling storeRecordedSession. That handler is the only place activeWriteStreams gets cleaned up—so the write streams stay open indefinitely, and partial .webm files are orphaned on disk.

The discardCursorTelemetry call looks like an incomplete attempt at cleanup; the handler doesn't even exist in the codebase.

You'll need either:

  1. A dedicated IPC to abort/cleanup a recording stream by recordingId, or
  2. Close streams directly when discard happens (maybe in a new handler or as part of the same finalize flow)

Without this, cancelled recordings will accumulate open file handles and disk clutter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 2130 - 2141, The finalize path leaks
file streams when a recording is discarded: update finalizeScreenRecording
(and/or cancelRecording flow) to explicitly close and cleanup any
activeWriteStreams entry for the recordingId (use the same logic as in the
existing write/close block that calls screenWs.end and
activeWriteStreams.delete) before bailing out, and also remove any partial
screenVideoPath (or ensure stream end is awaited) so open handles and orphaned
.webm files are cleaned up; reference activeWriteStreams,
finalizeScreenRecording, cancelRecording, discardRecordingId.current,
storeRecordedSession and discardCursorTelemetry when adding this cleanup or
creating a dedicated IPC abortRecording handler.


let webcamVideoPath: string | undefined;
if (payload.webcam) {
webcamVideoPath = resolveRecordingOutputPath(payload.webcam.fileName);
await fs.writeFile(webcamVideoPath, Buffer.from(payload.webcam.videoData));
const webcamWs = activeWriteStreams.get(createdAt + 1); // webcam stream keyed as recordingId+1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recordingId + 1 as the webcam key is brittle magic.

This + 1 convention is undocumented numeric coupling between renderer (activeRecordingId + 1) and main (createdAt + 1). Prefer explicit keys like ${id}:screen / ${id}:webcam, or return a real stream handle from open so callers never compute keys.

if (webcamWs) {
await new Promise<void>((resolve, reject) =>
webcamWs.end((err?: Error | null) => (err ? reject(err) : resolve())),
);
activeWriteStreams.delete(createdAt + 1);
} else if (payload.webcam.videoData && payload.webcam.videoData.byteLength > 0) {
await fs.writeFile(webcamVideoPath, Buffer.from(payload.webcam.videoData));
}
}

const session: RecordingSession = webcamVideoPath
Expand Down
6 changes: 6 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
storeRecordedSession: (payload: StoreRecordedSessionInput) => {
return ipcRenderer.invoke("store-recorded-session", payload);
},
openRecordingStream: (recordingId: number, fileName: string) => {
return ipcRenderer.invoke("open-recording-stream", recordingId, fileName);
},
appendRecordingChunk: (recordingId: number, chunk: ArrayBuffer) => {
return ipcRenderer.invoke("append-recording-chunk", recordingId, chunk);
},

getRecordedVideoPath: () => {
return ipcRenderer.invoke("get-recorded-video-path");
Expand Down
140 changes: 105 additions & 35 deletions src/hooks/useScreenRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type UseScreenRecorderReturn = {
type RecorderHandle = {
recorder: MediaRecorder;
recordedBlobPromise: Promise<Blob>;
streaming: boolean;
};

type NativeWindowsRecordingHandle = {
Expand All @@ -92,26 +93,77 @@ type NativeMacRecordingHandle = {
paused: boolean;
};

function createRecorderHandle(stream: MediaStream, options: MediaRecorderOptions): RecorderHandle {
function createRecorderHandle(
stream: MediaStream,
options: MediaRecorderOptions,
recordingId: number,
fileName: string,
): RecorderHandle {
const recorder = new MediaRecorder(stream, options);
const chunks: Blob[] = [];
const mimeType = options.mimeType || "video/webm";

// Try to open a disk-backed stream. Falls back to in-memory if IPC unavailable.
const streamOpenPromise =
window.electronAPI?.openRecordingStream?.(recordingId, fileName) ??
Promise.resolve({ success: false });

const pendingChunks: ArrayBuffer[] = [];
let streamReady = false;
let streamFailed = false;

streamOpenPromise.then((result) => {
if (result.success) {
streamReady = true;
for (const chunk of pendingChunks) {
void window.electronAPI.appendRecordingChunk(recordingId, chunk);
}
pendingChunks.length = 0;
} else {
streamFailed = true;
}
});

const fallbackChunks: Blob[] = [];

const recordedBlobPromise = new Promise<Blob>((resolve, reject) => {
recorder.ondataavailable = (event: BlobEvent) => {
if (event.data && event.data.size > 0) {
chunks.push(event.data);
if (!event.data || event.data.size === 0) return;

if (streamFailed) {
fallbackChunks.push(event.data);
return;
}

void event.data.arrayBuffer().then((buf) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] Chunk write order isn't guaranteed → intermittent corruption.

Blob.arrayBuffer() resolution order across separate calls isn't spec-guaranteed, and the append is fire-and-forget (void). Chunk N+1 can resolve and append before chunk N — for a sequential container that's an unplayable file, intermittently, with no error.

Fix: make ordering explicit — serialize through one queue (await chunk K's arrayBuffer() + append before K+1), or pass a monotonic seq and have the main side assert/order on it.

Related: the three-state handling here (streamFailed / streamReady / pendingChunks, two booleans + an array + a nested streamFailed re-check) is a small state machine bolted into the callback. A tiny ChunkSink (accept(chunk) that buffers-until-ready / appends / falls back) would collapse the branching and is the natural home for the ordering fix.

if (streamFailed) {
fallbackChunks.push(new Blob([buf], { type: mimeType }));
return;
}
if (streamReady) {
void window.electronAPI.appendRecordingChunk(recordingId, buf);
} else {
pendingChunks.push(buf);
}
});
};

recorder.onerror = () => {
reject(new Error("Recording failed"));
};

recorder.onstop = () => {
resolve(new Blob(chunks, { type: mimeType }));
if (streamFailed) {
// Streaming failed — return full in-memory blob as fallback.
resolve(new Blob(fallbackChunks, { type: mimeType }));
} else {
// Streaming succeeded — main process already has the data on disk.
resolve(new Blob([], { type: mimeType }));
}
};
});

recorder.start(RECORDER_TIMESLICE_MS);
return { recorder, recordedBlobPromise };
return { recorder, recordedBlobPromise, streaming: !streamFailed };
Comment on lines +104 to +166
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Race condition: streaming flag doesn't reflect actual outcome.

The streaming flag is captured at line 166 as !streamFailed, but at that point the streamOpenPromise hasn't resolved yet — so streamFailed is always false and streaming is always true initially.

If the IPC stream fails to open (permissions, disk error, etc.), the fallback logic in ondataavailable correctly buffers to fallbackChunks, and onstop correctly returns the fallback blob. But then in finalizeRecording, the check at line 430:

if (!activeScreenRecorder.streaming && screenBlob.size > 0) {

...is false because streaming was captured as true. The in-memory data gets thrown away and an empty ArrayBuffer(0) is sent instead, resulting in data loss.

kinda cursed timing issue — maybe make streaming a getter that checks the final state, or resolve the promise before returning.

🐛 One possible fix: defer flag evaluation to onstop
-	const fallbackChunks: Blob[] = [];
+	let usedFallback = false;
+	const fallbackChunks: Blob[] = [];

 	const recordedBlobPromise = new Promise<Blob>((resolve, reject) => {
 		recorder.ondataavailable = (event: BlobEvent) => {
 			if (!event.data || event.data.size === 0) return;

 			if (streamFailed) {
 				fallbackChunks.push(event.data);
+				usedFallback = true;
 				return;
 			}

 			void event.data.arrayBuffer().then((buf) => {
 				if (streamFailed) {
 					fallbackChunks.push(new Blob([buf], { type: mimeType }));
+					usedFallback = true;
 					return;
 				}
 				// ... rest unchanged
 			});
 		};

 		recorder.onstop = () => {
-			if (streamFailed) {
+			if (usedFallback) {
 				resolve(new Blob(fallbackChunks, { type: mimeType }));
 			} else {
 				resolve(new Blob([], { type: mimeType }));
 			}
 		};
 	});

 	recorder.start(RECORDER_TIMESLICE_MS);
-	return { recorder, recordedBlobPromise, streaming: !streamFailed };
+	// streaming is determined by whether we fell back to in-memory
+	// We need to expose this info; simplest: check blob size in finalizeRecording
+	// OR return a promise/getter. For now, returning true and relying on blob.size check.
+	return { recorder, recordedBlobPromise, streaming: true };

Then update finalizeRecording to check blob size regardless of streaming flag:

-if (!activeScreenRecorder.streaming && screenBlob.size === 0) {
+if (screenBlob.size === 0 && !activeScreenRecorder.streaming) {
   return;
 }
+// If blob has data, always process it (fallback case)
+let screenVideoData: ArrayBuffer = new ArrayBuffer(0);
+if (screenBlob.size > 0) {
+  const fixedScreenBlob = await fixWebmDuration(screenBlob, duration);
+  screenVideoData = await fixedScreenBlob.arrayBuffer();
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScreenRecorder.ts` around lines 104 - 166, The returned
streaming flag is captured before streamOpenPromise resolves, causing a race and
data loss; fix by determining streaming only when recording stops and exposing
that final value (or including it with the resolved blob). Concretely, update
recorder.onstop to set a finalStreaming boolean (based on streamFailed) before
resolving recordedBlobPromise, and change the hook's return value to either
provide a getter/function for streaming or have recordedBlobPromise resolve to
an object like { blob, streaming }; then update finalizeRecording to consume the
final streaming state from that getter/object instead of reading the
early-captured streaming variable. Ensure you touch streamOpenPromise,
streamFailed/streamReady, recorder.onstop, recordedBlobPromise, and the place
that checks activeScreenRecorder.streaming in finalizeRecording.

}

export function useScreenRecorder(): UseScreenRecorderReturn {
Expand Down Expand Up @@ -365,32 +417,41 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
window.electronAPI?.discardCursorTelemetry(activeRecordingId);
return;
}
if (screenBlob.size === 0) {
// When streaming succeeded the blob is empty — the data is already on disk.
if (!activeScreenRecorder.streaming && screenBlob.size === 0) {
return;
}

const fixedScreenBlob = await fixWebmDuration(screenBlob, duration);
let fixedWebcamBlob: Blob | null = null;
const screenFileName = `${RECORDING_FILE_PREFIX}${activeRecordingId}${VIDEO_FILE_EXTENSION}`;
const webcamFileName = `${RECORDING_FILE_PREFIX}${activeRecordingId}${WEBCAM_FILE_SUFFIX}${VIDEO_FILE_EXTENSION}`;

// Only fix duration / convert to ArrayBuffer if we have in-memory data.
let screenVideoData: ArrayBuffer = new ArrayBuffer(0);
if (!activeScreenRecorder.streaming && screenBlob.size > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] fixWebmDuration is skipped for every streamed recording → broken duration metadata.

Raw MediaRecorder WebM has no usable duration header — that's why fixWebmDuration exists. Streamed files now bypass it, so the long recordings this PR fixes save with N/A/infinite duration: no reliable seek bar, wrong length downstream. The crash is gone, but the file looks corrupt.

Fix: repair duration on the file after the stream closes, in the main process where the bytes now live. handlers.ts already imports spawn and the app ships ffmpeg, so a -c copy remux / duration-rewrite on close is the natural home (the renderer can't do it anymore — it no longer holds the blob).

const fixedScreenBlob = await fixWebmDuration(screenBlob, duration);
screenVideoData = await fixedScreenBlob.arrayBuffer();
}

let webcamVideoData: ArrayBuffer | undefined;
if (activeWebcamRecorder) {
const webcamBlob = await activeWebcamRecorder.recordedBlobPromise.catch(() => null);
if (webcamBlob && webcamBlob.size > 0) {
fixedWebcamBlob = await fixWebmDuration(webcamBlob, duration);
if (!activeWebcamRecorder.streaming && webcamBlob && webcamBlob.size > 0) {
const fixedWebcamBlob = await fixWebmDuration(webcamBlob, duration);
webcamVideoData = await fixedWebcamBlob.arrayBuffer();
} else if (activeWebcamRecorder.streaming) {
webcamVideoData = new ArrayBuffer(0);
}
}

const screenFileName = `${RECORDING_FILE_PREFIX}${activeRecordingId}${VIDEO_FILE_EXTENSION}`;
const webcamFileName = `${RECORDING_FILE_PREFIX}${activeRecordingId}${WEBCAM_FILE_SUFFIX}${VIDEO_FILE_EXTENSION}`;
const result = await window.electronAPI.storeRecordedSession({
screen: {
videoData: await fixedScreenBlob.arrayBuffer(),
videoData: screenVideoData,
fileName: screenFileName,
},
webcam: fixedWebcamBlob
? {
videoData: await fixedWebcamBlob.arrayBuffer(),
fileName: webcamFileName,
}
: undefined,
webcam:
webcamVideoData !== undefined
? { videoData: webcamVideoData, fileName: webcamFileName }
: undefined,
createdAt: activeRecordingId,
cursorCaptureMode,
});
Expand Down Expand Up @@ -861,10 +922,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return true;
}
if (webcamStream.current) {
nativeWebcamRecorder = createRecorderHandle(webcamStream.current, {
mimeType: selectMimeType(),
videoBitsPerSecond: BITRATE_BASE,
});
nativeWebcamRecorder = createRecorderHandle(
webcamStream.current,
{ mimeType: selectMimeType(), videoBitsPerSecond: BITRATE_BASE },
activeRecordingId + 1,
`${RECORDING_FILE_PREFIX}${activeRecordingId}${WEBCAM_FILE_SUFFIX}${VIDEO_FILE_EXTENSION}`,
);
} else {
webcamAcquireId.current++;
setWebcamEnabledState(false);
Expand Down Expand Up @@ -1252,13 +1315,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn {

recordingId.current = Date.now();
const activeRecordingId = recordingId.current;
screenRecorder.current = createRecorderHandle(stream.current, {
mimeType,
videoBitsPerSecond,
...(hasAudio
? { audioBitsPerSecond: systemAudioTrack ? AUDIO_BITRATE_SYSTEM : AUDIO_BITRATE_VOICE }
: {}),
});
screenRecorder.current = createRecorderHandle(
stream.current,
{
mimeType,
videoBitsPerSecond,
...(hasAudio
? { audioBitsPerSecond: systemAudioTrack ? AUDIO_BITRATE_SYSTEM : AUDIO_BITRATE_VOICE }
: {}),
},
activeRecordingId,
`${RECORDING_FILE_PREFIX}${activeRecordingId}${VIDEO_FILE_EXTENSION}`,
);
screenRecorder.current.recorder.addEventListener(
"error",
() => {
Expand All @@ -1268,10 +1336,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
);

if (webcamStream.current) {
webcamRecorder.current = createRecorderHandle(webcamStream.current, {
mimeType,
videoBitsPerSecond: Math.min(videoBitsPerSecond, BITRATE_BASE),
});
webcamRecorder.current = createRecorderHandle(
webcamStream.current,
{ mimeType, videoBitsPerSecond: Math.min(videoBitsPerSecond, BITRATE_BASE) },
activeRecordingId + 1,
`${RECORDING_FILE_PREFIX}${activeRecordingId}${WEBCAM_FILE_SUFFIX}${VIDEO_FILE_EXTENSION}`,
);
}

accumulatedDurationMs.current = 0;
Expand Down
Loading