fix(media): a played-out clip can play again; play button beside the Video File label

Reported 2026-11-05: "when I click Loop, the video doesn't play in the preview pane". Two
causes, both real:

- `VideoClipPipe.StartAsync` bailed out once it had run once (`_run != null`), so a clip that
  reached EOF could never be started again — it held its last frame forever.
- Loop is only consulted at end-of-file by a LIVE decoder, so flipping it on a finished clip
  did nothing at all.

Now: the pipe reports running state and is restartable after a natural end; the manager marks a
session finished (keeping its last frame) and exposes `IsRunning`/`RestartAsync`; the layer shows
`IsMediaRunning`/`IsMediaFinished`; flipping Loop on a finished clip restarts it.

UI: `▶ Play` sits to the right of the **Video File** label and is enabled only while Loop is off
(creator ruling: with Loop on the clip already runs continuously, so there is nothing to start).
The panel says "Clip finished — holding the last frame." so a frozen picture explains itself.

Also: verify.sh no longer runs physical-input tests by default. That test moves the real cursor
(SetCursorPos + mouse_event); running it mid-gate hijacks the machine someone is using, which is
exactly how it felt. Opt in with PHYSICAL_TESTS=1 when the desktop is idle; the header documents
the scoped-run command for working hours.

Test: `MediaPlaybackRestartTests` (pipe restart, finished state, Play again, Loop-on-finished).
8 scoped tests green. FULL SUITE NOT RUN — the machine was in use; run scripts/verify.sh in a
quiet window before the next milestone.
This commit is contained in:
2026-11-05 09:15:30 -08:00
parent e37215d9c4
commit 242a13ca30
8 changed files with 327 additions and 9 deletions
+6
View File
@@ -192,6 +192,8 @@ public partial class MainViewModel : ViewModelBase
public ICommand ChangeMediaFileCommand { get; }
/// <summary>Accepts the detector's green/blue-screen offer on a clip (TASK 64).</summary>
public ICommand AcceptChromaSuggestionCommand { get; }
/// <summary>Starts a finished clip again from the top.</summary>
public ICommand ReplayMediaCommand { get; }
public ICommand RemoveSourceCommand { get; }
public ICommand EditElementCommand { get; }
public ICommand ToggleElementVisibilityCommand { get; }
@@ -268,6 +270,9 @@ public partial class MainViewModel : ViewModelBase
ChangeMediaFileCommand = new RelayCommand(
element => ChangeMediaFile(element as Source),
element => element is Source { IsMediaSource: true });
ReplayMediaCommand = new RelayCommand(
element => ReplayMedia(element as Source),
element => element is Source { IsMediaSource: true });
AcceptChromaSuggestionCommand = new RelayCommand(
element => AcceptChromaSuggestion(element as Source),
element => element is Source { HasChromaSuggestion: true });
@@ -425,6 +430,7 @@ public partial class MainViewModel : ViewModelBase
_mediaManager.PreviewBitmapChanged += OnMediaPreviewBitmapChanged;
_mediaManager.MediaFailed += OnMediaFailed;
_mediaManager.AudioAvailable += OnMediaAudioAvailable;
_mediaManager.SessionCompleted += OnMediaSessionCompleted;
_chatLayer = new ChatOverlayLayer(new ChatBoxRenderer());
_alertLayer = new AlertOverlayLayer(