feat(media): add a video file layer that decodes into the scene

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.
This commit is contained in:
2026-11-05 07:17:43 -08:00
parent 785b5ff8f3
commit 77d7208bfa
11 changed files with 429 additions and 68 deletions
+25
View File
@@ -70,6 +70,8 @@
IsEnabled="{Binding CanAddWebcam}"
ToolTip="{Binding WebcamAddToolTip}"/>
<MenuItem Header="Image" Command="{Binding AddImageCommand}"/>
<MenuItem Header="Video File" Command="{Binding AddMediaCommand}"
ToolTip="Play a video file in the scene (mp4, mov, avi, mkv, webm)"/>
<MenuItem Header="Text" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.TextOverlay}"/>
<MenuItem Header="Countdown" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.Countdown}"
Visibility="{Binding IsCountdownAvailable, Converter={StaticResource BoolToVis}}"
@@ -725,6 +727,29 @@
</DockPanel>
</StackPanel>
<!-- Media (video file) config — TASK 21 -->
<StackPanel Visibility="{Binding SelectedElement.IsMediaSource, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
<TextBlock Text="Video File" Style="{StaticResource PropertyHeader}"/>
<Grid Margin="10,0,0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding SelectedElement.MediaFileDisplay}"
Style="{StaticResource PropertyFieldLabel}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
ToolTip="{Binding SelectedElement.MediaPath}"/>
<Button Grid.Column="1" Content="Change…" Command="{Binding ChangeMediaFileCommand}"
CommandParameter="{Binding SelectedElement}" Style="{StaticResource PropertyButton}"
Margin="4,0,0,0" Padding="8,2" FontSize="11"/>
</Grid>
<CheckBox Style="{StaticResource PropertyPillCheck}" Margin="10,4,0,4"
Content="Loop" IsChecked="{Binding SelectedElement.MediaIsLooping, Mode=TwoWay}"
ToolTip="Restart from the beginning at the end of the file — for starting-soon and BRB loops"/>
<TextBlock Text="Audio plays through your desktop audio, like TRAX."
Style="{StaticResource PropertyHint}"/>
</StackPanel>
<!-- Chat box config -->
<StackPanel Visibility="{Binding SelectedElement.IsChatBox, Converter={StaticResource BoolToVis}}" Margin="0,0,0,8">
<TextBlock Text="Chat Overlay" Style="{StaticResource PropertyHeader}"/>