Describe the bug
When recording on Windows (WGC capture backend), pressing Pause and then Resume causes the recording to either:
- Freeze entirely (cannot resume at all)
- Produce a corrupt output file where the video after the resume point is damaged/skipped
This is not just a cursor sync issue (already reported in #390, #470) — the recording itself fails to function correctly after pause/resume.
Root Cause Analysis
I did a deep-dive analysis of the source code (both the Electron main process and the native C++ wgc-capture module). The pause feature is fundamentally broken on Windows due to five interacting bugs:
Bug 1 (Fatal): Frame-gap fill blocks the encoder and freezes the capture thread
File: electron/native/wgc-capture/src/mf_encoder.cpp (lines 259–273, 311–333)
When resume happens, writeFrame() calls extendLastFrameToLocked() which fills every missing frame slot since the pause by duplicating the last frame:
while (nextSampleTimeHns + frameDurationHns <= timestampHns) {
if (!writeNv12SampleLocked(lastFrameBuffer_, nextSampleTimeHns)) {
return false; // WriteSample fails here
}
...
}
This synchronous while-loop runs inside the WGC frame callback thread. For a 1-minute pause at 60fps, it writes 3600 duplicate frames before the first real resumed frame. The IMFSinkWriter::WriteSample() is not designed for batch-backfilling — it can fail under memory pressure, effectively killing the recording.
Bug 2 (Race condition): Timestamp adjustment has a TOCTOU race
File: electron/native/wgc-capture/src/main.cpp (lines 171–185)
adjustedVideoTimestampHns() reads g_pauseRequested and g_pauseStartTimestampHns from different threads without atomic ordering guarantees. If the WGC frame callback fires between the stdin thread setting g_pauseRequested and recording pauseStartTimestampHns, the computed pause duration is wrong, causing timestamp corruption.
Bug 3: Audio/video pause transitions are not synchronized
File: electron/native/wgc-capture/src/main.cpp (lines 400–411)
The main loop polls g_pauseRequested every 20ms and toggles WASAPI audio accordingly. But the WGC frame callback responds to the flag immediately. This creates a ~20ms window where video has already stopped but audio continues, and vice versa on resume. Repeated pause/resume cycles accumulate drift.
Bug 4: Electron-side pause state variable may desync
The minified main process code shows:
- Pause handler checks
Tl but the setter (pr) may target a different variable (Al)
- On rapid pause→resume→pause cycles, the guard condition (
if (Tl) return) can short-circuit incorrectly, causing the native process to receive commands in the wrong order
Bug 5: Audio fade-in state can be overwritten after resume
File: electron/native/wgc-capture/src/wasapi_loopback.cpp (lines 237–238)
resume() sets a fade-in counter, but writeSilenceFrames() also overwrites it (line 348). If the first audio packet after resume has a gap, the fade-in is replaced by abrupt silence insertion → "no sound after resume".
The Real Root Cause
Windows.Graphics.Capture API does not support pausing. The Direct3D11CaptureFramePool and GraphicsCaptureSession only have StartCapture() and Close(). Recordly fakes pause by silently dropping frames in the callback, then tries to backfill the gap on resume — a fundamentally flawed approach.
System
- OS: Windows 10/11
- Recordly Version: 1.1.20
- Capture Backend: WGC (native Windows capture)
To Reproduce
- Start a screen recording (any source)
- Let it record for a few seconds
- Press Pause
- Wait 10+ seconds
- Press Resume
- Observe: recording freezes, or the resumed video has visual/audio corruption
Additional Context
I have the full analysis of all involved source files ready if any maintainer wants to dive deeper:
electron/native/wgc-capture/src/main.cpp — stdin command handling, pause coordination
electron/native/wgc-capture/src/wgc_session.cpp — WGC frame callback (drops frames on pause)
electron/native/wgc-capture/src/mf_encoder.cpp — gap-fill logic in writeFrame()/extendLastFrameToLocked()
electron/native/wgc-capture/src/wasapi_loopback.cpp — pause/resume via IAudioClient::Stop/Start
dist-electron/main.cjs — IPC handlers pause-native-screen-recording / resume-native-screen-recording
Suggested Fix
The encoder should never stop receiving frames during pause. Instead of dropping frames, the WGC callback should continue writing the last frame with a "pause marker" (e.g., via SEI NAL units or a separate track). This keeps the encoder pipeline hot and avoids the destructive backfill pattern.
If true pause markers are not feasible, at minimum:
- Add a maximum pause duration warning (e.g., "Pausing longer than 30 seconds may cause issues")
- Move the gap-fill logic off the WGC callback thread
- Use a
std::atomic<int64_t> with memory_order_seq_cst for all pause-related shared state
- Lock audio and video pause state changes to the same cycle
Describe the bug
When recording on Windows (WGC capture backend), pressing Pause and then Resume causes the recording to either:
This is not just a cursor sync issue (already reported in #390, #470) — the recording itself fails to function correctly after pause/resume.
Root Cause Analysis
I did a deep-dive analysis of the source code (both the Electron main process and the native C++
wgc-capturemodule). The pause feature is fundamentally broken on Windows due to five interacting bugs:Bug 1 (Fatal): Frame-gap fill blocks the encoder and freezes the capture thread
File:
electron/native/wgc-capture/src/mf_encoder.cpp(lines 259–273, 311–333)When resume happens,
writeFrame()callsextendLastFrameToLocked()which fills every missing frame slot since the pause by duplicating the last frame:This synchronous while-loop runs inside the WGC frame callback thread. For a 1-minute pause at 60fps, it writes 3600 duplicate frames before the first real resumed frame. The
IMFSinkWriter::WriteSample()is not designed for batch-backfilling — it can fail under memory pressure, effectively killing the recording.Bug 2 (Race condition): Timestamp adjustment has a TOCTOU race
File:
electron/native/wgc-capture/src/main.cpp(lines 171–185)adjustedVideoTimestampHns()readsg_pauseRequestedandg_pauseStartTimestampHnsfrom different threads without atomic ordering guarantees. If the WGC frame callback fires between the stdin thread settingg_pauseRequestedand recordingpauseStartTimestampHns, the computed pause duration is wrong, causing timestamp corruption.Bug 3: Audio/video pause transitions are not synchronized
File:
electron/native/wgc-capture/src/main.cpp(lines 400–411)The main loop polls
g_pauseRequestedevery 20ms and toggles WASAPI audio accordingly. But the WGC frame callback responds to the flag immediately. This creates a ~20ms window where video has already stopped but audio continues, and vice versa on resume. Repeated pause/resume cycles accumulate drift.Bug 4: Electron-side pause state variable may desync
The minified main process code shows:
Tlbut the setter (pr) may target a different variable (Al)if (Tl) return) can short-circuit incorrectly, causing the native process to receive commands in the wrong orderBug 5: Audio fade-in state can be overwritten after resume
File:
electron/native/wgc-capture/src/wasapi_loopback.cpp(lines 237–238)resume()sets a fade-in counter, butwriteSilenceFrames()also overwrites it (line 348). If the first audio packet after resume has a gap, the fade-in is replaced by abrupt silence insertion → "no sound after resume".The Real Root Cause
Windows.Graphics.Capture API does not support pausing. The
Direct3D11CaptureFramePoolandGraphicsCaptureSessiononly haveStartCapture()andClose(). Recordly fakes pause by silently dropping frames in the callback, then tries to backfill the gap on resume — a fundamentally flawed approach.System
To Reproduce
Additional Context
I have the full analysis of all involved source files ready if any maintainer wants to dive deeper:
electron/native/wgc-capture/src/main.cpp— stdin command handling, pause coordinationelectron/native/wgc-capture/src/wgc_session.cpp— WGC frame callback (drops frames on pause)electron/native/wgc-capture/src/mf_encoder.cpp— gap-fill logic inwriteFrame()/extendLastFrameToLocked()electron/native/wgc-capture/src/wasapi_loopback.cpp— pause/resume viaIAudioClient::Stop/Startdist-electron/main.cjs— IPC handlerspause-native-screen-recording/resume-native-screen-recordingSuggested Fix
The encoder should never stop receiving frames during pause. Instead of dropping frames, the WGC callback should continue writing the last frame with a "pause marker" (e.g., via SEI NAL units or a separate track). This keeps the encoder pipeline hot and avoids the destructive backfill pattern.
If true pause markers are not feasible, at minimum:
std::atomic<int64_t>withmemory_order_seq_cstfor all pause-related shared state