Skip to content

[Bug]: Mic recording is laggy/quirky at the start and desynced from the video track #57

Description

@EtienneLescot

Search existing issues

  • I have searched the existing issues

Describe the bug

When recording with the microphone enabled, the mic track in the captured .webm is audibly delayed and "quirky" (clicks/pops/garbled audio) during the first ~1 second of the recording. After the initial second the mic track also remains visibly desynchronized from the video track — speech is shifted a few hundred milliseconds later than the corresponding mouth movement / screen action. The longer the recording runs, the more pronounced the offset feels.

This is reproducible on main (72cb541) using the browser capture path (no native helper). Both the saved WebM and the MP4 produced by the exporter exhibit the same A/V drift at the start.

Expected behavior

The microphone should be in sync with the video track from the very first frame of the recording. The first ~1 second of audio should not be garbled / clipped / delayed, and the mic should not drift relative to the video over the course of the recording.

To Reproduce

  1. npm install && npm run dev
  2. Pick any screen source, enable system audio + microphone, leave webcam off.
  3. Click Record (no countdown).
  4. Speak "one, two, three" within the first second of recording, then keep speaking for ~10 more seconds.
  5. Stop the recording and open the resulting .webm (and the exported .mp4).
  6. Play it back and compare the spoken "one, two, three" with the screen timeline / a visible cue — the audio lands noticeably after the visual cue, and the first word is clipped or garbled.

Screenshots

N/A — audio-only symptom.

OS

  • Linux (also reproducible on Windows / macOS when the native helper is not in use and the browser capture path is taken)

OS Version

N/A

Browser

  • Electron renderer (Chromium). Not browser-specific.

Browser Version

N/A

Device Type

  • Desktop

Additional context

Investigation summary (browser capture path)

The desync + leading-garble is a clock-anchor problem in src/hooks/useScreenRecorder.ts. There is no shared audio/video start anchor, no pre-roll discard, and no fade-in for the mic track — unlike the native Windows path which explicitly clears mic pre-roll in electron/native/wgc-capture/src/audio_sample_utils.cpp:289 (beginTimeline).

Current start sequence in useScreenRecorder.ts:

Step Line What runs
1 useScreenRecorder.ts:1161 (win32) / 1184 / 1196 / 1202 (other) getDisplayMedia / getUserMedia for screen + system audio — screen track starts here
2 useScreenRecorder.ts:1217 getUserMedia for the microphone — mic track starts here, after the screen has already been capturing for Δt₁
3 useScreenRecorder.ts:1281 new AudioContext() — mixed destination stream begins here, after step 2
4 useScreenRecorder.ts:1298 videoTrack.applyConstraints(...) — additional async renegotiation
5 useScreenRecorder.ts:1341 createRecorderHandle(...)recorder.start(1000) (via recorderHandle.ts:141, RECORDER_TIMESLICE_MS = 1000)

Three things combine to produce the symptom:

  1. No shared clock anchor. The screen track begins at step 1, the mic track at step 2, and the AudioContext clock at step 3. Between step 1 and step 3 the mic may already have buffered audio that is silently discarded by the destination stream because the AudioContext did not exist yet to route it.
  2. No pre-roll discard. The Windows-native path (audio_sample_utils.cpp:289) explicitly resets emittedFrames_=0 and clears the mic queue when the first video frame arrives; the browser path has no equivalent — even though the MediaRecorder has timeslice=1000, no code drops or rewrites the first chunk's timestamps.
  3. Static gain, no fade-in. micGain.gain.value = MIC_GAIN_BOOST (useScreenRecorder.ts:1286, MIC_GAIN_BOOST = 1.4 at line 47) is a constant, so any DC offset / warm-up discontinuity at the very start of the stream survives the mix and can sound like a click/pop.

The MP4 export inherits the misalignment because src/lib/exporter/muxer.ts:62-70 interleaves audio/video packets by the timestamps that came out of the WebM; if those are wrong going in, the muxer preserves the error.

The macOS-native (electron/native/screencapturekit/.../main.swift:301-306) and Windows-native (audio_sample_utils.cpp:289-298) paths do not exhibit this — both anchor audio to the first video frame in the same clock domain. So this is scoped to the browser capture path, which is also the Linux path and the fallback path on Windows/macOS.

Suggested fix shape (not in this issue, just for triage)

  • Start both getUserMedia / getDisplayMedia calls in parallel before constructing the AudioContext.
  • Capture AudioContext.currentTime once both streams are connected and use it as a shared origin.
  • Either drop the first ~RECORDER_TIMESLICE_MS chunk from the recorder, or rewrite the timestamps of the first audio chunk so it starts at t=0 together with the first video keyframe.
  • Apply a short linearRampToValueAtTime on micGain over ~10–20 ms to suppress the start-of-stream click.

Linked references:

  • src/hooks/useScreenRecorder.ts:1161-1290 (capture + mix)
  • src/hooks/recorderHandle.ts:36, 141 (MediaRecorder + timeslice)
  • src/lib/exporter/muxer.ts:62-70 (export-time interleaving)
  • electron/native/wgc-capture/src/audio_sample_utils.cpp:289-298 (reference: native pre-roll discard)
  • docs/engineering/windows-native-recorder-roadmap.md:116, 144 (existing known-issue notes for related drift)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions