Search 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
npm install && npm run dev
- Pick any screen source, enable system audio + microphone, leave webcam off.
- Click Record (no countdown).
- Speak "one, two, three" within the first second of recording, then keep speaking for ~10 more seconds.
- Stop the recording and open the resulting
.webm (and the exported .mp4).
- 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
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:
- 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.
- 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.
- 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)
Search existing issues
Describe the bug
When recording with the microphone enabled, the mic track in the captured
.webmis 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
npm install && npm run dev.webm(and the exported.mp4).Screenshots
N/A — audio-only symptom.
OS
OS Version
N/A
Browser
Browser Version
N/A
Device Type
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 inelectron/native/wgc-capture/src/audio_sample_utils.cpp:289(beginTimeline).Current start sequence in
useScreenRecorder.ts:useScreenRecorder.ts:1161(win32) /1184/1196/1202(other)getDisplayMedia/getUserMediafor screen + system audio — screen track starts hereuseScreenRecorder.ts:1217getUserMediafor the microphone — mic track starts here, after the screen has already been capturing forΔt₁useScreenRecorder.ts:1281new AudioContext()— mixed destination stream begins here, after step 2useScreenRecorder.ts:1298videoTrack.applyConstraints(...)— additional async renegotiationuseScreenRecorder.ts:1341createRecorderHandle(...)→recorder.start(1000)(viarecorderHandle.ts:141,RECORDER_TIMESLICE_MS = 1000)Three things combine to produce the symptom:
AudioContextdid not exist yet to route it.audio_sample_utils.cpp:289) explicitly resetsemittedFrames_=0and clears the mic queue when the first video frame arrives; the browser path has no equivalent — even though theMediaRecorderhastimeslice=1000, no code drops or rewrites the first chunk's timestamps.micGain.gain.value = MIC_GAIN_BOOST(useScreenRecorder.ts:1286,MIC_GAIN_BOOST = 1.4at 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-70interleaves 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)
getUserMedia/getDisplayMediacalls in parallel before constructing theAudioContext.AudioContext.currentTimeonce both streams are connected and use it as a shared origin.~RECORDER_TIMESLICE_MSchunk from the recorder, or rewrite the timestamps of the first audio chunk so it starts att=0together with the first video keyframe.linearRampToValueAtTimeonmicGainover ~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)