TASK 21 slice 1 step 4: wire media into resolver + preview routing

Wire the MediaVideoSourceManager into the live pipeline (new partial
ViewModels/MainViewModel.Media.cs mirrors Webcam/Background partials):
readonly _mediaManager assigned in the core ctor (decode at 1920x1080 master,
dispatcher-coalesced preview), OnMediaPreviewBitmapChanged adopts the shared
bitmap onto every IsMediaSource with a matching MediaPath, a
Source { IsMediaSource, MediaPath } case in ResolveOutputFrame feeds
GetLatestFrame, and the manager is disposed on shutdown.

Source.DisplaySource now routes VideoImageSource for MediaSource (Source.cs),
so media previews/canvas show decoded video.

Compositor needs no change: BlitContent UniformToFill-scales any frame to the
element rect. Decoder/manager/seam derived-work mirrors ScreenCaptureManager
(citation in commit for slice-step-3).

Test (one unit for this change, mirroring the live-capture case):
Source_DisplaySource_IsVideoImageSource_WhenMediaSource. 14/14 media +
DisplaySource tests pass, build 0 warnings. Docs (TASKS/HANDOFF/ai.md) updated.
This commit is contained in:
2026-08-31 19:32:41 -07:00
parent 480ead0fc7
commit 071ec8be0c
7 changed files with 85 additions and 16 deletions
+35
View File
@@ -0,0 +1,35 @@
using System.Linq;
using System.Windows.Media.Imaging;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.ViewModels;
// Media source (TASK 21): decode sessions are owned app-wide by
// MediaVideoSourceManager, refcounted by MediaPath. Modeled on the Webcam/Background
// partials: the readonly manager field is assigned in the core constructor, the
// shared WriteableBitmap published per file is adopted by every media Source whose
// MediaPath matches, and ResolveOutputFrame feeds the live compositor.
public partial class MainViewModel
{
// Decode resolution: the 1920x1080 master. Media frames are scaled to the
// element rect by the compositor (UniformToFill), so this is the capture size.
private const int MediaDecodeWidth = 1920;
private const int MediaDecodeHeight = 1080;
private readonly MediaVideoSourceManager _mediaManager;
// MediaVideoSourceManager publishes one shared WriteableBitmap per file; every
// media Source pointing at that path picks it up for the preview/canvas.
private void OnMediaPreviewBitmapChanged(string path, WriteableBitmap bitmap)
{
foreach (var source in Scenes.SelectMany(s => s.Elements.OfType<Source>())
.Where(s => s.IsMediaSource && s.MediaPath == path))
source.VideoImageSource = bitmap;
}
// Called from the encoder error path is out of scope (TASK 21 sees the decoder
// fail at startup); log to the debug console for now, surfaced via the UI later.
private static void OnMediaFailed(string path, string message)
=> System.Diagnostics.Debug.WriteLine($"MediaSource: '{path}' failed: {message}");
}
+10
View File
@@ -282,6 +282,14 @@ public partial class MainViewModel : ViewModelBase
_screenCaptureManager.CaptureFailed += (key, message) =>
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
_mediaManager = new MediaVideoSourceManager(
path => new MediaVideoSource(
path, MediaDecodeWidth, MediaDecodeHeight,
new FfmpegLocator(), new FfmpegDecodeProcess()),
System.Windows.Application.Current?.Dispatcher);
_mediaManager.PreviewBitmapChanged += OnMediaPreviewBitmapChanged;
_mediaManager.MediaFailed += OnMediaFailed;
_chatLayer = new ChatOverlayLayer(new ChatBoxRenderer());
_sceneGraph = new SceneGraph();
@@ -397,6 +405,7 @@ public partial class MainViewModel : ViewModelBase
_framePump.Dispose();
_cameraManager.Dispose();
_screenCaptureManager.Dispose();
_mediaManager.Dispose();
_webView2Manager?.Dispose();
_layoutStore.Dispose();
_musicPlayer.Dispose();
@@ -452,6 +461,7 @@ public partial class MainViewModel : ViewModelBase
Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey),
Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox),
Source { Type: SourceType.WebSource } web => _webView2Manager?.GetLatestFrame(web.Id),
Source { IsMediaSource: true, MediaPath: not null } media => _mediaManager.GetLatestFrame(media.MediaPath),
Source { AssetId: not null } image => StaticPixelCache.Get(image.AssetId),
_ => null,
};