TASKS.md is now the index (status table, open items, research pointer). 33 files: 32 task files + 1 research facts file. The full take-saga narrative and all design decisions are preserved verbatim; the catalog makes the queue readable without opening every task body. Schema and AGENTS.md updated to reflect the new layout.
6.6 KiB
TASK 21 — Media source (video file playback)
Catalog:
TASKS.md— status and requirements live here.
Goal: play video files (MP4, MOV, AVI) into scenes — starting soon videos, BRB loops, intro/outro clips.
Status: 🔶 In progress — Increment A (model + persistence) + Increment B (decoder) + slice 1 (manager + resolver/preview wire-in) + slice 2a/2b (ffprobe probe + native-FPS pacing) + slice 3 (loop mechanism) shipped. Remaining: the UI picker slice (AddMedia command + file dialog + Acquire/Release wiring + loop-flag wiring) — GUI, handed off to build/verify natively on Windows (see HANDOFF → "UI picker slice — handoff spec").
- ✅
MediaSourceTypeenum:Video,Audio(audio-only files via media source) - ✅
SourceType.MediaSourceaddition to the enum - ✅
MediaSourceModel:MediaPath,MediaIsLooping,MediaVolume(0-1),MediaPlaybackState— persisted in LayoutStore (schema migration + SELECT/INSERT + round-trip test) - ✅
MediaVideoSourceimplementsIMediaFrameSource: FFmpeg raw video decoder →VideoFramepipeline — spawns ffmpeg-f rawvideo -pix_fmt bgra, drains the pipe via pureRawVideoFrameReader(Services/RawVideoFrameReader.cs), raisesFrameAvailable/Completed; lifecycle isStartAsync/StopAsync; decode process behindIDecodeProcess/FfmpegDecodeProcessseam (binary-stdout mirror ofIEncoderProcess). Shipped 2026-08-31; refactored ontoIMediaFrameSourcewith this slice. 4b. ✅ Slice 2a — native-FPS probe seam:FfmpegFrameRateParser(pure, prefersavg_frame_rate=thenr_frame_rate=, rational N/N/M, unknown→null) +IFrameRateProbe/FfmpegFrameRateProbe(derives siblingffprobe.exefrom the located ffmpeg dir, reuses theIDecodeProcessseam; null if ffprobe absent) +FfmpegLocator.ProbeFileNamenow also extractsffprobe.exefrom the pinned archive (conditional). Tests:FfmpegFrameRateParserTests(6 pure units + 1 probe integration via fake locator/process) +FfmpegLocatorTestsstill green — 15/15. - ✅
IMediaFrameSource+MediaVideoSourceManager(Services/IMediaFrameSource.cs,Services/MediaVideoSourceManager.cs): app-wide decode-session ownership refcounted byMediaPathwith aFunc<string, IMediaFrameSource?>factory seam;AcquireAsync/ReleaseAsync/ReleaseAllAsync/GetLatestFrame; coalesces frames onto the UI dispatcher onto a single sharedWriteableBitmapper file (mirror ofScreenCaptureManager);MediaFailed+PreviewBitmapChanged. Unit tests + one integration test (bitmap share/coalesce) —MediaVideoSourceManagerTests, 5/5 pass. - ✅ Native-FPS pacing (slice 2b):
MediaVideoSourcetakes optionalIFrameRateProbe?+Func<TimeSpan,CancellationToken,Task>? delayseams, probes FPS once inRunAsync, and delays by 1/fps after each emitted frame; unknown/absent probe → no pacing. Test:MediaVideoSource_PacesFramesByProbedFps(fake probe + recording delay, one delay per frame ≈ 1ms). - ☐ Wire into
FramePumpresolver —Source { Type: MediaSource }→ latest video frame — resolver + preview routing + manager wired; session acquisition (start/stop on add/remove) still open (comes with the UI picker) - ☐ Wire into
SceneCompositor— render media source as an image element at its position/size - ✅ Loop control (mechanism) —
IMediaFrameSource.Looping;MediaVideoSourcetakes aFunc<IDecodeProcess>process factory and restarts the decode on natural EOF whenLooping(fresh process per pass, since aProcesscan't be re-Start()ed). Test:MediaVideoSource_LoopsUntilLoopDisabled(single frame re-emits across passes,Completedonly after loop cleared). WiringSource.MediaIsLoopinginto the flag lands with the UI-picker (acquisition) slice. - ☐ Volume control — per-source volume slider for audio playback
- ☐ UI: file picker (filtered to video formats), loop toggle, volume slider
- ☐ Schema migration for media source settings (file path, loop, volume)
- ☐ Tests: video frame extraction, loop behavior, volume scaling, file validation
Design decisions
- FFmpeg handles all formats — no codec-specific code. FFmpeg already in the project.
- Audio plays through the desktop channel — media source audio is captured by the WASAPI loopback (like TRAX/game audio). No separate audio routing needed.
- This pairs with TRAX — TRAX is background music, media source is background video. Together they make non-Live scenes (Starting/BRB/Ending) feel polished.
- Not a full NLE — no trimming, no multi-track, no effects. Just "play this video in the scene."
UI picker slice — spec (imported from HANDOFF 2026-09-01 so it can't be lost by a rewrite)
Nothing AcquireAsyncs a media path yet, so no frames flow in a running app — decoder, manager,
pacing, and loop mechanism are shipped and unit-tested, but a media Source has no way to start a
session. Deliverables, in order:
- File picker + add/remove commands (mirror
MainViewModel.Trax.cs:68OpenFileDialogusage). A "Media" source action opens anOpenFileDialogfiltered to video (mp4/mov/avi); on OK setSource.MediaPath, add to scene, andAcquireAsync(MediaPath); on removeReleaseAsync(MediaPath)(refcounted — notReleaseAllAsync, unless the path leaves the layout entirely). OnStagedScenelayout loads/teardown, release every media path no longer present and acquire new ones so sessions track the live layout. - Wire
Source.MediaIsLooping→IMediaFrameSource.Looping— needs a manager-level per-path loop provider. Ambiguity to rule + record: sessions are refcounted perMediaPathand shared across scenes, butMediaIsLoopingis per-Source. Chosen rule: the path's sessionLooping= any live reference has it on (shared behaviour). Record it in the slice commit. - Loop toggle + volume slider in the source context menu / inspector (items 10/11).
MediaVolume/MediaPlaybackStatewiring — audio path for media is future work (desktop loopback already carries it, TRAX-style;MediaVolumeslider can drive the local file's audio separately later — out of this slice).
Design notes to preserve: ResolveOutputFrame reads _mediaManager.GetLatestFrame(MediaPath);
OnMediaPreviewBitmapChanged adopts the shared WriteableBitmap per path; MediaFailed →
OnMediaFailed (currently Debug.WriteLine — surface in UI via toast, TASK 24 stack). The compositor
scales any frame size — media renders through the generic frameFor(element) path, no compositor
change. First GUI smoke test: add a short real mp4 on native Windows, confirm it plays and previews.