Skip to content

Windows: screen video falls behind audio on long takes (fragmented MP4 drops the gap at fragment ends) #1164

Closed

Description

@EtienneLescot

Found in the v2.1.0 release-candidate QA. Refs #945.

Symptom

On Windows, the screen video of a long take falls behind its audio and webcam under load.

Take on the CI build of release/v2.1.0 @ 9c55596 (installed; Windows 11 26200, Ryzen 7 5800X, RTX 4070 Ti, 1920x1080 @ 100 %, hardware H.264, fragmented MP4):

  • Full screen + webcam (StreamCam 1080p30) + system audio, 10:25, while a CLI export loop ran (Openscreen.exe export … --quality source, ~40 % CPU).
  • ffprobe: screen video 625.456 s (37,539 frames), audio 625.643 s, webcam 625.677 s.
  • Wall-clock reference, the HUD timer visible in the screen track (first frame showing second N, at video time): N=21 → 21.017, N=301 → 301.017, N=341 → 340.983, N=376 → 375.967, N=411 → 410.917, N=451 → 450.850, N=601 → 600.800. In step for 5 minutes, then 217 ms lost between 5:00 and 10:00, steepest from 376 to 451 s.
  • Audio (TTS markers against the wall clock) loses 43 ms over the take; the webcam none against the audio.

Root cause

The fragmented MP4 sink drops the real gap at every fragment boundary.

  • The frame count is right: 37,539 frames is 625.65 s at 60 fps, the audio's length. The pacing from fix(windows): keep the screen video on the clock when the writer misses ticks #1056 does its job.
  • The sink keeps per-sample times inside a fragment: the trun durations are 858 / 1072 / 1286 units of 1/60000 s, that is 4, 5 and 6 refresh periods of the 280 Hz display, because the desktop was updating at ~56 Hz.
  • But every fragment ends on a duration of exactly 1000 units (1/60 s), and the next fragment's tfdt starts right after it. MFCreateFMPEG4MediaSink closes a fragment before it has the next sample, so it takes the last sample's own SampleDuration, which the helper always sets to the nominal 1/fps. Whatever the real gap held beyond 1/60 s is lost, once per fragment (about once a second).
  • In the take, around 400 s every fragment reads … 1072, 1072, 1000: 72 units (1.2 ms) lost per second while the desktop updates at 56 Hz, none while it is static. That is the 217 ms, and why the loss starts and stops with screen activity instead of growing linearly.
  • Reproduced on a fresh 10-minute take under synthetic load (x264 + NVENC encodes), with the RC's helper: video 599.702 s against audio 600.085 s, 383 ms short. An instrumented build logging the time given to each sample shows the file departing from those times only at samples 61, 122, 183… (the fragment ends), each set to 1000 units whatever the real interval.

Two contributing faults in how the screen is stamped (writeVideoFrames, main.cpp):

  • A new WGC frame carries its SystemRelativeTime, but a repeated one carries last + 1/fps: a count, not the clock. Mixing the two makes the times jitter by a refresh period (the 1000 + 72 pairs in the file).
  • Since fix(windows): keep the screen video on the clock when the writer misses ticks #1056 a late writer repays every missed slot in a back-to-back burst, on the premise that the encoder renumbers frames at the nominal rate. It does not: the hardware encoder with the fragmented sink, the software encoder with the fragmented sink, and the software encoder with the plain-MP4 fallback all keep the sample times (a 25 ms-per-tick stall writes ~35 fps over 10 s and the video still lasts 10 s). The perfectly even 16.667 ms Recording (Windows): keep the video on the clock when the writer misses ticks #945 measured was the last + 1/fps stamping of repeats.

Fix plan

  1. Give every video sample its real duration in MFEncoder::submitVideoSample: hold each sample until the next one arrives, set its SampleDuration to the gap between them, then write it. finalize() writes the last one. One place, so the screen and webcam encoders, the hardware and software paths and both containers all get it.
  2. Stamp every screen and webcam sample with its tick's time on the recording clock (steady clock since the shared start, pauses excluded: the clock the audio mixer and the webcam already follow), new picture or repeat alike. No more last + 1/fps.
  3. Let a late tick leave a gap instead of repaying missed slots in a burst: with (1) the gap is correctly timed and the previous frame plays through it.
  4. Tests: a deterministic test of the slot clock and stamping rule with injected stalls, drops and sustained overload; an encoder test that writes irregular times across fragment boundaries (25 ms ticks, a 300 ms stall) and checks every packet's pts against its stamp.

Activity

  1. added
    status: fixed in mainWork is merged into main but may not be in a downloadable release yet.
    status: pending releaseMerged change is waiting for a packaged desktop release.
    on Oct 13, 2026
  2. added this to the Next Release milestone on Oct 13, 2026
  3. github-actions commented on Oct 13, 2026

    @github-actions
    Contributor

    Fixed by #1165 and merged into main.

    This change is assigned to the Next Release release milestone and is not necessarily available in the latest downloadable desktop release yet. It is currently marked as status: pending release until a packaged release containing it is published.

  4. EtienneLescot commented on Oct 15, 2026

    @EtienneLescot
    CollaboratorAuthor

    Moved into v2.1.0 as part of the pre-release cut.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: fixed in mainWork is merged into main but may not be in a downloadable release yet.status: pending releaseMerged change is waiting for a packaged desktop release.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions