Skip to content

fix(recording): start webcam recorder after native macOS capture begins#85

Open
josiahcoad wants to merge 1 commit into
getopenscreen:mainfrom
josiahcoad:fix/native-mac-webcam-sync
Open

fix(recording): start webcam recorder after native macOS capture begins#85
josiahcoad wants to merge 1 commit into
getopenscreen:mainfrom
josiahcoad:fix/native-mac-webcam-sync

Conversation

@josiahcoad

@josiahcoad josiahcoad commented Jul 12, 2026

Copy link
Copy Markdown

Problem

On the native macOS capture path, recordings made with the webcam enabled are audibly out of sync: the voice/screen lead the webcam PiP by about a second, in both the editor preview and exports.

Root cause

startNativeMacRecordingIfAvailable created the webcam RecorderHandle (whose constructor calls MediaRecorder.start()) before window.electronAPI.startNativeMacRecording(request). That IPC call only resolves once the ScreenCaptureKit helper emits recording-started — i.e. after permission checks, stream construction, and first-frame latency. Measured on an M1 Pro, that startup takes ~1.03s (diagnostic: session createdAt vs helper recording-started timestamps).

So the webcam webm contains ~1s of pre-roll footage from before the screen/mic recording began. The editor and exporter align both files at t=0, which shifts the webcam behind the voice/screen by exactly the helper's startup latency. The screen recording itself (video + system audio + mic) is internally in sync — only the webcam sidecar is offset.

Fix

Start the webcam recorder only after startNativeMacRecording resolves, so both recordings share the same time origin. Residual offset is now just IPC + MediaRecorder start latency (tens of ms) instead of the helper's full startup time.

Note: the native Windows path (startNativeWindowsRecordingIfAvailable) has the same pattern and likely the same skew; I left it untouched since I can only verify on macOS.

Testing

  • tsc --noEmit, biome check, and the full vitest suite (352 tests) pass.
  • Verified the diagnosis empirically on a real recording: remuxing the exported video with the mic track delayed by the measured 1.03s startup latency produces perfect lip sync; the unshifted mix reproduces the reported desync.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved synchronization when starting macOS screen recordings with webcam capture enabled.
    • Prevented recordings from starting or continuing when the countdown is no longer active.
    • Improved webcam initialization reliability and reduced timing-related capture issues.

On the native macOS path the webcam MediaRecorder was started while the
ScreenCaptureKit helper was still spinning up (permission checks, stream
construction, first-frame latency — ~1s). The webcam sidecar therefore
contained ~1s of pre-roll footage from before the screen/mic recording
began, and since the editor and exporter align both files at t=0, the
webcam PiP lagged the voice/screen by that startup latency in both
preview and export.

Start the webcam recorder only after startNativeMacRecording resolves,
which happens when the helper emits recording-started (first video frame
written), so both recordings share the same time origin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The native macOS recording startup flow now handles missing webcam streams, discards recordings when countdowns become inactive, and creates the webcam recorder after the native recording-started event.

Changes

Native recording synchronization

Layer / File(s) Summary
Native startup and webcam recorder sequencing
src/hooks/useScreenRecorder.ts
Missing webcam streams increment the acquisition id and disable webcam capture; inactive countdowns discard native recordings, while active runs defer webcam recorder creation until native recording has started.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: etiennelescot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: delaying webcam recorder start until native macOS capture begins.
Description check ✅ Passed The description covers the problem, root cause, fix, and testing, though several template sections are missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useScreenRecorder.ts (1)

1014-1051: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Native macOS recording leaks if webcam recorder creation throws.

startNativeMacRecording resolves at line 1014, but createRecorderHandle at line 1029 runs afterward. If createRecorderHandle throws (e.g., MediaRecorder constructor or recorder.start() fails), the catch block at line 1049 logs and rethrows without stopping the now-running native recording. Since nativeMacRecording.current is never assigned (line 1036 is unreachable), no cleanup path — the unmount effect (line 716), cancelRecording, or startRecording's catch — will find or stop the orphaned native recording.

🔒 Proposed fix: stop native recording in the catch block if it was started
 		const result = await window.electronAPI.startNativeMacRecording(request);
 		if (!result.success || !result.recordingId) {
 			throw new Error(result.error ?? "Native macOS capture failed.");
 		}
