TASK 21's picker slice: the decoder, manager, pacing and loop mechanism were
already shipped but nothing ever acquired a session, so a media Source rendered
nothing. Adds "Video File" to the (+) menu (video-filtered OpenFileDialog),
one decode session per distinct path with a HasSession guard, release when the
last scene reference goes, and a post-load reconcile. Loop is a manager-level
per-path answer ("any live reference loops") applied before StartAsync and
updated live via SetLooping. Properties panel shows the file with Change… and
Loop. Volume stays unwired: media audio rides the desktop loopback, TRAX-style.
Reference: OBS's media source (file → decode → composite into the scene) is the
established answer for this feature; the lifecycle shape mirrors this repo's
WebView2/ScreenCapture registration rather than inventing a new one.
Test: ytLive.Tests/MediaSourceAcquisitionTests.cs — add starts one decoder and
frames reach the layer, loop flip reaches the decoder, remove stops it.
Docs: task-21 closed, TASKS row + Day-4 row + stale 3·17 row corrected,
frame-pipeline acquisition note, HANDOFF rewritten. verify.sh green.
5.1 KiB
5.1 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. Audio is the desktop loopback (TRAX-style); a per-file volume route is not built.
- ✅
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. - ⏸ Volume control — deliberately not wired: media audio is captured by the desktop loopback like TRAX, so a per-file slider would be a control with no audible effect. Revisit only with a local audio route for the file's own stream.
- ✅ 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 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."
- 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.