# HANDOFF — 2026-09-12 (webcam FIXED; audio-silence FIXED; TRUNCATION ROOT-CAUSED + FIXED for static scenes) ## Branch / Commit State `main` HEAD = `a62a283`. Ahead of origin by 25 commits. **NOT pushing** — user ruling: no push until web-overlay transparency AND audio-silence are addressed. Uncommitted (all belong to the current work, none committed yet): ``` M .gitignore M HANDOFF.md (this file) M MyMistakes.md (webcam collateral recipe added) M Services/Audio/AudioSyncDelay.cs (audio-silence fix) M Services/Compositor/SceneCompositor.cs (webcam cbW/cbH fix) M Services/MediaCaptureFrameSource.cs (diagnostics REMOVED — clean) M ytLive.Tests/AudioSyncDelayTests.cs (silence regression test, proven both ways) M ytLive.Tests/SceneCompositorTests.cs (webcam regression test, proven both ways) M Services/Encoder/FramePump.cs (slow-render probe, see TRUNCATION below) ``` Build is green (0 warnings); 295 tests pass (audio + compositor + pump suites). ## ✅ TRANSPARENCY — CLOSED (pushed gate #1) User confirmed transparency is fixed (`efe88b8` era). Push gate #1 clears. ## ✅ WEBCAM GRAY BLOCK — CLOSED Root cause: `SceneCompositor.BlitContentRaw` defaulted `cbW=cbH=0` when `src.CropBounds` is null — regression from `ed9d7c1`. Every webcam frame (no crop bounds) blitted to an empty 0×0 source rect → nothing drawn where the webcam should be. Fix: default `cbW=src.Width`, `cbH=src.Height`. Regression test `BlitContentRaw_NoCropBounds_SamplesAcrossFullSource_NotJustOrigin` proven BOTH ways (fails on old code, passes on new). User confirmed correct webcam render in the latest recording. All temp diagnostics stripped from `MediaCaptureFrameSource.cs` and `SceneCompositor.cs`. ## 🟡 AUDIO SILENCE — ROOT-CAUSED AND FIXED, pending final user take **Symptom:** mic/desktop levels showed real values in preview and in the per-5s `Audio live:` telemetry, but `peakMix` stayed exactly `0.000` and saved recordings were digital silence (-91 dB, verified with `ffprobe -af volumedetect`). **Root cause:** `AudioMixer.FillAndMix` calls `_syncDelay.Configure(...)` on EVERY ~10ms tick (it re-reads the live UI setting each mix). The old `AudioSyncDelay.Configure` unconditionally reallocated and zeroed the delay buffer AND reset `_writePos=0` on every call — discarding the just-written audio before the delay offset could ever read it back. With offset 0 the early passthrough masked it; any non-zero saved offset = total silence. The DB has `Audio.SyncOffsetMs = 500` (left over from TASK 22 testing) → that's what triggered it. **Fix:** `Configure` now early-returns when the delay samples haven't actually changed (still reallocates/flushes on a *change*, so the live slider still clicks rather than smearing). Regression test `RepeatedConfigureWithSameDelay_EveryTick_StillDelivers_TheMarker` mirrors the mixer's exact calling pattern and was proven BOTH ways (marker lost = silence on old code; marker survives → expected 960-sample shift on new). **Telemtery proof the fix works:** latest take (ty-20260912-1135-0000-2.mp4, recorded WITH the fix, sync offset still 500): `peakMix` = 0.434–0.523 across all five 5s windows (was 0.000 before). **Audio is NOT silence anymore.** **Follow-up:** sync offset `Audio.SyncOffsetMs` is still in the DB — currently **300** (flipped 500→0→500→300 during lip-sync calibration; user judged 0 worse than 500, so it was bisected back up). NOT the recording's natural state. When the user is ready to finalize, measure on the classic method (clap visible in frame; read delta on the waveform — the auto-correlation approach failed to converge at ~10–14Hz webcam shutter jitter). ## ✅ TRUNCATED VIDEO — ROOT-CAUSED AND FIXED (for baked/static scenes) **Symptom:** recordings ran ~1s short at first, then HALF-length: 22.8s wall → 10.48s file. Not a mux/`-shortest` cut — the pump simply produces frames slower than the declared 60fps. **Mechanism (proven):** ffmpeg rawvideo timestamps frames by ARRIVAL at declared `-r 60`. Pump deadline pacing (OBS libobs pattern) yields each frame when its ~17ms slot lands; if the render stage for that tick takes longer than the slot, the pump can only do ~1 frame per 35ms → ~27–28fps of *submitted* frames → file length ≈ submittedFrames/60. So a take that *should* be 22.8s muxes to ~10.5s, and logs show `avg render 35–38ms` with 136/300 per 5s. FramePump had NO way to tell which compositor pass ate a slow frame. **Root cause of the 35ms:** the scene was NOT actually fully static. Any dynamic element still in the scene — **including hidden ones**, which still count as Dynamic in `SceneGraph.GetSplitPoint` — forces the per-frame composite path. If it sits at split 0, `GetBakedBase` returns null and every tick is a FULL render (no baked base). The 12:59/13:39 takes logged `resolve 1.0ms` per frame — a live element was still resolving, confirming dynamic content was present. **The fix that matters (no code changed in the renderer):** a truly static scene renders in <1ms. New slow-render probe (`ProbeRender` in FramePump.cs) names the path; the 13:53 take of a 1-element static scene logs `render=fully-static split=1 dynamic=0 bakeMs=246 totalMs=246` on the FIRST frame (cold raster of the background at 1080p), then `0.8→0.0ms` avg render, `301/300` frames per 5s, 0 stalls → **12.46s file from 12.29s wall, 735 frames @ 60fps. No truncation.** **Rule for recording:** every scene used for RECORDING should be ≥1 static element first (background image) with all web/cam/chat widgets AFTER it in layer order (split ≥ 1 → bake feasible) — a scene whose FIRST layer is dynamic at recording time will truncate. If a take with widgets comes back truncated again, the probe now points at whether it took `fully-static` (bake), or `base+layers` / `full-render` (composite) and how long. **Probe left in place** (≈0 overhead: one Stopwatch, string built only when a render ≥20ms): it is the diagnostic that named this bug; keep it until the dynamic-scene path is proven fast too, then remove. ## 🔴 OPEN — user-reported "video seems truncated ~1s" (unconfirmed) Recording ty-20260912-1135-0000-2.mp4: format duration 26.370s, video 26.167s (1570 frames @ 60fps), audio 26.370s. FramePump stats for the whole take show `dropped 0` and 300/300 frames every 5s window. End-frames (n=1500 vs 1560 vs 1569) differ — not a frozen/repeat-last-frame truncation. Log shows recording window 11:35:01.386 → 11:35:27.562 = 26.2s of video: matches the file. No evidence of truncation found in data. Possibly user perceived the 500ms audio delay (audio trailing video start / leading video end) as truncation — worth re-checking with offset zeroed. If a real freeze/truncation reappears: pull last-3-frames diffs and confirm_frame timestamps next. ## Other threads (paused) - **Audio-silence verification** — the FIXED take has real peakMix; final = user re-records with the finalized offset and confirms audible audio in playback. - **Audio sync offset final value** — DB is at 300 (bisect point). Needs the clap/waveform measurement; do NOT rely on the 500 left over from TASK 22. - **Truncation with DYNAMIC scenes** — proven mechanism; static scenes now record full-length. If a widget-heavy take truncates again, read the `ProbeRender` path/stage breakdown in startup.log, then optimize composite. - **Webcam MJPG missing** — "MJPG negotiation refused (being used by another process)". Queued. - **Web capture speed** — ~10-14Hz effective. Revisit only on request. - **Layer order** — SortOrder not persisting on drag. Queued. ## Landmines - testhost shares startup.log — filter by time. - `cmd.exe /c "taskkill /F /IM ytLive.exe"` (WSL form double-slashes mangle) before rebuilds. - Build/tests: Windows dotnet host (`/mnt/c/Program Files/dotnet/dotnet.exe`). 0 warnings. - ffmpeg/ffprobe: `/mnt/c/Program Files/Krita (x64)/bin/ffmpeg.exe` with Windows paths. - `Audio.SyncOffsetMs` persisted non-zero WILL re-trigger silence symptoms if `Configure` is ever called unconditionally again — the idempotent guard is the fix, keep it. - `MyMistakes.md` has the transparency RESOLVED entry and the new webcam collateral recipe — grep before any re-derivation.