+		const nativeRecordingStarted = true;
 		if (!isCountdownRunActive(countdownRunToken)) {
 			await window.electronAPI.stopNativeMacRecording(true);
 			return true;
 		}

 		// Start the webcam recorder only after the helper has emitted
 		// recording-started (startNativeMacRecording resolves on that event).
 		// Starting it earlier bakes the helper's capture startup latency
 		// (~1s of pre-roll) into the webcam file, desyncing it from the
 		// screen/mic recording that the editor and exporter align at t=0.
 		if (webcamEnabled && webcamStream.current) {
 			nativeWebcamRecorder = createRecorderHandle(webcamStream.current, {
 				mimeType: selectMimeType(),
 				videoBitsPerSecond: BITRATE_BASE,
 			});
 		}

 		recordingId.current = result.recordingId;
 		nativeMacRecording.current = {
 			recordingId: result.recordingId,
 			finalizing: false,
 			paused: false,
 		};
 		webcamRecorder.current = nativeWebcamRecorder;
 		accumulatedDurationMs.current = 0;
 		segmentStartedAt.current = Date.now();
 		allowAutoFinalize.current = true;
 		setRecording(true);
 		setPaused(false);
 		setElapsedSeconds(0);
 		return true;
 	} catch (error) {
 		console.error("Native macOS capture failed:", error);
+		if (nativeRecordingStarted) {
+			await window.electronAPI.stopNativeMacRecording(true).catch(() => undefined);
+		}
 		throw error;
 	}

Note: nativeRecordingStarted should be declared as let nativeRecordingStarted = false; before the try block and set to true after the success check.

🤖 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 1014 - 1051, Track whether
native recording started in the recording startup flow by declaring
nativeRecordingStarted before the try block and setting it true after the
startNativeMacRecording success check. In the catch block, stop the native
recording with stopNativeMacRecording(true) when that flag is set, then log and
rethrow the original error.
🧹 Nitpick comments (1)
src/hooks/useScreenRecorder.ts (1)

946-960: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use waitForWebcamReady instead of duplicating its logic.

The inline webcam-ready wait at lines 948-959 is identical to the waitForWebcamReady helper at lines 779-796. The Windows path already calls the helper (line 830). Extracting this reduces duplication across three call sites (macOS, Windows, browser capture at lines 1261-1272).

♻️ Proposed refactor
 		if (webcamEnabled) {
-			if (!webcamReady.current) {
-				await new Promise<void>((resolve) => {
-					const interval = setInterval(() => {
-						if (webcamReady.current) {
-							clearInterval(interval);
-							resolve();
-						}
-					}, 50);
-					setTimeout(() => {
-						clearInterval(interval);
-						resolve();
-					}, 5000);
-				});
-			}
+			await waitForWebcamReady();
 			if (!isCountdownRunActive(countdownRunToken)) {
 				return true;
 			}
🤖 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 946 - 960, Replace the inline
webcam-ready polling block in the webcamEnabled recording flow with the existing
waitForWebcamReady helper, preserving the current conditional check and timeout
behavior provided by that helper. Do not duplicate the interval or timeout logic
at this call site.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@src/hooks/useScreenRecorder.ts`:
- Around line 1014-1051: Track whether native recording started in the recording
startup flow by declaring nativeRecordingStarted before the try block and
setting it true after the startNativeMacRecording success check. In the catch
block, stop the native recording with stopNativeMacRecording(true) when that
flag is set, then log and rethrow the original error.

---

Nitpick comments:
In `@src/hooks/useScreenRecorder.ts`:
- Around line 946-960: Replace the inline webcam-ready polling block in the
webcamEnabled recording flow with the existing waitForWebcamReady helper,
preserving the current conditional check and timeout behavior provided by that
helper. Do not duplicate the interval or timeout logic at this call site.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1000923-2481-41dc-9cc3-7c06b3b9fff0

📥 Commits

Reviewing files that changed from the base of the PR and between c2ba750 and cbef8e4.

📒 Files selected for processing (1)
  • src/hooks/useScreenRecorder.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant