fix(rec): recordings played ~1.4x fast — CFR emission, drop -re
Root cause (measured, not guessed): the take-3 'rebase on overrun' reset
nextTick to wall-now, erasing every missed slot. Pump delivered 215-219/300
per 5s (~43fps) but rawvideo carries no timestamps — ffmpeg muxes by frame
count at -framerate 60, so every recording played fast with honest-looking
stats. A second pacer, ffmpeg -re, throttled the demux separately
('Resumed reading ... after a lag' 0.79s->4.82s).
Fix follows libobs video-io.c (https://github.com/obsproject/obs-studio)
- the video thread never resets its deadline; one frame per interval slot,
a late render repeats content (judder), never skips time:
1. FramePump: count-based emission, while(now>=nextTick){Submit; nextTick+=I}
2. FfmpegArgs: -re removed - the pump is the pacer
Muxed duration is now frame-count/fps == wall time by construction. Covers
game background + webcam (one shared pump). 0 warnings; suite 288/289 -
the one failure (Composite_FullScene_MasterPixels 1380,700) also fails with
this change stashed: pre-existing, untouched, recorded as follow-up.
This commit is contained in:
@@ -515,7 +515,9 @@ integration test fakes the whole subprocess (probe + encoder) with a Channel-bac
|
||||
`Complete()` is EOF (`null`), never a `ChannelClosedException`.
|
||||
|
||||
**Decisions (locked):** args are pure (`FfmpegArgs.Build`, no string building in the encoder):
|
||||
`-re -f rawvideo -pix_fmt bgra -video_size WxH -framerate FPS -i pipe:0` + a **real audio input — the
|
||||
`-f rawvideo -pix_fmt bgra -video_size WxH -framerate FPS -i pipe:0` (**NO `-re`** — the FramePump
|
||||
is the pacer since slice 9, 2026-09-10; `-re` added a second, fighting clock on the rawvideo demux)
|
||||
+ a **real audio input — the
|
||||
mixer writes IEEE-float stereo to a Windows named pipe** (`-f f32le -ar 48000 -ac 2
|
||||
-i \\.\pipe\ytllive_audio`, name via `EncoderOptions.AudioPipeName`; replaced the old `-f lavfi -i
|
||||
anullsrc` silence in the TASK 8 audio milestone) + explicit `-map 0:v -map 1:a` + `-c:v <enc> -b:v K
|
||||
@@ -700,7 +702,8 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
258.1ms, avg submit 1.5ms`. Two defects, both fixed: (1) the pump slept the FULL interval after each
|
||||
render, so period = render + interval — OBS's `video_thread` (libobs/media-io/video-io.c) pattern
|
||||
replaces it: absolute `nextTick += intervalTicks` deadline, sleep only the remainder, and on overrun
|
||||
skip the wait AND the missed ticks (rebase, never burst stale frames). (2) the compositor did
|
||||
skip the wait. (NOTE, slice 9: the original "skip the missed ticks (rebase)" half of this fix was
|
||||
wrong — see the slice 9 correction below.) (2) the compositor did
|
||||
per-pixel float sampling + `Math.Round` blending over all 2.07M master pixels (backdrop) and scanned
|
||||
the whole destination per overlay (a 64px social-bar strip cost 2M iterations). `SceneCompositor` now
|
||||
follows the libyuv pattern (BSD-3, chromium.googlesource.com/libyuv/libyuv — cited per the
|
||||
@@ -810,6 +813,27 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
|
||||
reuses a canvas scratch + 8-deep output ring + a reused WriteableBitmap instead of minting two
|
||||
fresh arrays + a new bitmap per 10Hz tick. Next suspect if gen2 stays hot: the WPF preview load
|
||||
itself driving gen2 — recorded as follow-up, untouched this slice.
|
||||
- **Slice 9 — the "rebase" WAS the recording time-lapse (2026-09-10, the recording playback-timing
|
||||
take):** slice 6's "blown deadlines still rebase" was the recording-timing bug all along. In a real
|
||||
take the pump emitted `215-219/300 per 5s` (~43fps) while `render+wait == period` still looked closed —
|
||||
the rebase ERASED every missed slot (deadline = wall-now again), so the missed ticks never showed in
|
||||
the stats. rawvideo has no per-frame timestamps: ffmpeg muxes by frame count at `-framerate 60`, so a
|
||||
43fps reality was authored into a 60fps container and **every recording played ~1.4x fast** (takes
|
||||
confirmed with a WSL ticker visible in the recording: file duration 11.44s vs ~15.5s wall). Two
|
||||
defects, both fixed the OBS way (`libobs/media-io/video-io.c` — the video thread NEVER resets its
|
||||
deadline; every interval tick outputs ONE frame, and a late render means repeated content — judder —
|
||||
never a skipped timestamp):
|
||||
(1) **count-based CFR emission:** `while (GetTimestamp() >= nextTick) { SubmitFrame(frame); nextTick += intervalTicks; statFrames++; }`
|
||||
submits exactly one frame per crossed slot, re-writing the current composite when the render overruns
|
||||
(duplicated footage = correct duration, not a time-lapse), and `nextTick` is NEVER reset to wall-now;
|
||||
(2) **`-re` removed from `FfmpegArgs`** — it was a second, fighting pacer on the rawvideo demux
|
||||
(its "Resumed reading … after a lag" grew 0.79s→4.82s across the take, leaving the pump behind its own
|
||||
honest-stats count). One fix covers the game background AND the webcam — both flow through this one
|
||||
pump. Muxed duration is now frame-count ÷ fps == wall time by construction. Test: the pump suite stays
|
||||
green unchanged — the pacing test's `<interval` wait assertion still holds because the frame emits at
|
||||
the next slot boundary, not immediately. (Unrelated pre-existing failure found the same day:
|
||||
`Composite_FullScene_MasterPixels` pixel (1380,700) cyan-vs-magenta — reproduces with this fix stashed,
|
||||
untouched by it, recorded as a follow-up.)
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder (closes stdin → EOF → ffmpeg finalizes+exits)
|
||||
**before** awaiting the loop, because closing stdin unblocks a write stuck on pipe backpressure — the
|
||||
reverse order would deadlock. `ProcessFailed` self-stops the pump. `Failed` while live flips
|
||||
|
||||
Reference in New Issue
Block a user