Files
LlamaCasty/HANDOFF.md
T
gramps 097dd0d9bd docs: hand off TASK 21 UI picker slice (acquisition + loop wiring spec)
All headless-testable TASK 21 decoder/mechanism slices shipped; the remaining UI
picker slice (AddMedia command + file dialog + Acquire/Release + Source.MediaIsLooping
-> IMediaFrameSource.Looping) is a GUI feature and is spec'd in HANDOFF for native
Windows build + verification. TASKS status + HANDOFF updated.
2026-08-31 20:17:09 -07:00

7.7 KiB

HANDOFF — Session State

Branch / Commit State

main, working through TASK 21 (media source). Local commits ahead of origin/main (not yet pushed — user pushes at agreed checkpoints):

  • a1d9751 — docs: ffmpeg decode-contract verification recipe + WSL CLR boundary
  • 8f49010 — TASK 21 Increment B: FFmpeg rawvideo video decoder -> VideoFrame
  • f89b9f9 — docs: default-device audio assumption (README + startup log)
  • e8ff4df — TASK 22: global audio sync offset (positive delay at the mixer out)

Prior milestones pushed through origin/main: TASK 31 (3bf053a..670fe3a), plus the full refactor series (Commit F 3107f92, Commit G 85893ea).

What's In Flight

TASK 21 (media source) — all headless-testable decoder/mechanism slices shipped (slice 1 + 2a + 2b + 3); the remaining UI-picker (acquisition) slice is handed off below — it is a GUI feature to build + verify on native Windows.

Increment A (the Source model: MediaSourceType, SourceType.MediaSource, MediaPath/MediaIsLooping/MediaVolume/MediaPlaybackState + LayoutStore persistence) and Increment B (the decoder MediaVideoSource) shipped earlier.

This session:

  • Refactored Services/MediaVideoSource.cs onto the new Services/IMediaFrameSource.cs (Key, FrameAvailable, Completed, StartAsync/StopAsync; Start→StartAsync, FrameReady→FrameAvailable) — behavior preserved, MediaVideoSourceTests updated and passing.
  • Added Services/MediaVideoSourceManager.cs — app-wide decode-session ownership refcounted by MediaPath, Func<string, IMediaFrameSource?> factory seam, AcquireAsync/ReleaseAsync/ReleaseAllAsync/GetLatestFrame, frames coalesced onto the UI dispatcher onto one shared WriteableBitmap per file, MediaFailed + PreviewBitmapChanged. Structural mirror of ScreenCaptureManager.
  • Added ytLive.Tests/MediaVideoSourceManagerTests.cs (5 unit + 1 integration: single shared bitmap, coalesce-to-latest). Verified: all 10 media tests pass (5 manager + 3 reader + 1 decoder integration + 1), build 0 warnings.

Slice 1 step 4 (wire-in) shipped: _mediaManager wired in the core ctor (path => new MediaVideoSource(path, 1920, 1080, new FfmpegLocator(), new FfmpegDecodeProcess()), dispatched preview), OnMediaPreviewBitmapChanged sets VideoImageSource on every IsMediaSource with a matching MediaPath, a Source { IsMediaSource, MediaPath } case in ResolveOutputFrame feeds _mediaManager.GetLatestFrame, Source.DisplaySource routes VideoImageSource for MediaSource, and the manager is disposed on shutdown. Lives in the new ViewModels/MainViewModel.Media.cs partial. Verified: 14/14 media + DisplaySource tests pass, build 0 warnings.

