5.8 KiB
5.8 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: ✅ Complete 2026-11-05 — a video file added in the (+) menu decodes, composites into the
preview and the stream, loops on the creator's toggle, and stops when the layer or its last scene
reference goes away. Picture only: the file's own audio is not played anywhere (the decode pipe
passes -an), so a video with a soundtrack is silent on stream — the audio route is open, item 10.
- ✅
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/HasSession/GetActiveKeys/SetLooping; coalesces frames onto the UI dispatcher onto a single sharedWriteableBitmapper file (mirror ofScreenCaptureManager);MediaFailed+PreviewBitmapChanged. Unit tests + one integration test (bitmap share/coalesce) —MediaVideoSourceManagerTests. - ✅ 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. - ✅ Wire into
FramePumpresolver —Source { Type: MediaSource }→ latest video frame, and the same resolver serves preview and thumbnails (FramePumpdefaultsresolverto the frame resolver). - ✅ Wire into
SceneCompositor— media renders through the genericframeFor(element)path at the element's rect, so no compositor change was needed. - ✅ Loop control —
IMediaFrameSource.Looping;MediaVideoSourcerestarts the decode on natural EOF whenLooping(fresh process per pass).MediaIsLoopingreaches the session through the manager's per-pathloopProviderat start andSetLoopingon a live toggle. Rule (recorded): the path's session loops if any live media reference has it on, because one decoder is shared per path. - ⏸ File audio + volume — the file's soundtrack is silent everywhere today: the decode pipe
passes
-an, so nothing plays it and the desktop loopback has nothing to carry. Two established routes when this is taken: alert-style (a secondf32lepipe into the mixer —AlertClipDecoder.RunAudioAsyncis the working model, independent of desktop capture) or TRAX-style (MusicPlayerplays it to the default device, so the loopback, the desktop fader and ducking all apply — at the cost of a second clock to hold in sync with the video). Product call; not started, and no volume slider until one of them exists. - ✅ UI:
AddMediaCommand(+ menu → "Video File", video-filteredOpenFileDialog),ChangeMediaFileCommand("Change…" in the properties panel), loop toggle, video name display. - ✅ Schema migration for media source settings (file path, loop, volume) — shipped with item 3.
- ✅ Tests: decode/loop/pacing at the unit level, and
MediaSourceAcquisitionTests(the Good Dog for the wiring): add a file → one decoder starts, frames reach the layer, loop flip reaches the decoder, remove → stops.
Design decisions
- FFmpeg handles all formats — no codec-specific code. FFmpeg already in the project.
- Audio is NOT wired, and it is not hiding behind the loopback —
MediaVideoSourcedecodes with-an, so a video file's soundtrack is silent on stream today (the loopback only carries what the machine actually plays). Item 10 records the two established routes; do not describe media as audible until one is built. - 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."
- Layers are defined in Live first (creator 2026-11-05): this unit adds the layer and the picker in the ordinary (+) menu, with no per-scene availability rules. Which scenes may host a video layer is a later pass, and scene-only layers (an ending credit roll, for instance) are defined after that pass.