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:
2026-09-10 08:35:30 -07:00
parent d212d5ae2c
commit bd396e488c
5 changed files with 117 additions and 62 deletions
+41 -29
View File
@@ -1,46 +1,58 @@
# HANDOFF — 2026-09-08
# HANDOFF — 2026-09-10
## Branch / Commit State
**`main` HEAD = `081e4c1`** — take-24 (PasteKey CropBounds). Working tree carries the
real fix (uncommitted): pre-parse transparency injection + diagnostics.
`main` HEAD = `1a39b09`, ahead of origin by 12, working tree carries THE recording-timing
fix (uncommitted): count-based CFR emission in `FramePump.cs` + `-re` removal in
`FfmpegArgs.cs`, plus this handoff / `ai.md` slice 9 / `MyMistakes.md` recipe correction.
`tools/` holds `ticker.c` + the built `ticker` (WSL, unused by git yet).
## Web Overlay — the ROOT CAUSE (finally named, not guessed)
## Recording TIMING — the fix (the "plays too fast" saga)
WebView2's `CapturePreviewAsync` **always honors the page's own background** — the widget
page paints html/body opaque, so the captured PNG has **no alpha-0 margins, ever**.
`DefaultBackgroundColor=Transparent` only shows through pages without a background style.
All prior takes (19-24) assumed the capture had transparent margins — it never did.
`FindContentBounds` therefore had nothing to crop to, and every blend saw alpha=255 =
black box + "transparency broken" + lost resizing. One bug, three symptoms.
**Root cause, proven by measurement (not guessed):** the pump's take-3 "rebase on overrun"
reset `nextTick` to wall-now every time it fell behind, silently erasing missed slots. The
pump delivered `215-219/300 per 5s` (~43fps) but ffmpeg muxes rawvideo by frame count at
`-framerate 60` — no per-frame timestamps — so every recording played ~1.4x fast with
stats that looked honest (a rebased frame is never "late"). `-re` on the demux was a second
fighting pacer ("Resumed reading … after a lag" grew 0.79s→4.82s).
**Fix applied (verify by running):**
1. `WebView2Manager.InitializeAsync`: `AddScriptToExecuteOnDocumentCreatedAsync(TransparentBackgroundScript)`
— injects html/body `background:transparent` BEFORE the page parses/scripts run (the
OBS user.css equivalent). NavStarting/NavCompleted keep the script as post-load re-assert.
2. Restored the take-23/24 compositor crop path (stride-correct `BlitContentRaw` +
`CropBounds` in PasteKey) — my previous take-25 gutting was wrong.
3. Diagnostics: first capture per source dumps the RAW WebView2 PNG to
`%TEMP%\ytLive-web-<id>.png` and logs alpha stats + FindContentBounds result to
startup.log.
**Fix (the OBS `video-io.c` shape — one frame per interval slot, deadline never reset):**
1. `FramePump.PumpAsync`: `while (now >= nextTick) { SubmitFrame(frame); nextTick += intervalTicks; }`
— one submit per crossed slot; a slow render re-writes the current composite (judder,
never a skip). `nextTick` NEVER rebases to wall-now.
2. `FfmpegArgs`: `-re` deleted from the rawvideo input; the pump is the pacer.
## NEXT STEP (user run required once)
**Verified:** build 0 warnings; full suite 288/289 — the sole failure
(`Composite_FullScene_MasterPixels` line 109, pixel (1380,700) cyan vs magenta) ALSO fails
with this fix stashed, i.e. pre-existing and untouched by it. DO NOT fix it here; it is a
separate compositor investigation.
Run the app with the web overlay, then:
- Read `%APPDATA%\ytLlive\startup.log` — the `alpha[min=..,max=..,mean=..,zero=..%]`
line PROVES whether the capture is transparent now (mean near 0 + high zero% + a tight
contentBounds = fixed) or still opaque (mean ~255 → injection didn't beat the page).
- Agent can PIL-analyze `%TEMP%\ytLive-web-<id>.png` for the true alpha bbox.
## NEXT STEP (ONE user run required)
Record ~30s with the WSL ticker visible in the preview (`cd tools && ./ticker` in a terminal,
Ctrl-C to stop), then check:
- recording duration ≈ wall time (should be, by frame-count construction)
- `%APPDATA%\ytLlive\startup.log` "FramePump stats:" shows ≈ n/n per 5s, n = 300 @60fps
- NO "Resumed reading … after a lag" lines remain
- ticker advances ~1s per second of footage
## Still Open
- Chat overlay missing from recording (separate issue — not yet touched this session)
- Audio/sync: non-event (headset volume), closed
- Web overlay: the pre-parse transparency injection (committed `1a39b09`) is NOT yet proven
— both recorded diagnostic PNGs (`%TEMP%\ytLive-web-<id>.png`) were 100% alpha=0 AND
RGB=0 (an EMPTY capture, not an opaque one). Diagnosis "capture is empty" needs a fresh
run; the first-capture dump may be the pre-load about:blank frame. Frozen pending the
timing verification.
- Audio silence: `no audio.mp4` measured -91dB (digital silence), 3KiB muxed audio stream —
the named-pipe audio delivered ~nothing. Separate from timing; not yet touched.
- Pre-existing: `Composite_FullScene_MasterPixels` (see above).
## Landmines
- testhost shares startup.log with app — filter by time when triaging
- testhost/exe lock DLLs: `taskkill /F /IM testhost.exe /IM ytLive.exe` before rebuild
- Do NOT run full-suite vstest (WASAPI hang); flow = clean build + per-class + scope-check
- Build via the Windows dotnet host: `/mnt/c/Program Files/dotnet/dotnet.exe build …`
- Kill app before build: `/mnt/c/Windows/System32/taskkill.exe /F /IM ytLive.exe`
- The PNG dump is written ONCE per session per source (DebugPngWritten flag)
- No Linux ffmpeg / no sudo on this box — probing uses `/mnt/c/Program Files/Krita (x64)/bin/ffmpeg.exe`
via WSL interop; frames landed in `/mnt/c/Users/gramp/AppData/Local/Temp/ylf/`
- `tools/ticker`: constant ~+982ms offset on the FIRST line is cosmetic (t0 captured a beat late)