Next (slice 1 step 4's remaining open end): no session acquisition yet — nothing AcquireAsyncs a media path, so no frames flow until the UI picker (a later slice) starts a session on add and releases on remove.

Slice 2a shipped: the native-FPS probe seam (not yet wired into pacing). Services/FfmpegFrameRateParser.cs (pure — prefers avg_frame_rate= then r_frame_rate=, rational N/N/M, unknown→null), Services/FrameRateProbe.cs (IFrameRateProbe + FfmpegFrameRateProbe — derives sibling ffprobe.exe from the located ffmpeg dir, reuses the IDecodeProcess seam for the ffprobe subprocess text, null if ffprobe absent), and FfmpegLocator.ProbeFileName (ffprobe.exe) + conditional extraction alongside ffmpeg.exe. Tests: ytLive.Tests/FfmpegFrameRateParserTests.cs (6 pure units + 1 probe integration via fake locator/process); FfmpegLocatorTests still green — 15/15 pass, build 0 warnings. Slice 2b shipped too: MediaVideoSource now takes optional IFrameRateProbe? + Func<TimeSpan,CancellationToken,Task>? delay seams, probes FPS once in RunAsync, and paces each emitted frame by 1/fps (unknown/ absent probe → no pacing). New test MediaVideoSource_PacesFramesByProbedFps (fake probe + recording delay, one delay per frame ≈1ms). Media tests 5/5, build 0 warnings.

Next (slice 3): loop control — SHIPPED. MediaVideoSource takes a Func<IDecodeProcess> process factory (a single Process can't be re-Start()ed, so each loop pass creates a fresh decoder) and wraps the decode in a do…while (Looping) restart, keyed on the new IMediaFrameSource.Looping flag; Production wiring in MainViewModel (media factory) now passes the factory AND FfmpegFrameRateProbe (closing the slice-2b gap where production had no probe → no pacing). Loop test MediaVideoSource_LoopsUntilLoopDisabled (single frame re-emits, Completed only when loop cleared) — media tests 12/12 (6 source + 6 manager), build 0 warnings. Open: wiring Source.MediaIsLooping into IMediaFrameSource.Looping needs a manager-level per-path loop provider — lands with the UI picker (acquisition) slice.

Landmine: a stale testhost can lock ytLive.Tests's ytLive.dll and break dotnet build of the test project — kill it first (cmd.exe /c "taskkill /PID N /F") on MSB3027. Full-suite vstest hangs headless (RealAppHost/WASAPI) — only filtered pure tests run in WSL; user verifies the GUI suites on native Windows PowerShell (which may also hang if WASAPI startup blocks — pre-existing, not this change).

Next

  1. (Handed off) UI picker slice — the last TASK 21 work unit. GUI feature; build + verify natively on Windows.
  2. Await user push checkpoint for the pending commits (TASK 22 + TASK 21 slices below).

UI picker slice — handoff spec (2026-08-31)

Nothing AcquireAsyncs a media path yet, so no frames flow in a running app — the decoder, manager, pacing, and loop mechanism are all shipped and unit-tested, but a media Source has no way to start a session. Deliverables, in order:

  1. File picker + add/remove commands (mirror MainViewModel.Background.cs/Trax.cs OpenFileDialog usage — see MainViewModel.Trax.cs:68). A "Media" source action opens an OpenFileDialog filtered to video (mp4/mov/avi); on OK set Source.MediaPath, add to scene, and AcquireAsync(MediaPath); on remove ReleaseAllAsync(MediaPath). On StagedScene layout loads/teardown, call ReleaseAllAsync for every media path no longer present (and acquire new ones) so sessions track the live scene.
  2. Wire Source.MediaIsLooping → IMediaFrameSource.Looping — needs a manager-level per-path loop provider (see ambiguity: sessions are refcounted per MediaPath and shared across scenes, but MediaIsLooping is per-Source; a scene set → the path's session Looping = any live reference has it on, so one scene turning it on while another shares the file is shared behaviour — pick a rule and record it).
  3. Loop toggle + volume slider in the source context menu / inspector (TASKS 11/10).
  4. MediaVolume/MediaPlaybackState wiring (audio path for media is future work).

Design notes to preserve: ResolveOutputFrame reads _mediaManager.GetLatestFrame(MediaPath); OnMediaPreviewBitmapChanged adopts the shared WriteableBitmap per path; MediaFailed → OnMediaFailed (currently Debug.WriteLine, surface in UI). Composer scales any frame size. Tmpl: the real-ffmpeg decode/loop/restart path is only exercised natively on Windows — first GUI smoke test should add a short mp4, confirm it plays and previews, then crash further.

The directive (2026-08-31, user)

Rewrite the project into functional components to aid AI retrieval — Services/ChatOverlayLayer.cs / SceneGraph.cs style (owner-state extraction), not line-count chasing. Audio assumption is now an explicit contract (README "Audio Assumption"): the app uses system defaults, never fights Windows device locking, and does not debug user audio issues — OS's problem, not ours.