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:
@@ -46,7 +46,7 @@ public sealed class VideoClipPipe : IDisposable
|
||||
private readonly bool _audio;
|
||||
private readonly ChromaKeyOptions? _key;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private IDisposable? _run;
|
||||
private bool _running;
|
||||
private IDecodeProcess? _activeVideo;
|
||||
private IDecodeProcess? _activeAudio;
|
||||
|
||||
@@ -72,10 +72,13 @@ public sealed class VideoClipPipe : IDisposable
|
||||
_key = chromaKey;
|
||||
}
|
||||
|
||||
/// <summary>Runs one pass (or a loop of passes). A pass that ended naturally leaves the pipe
|
||||
/// restartable — the creator can ask a finished clip to play again, which is what the Loop
|
||||
/// switch and the "Play again" button need.</summary>
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_run != null) return Task.CompletedTask;
|
||||
_run = _cts.Token.Register(StopInternal);
|
||||
if (_running) return Task.CompletedTask;
|
||||
_running = true;
|
||||
_ = Task.Run(RunAsync, CancellationToken.None);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -83,13 +86,16 @@ public sealed class VideoClipPipe : IDisposable
|
||||
public Task StopAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_run = null;
|
||||
Kill(_activeVideo);
|
||||
Kill(_activeAudio);
|
||||
_running = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_running = false;
|
||||
_cts.Dispose();
|
||||
_activeVideo?.Dispose();
|
||||
_activeAudio?.Dispose();
|
||||
@@ -97,6 +103,7 @@ public sealed class VideoClipPipe : IDisposable
|
||||
|
||||
private void StopInternal()
|
||||
{
|
||||
_running = false;
|
||||
Kill(_activeVideo);
|
||||
Kill(_activeAudio);
|
||||
}
|
||||
@@ -129,6 +136,7 @@ public sealed class VideoClipPipe : IDisposable
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_running = false;
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -136,9 +144,11 @@ public sealed class VideoClipPipe : IDisposable
|
||||
// A failed decode is not fatal: the alert layer falls back to its animations,
|
||||
// and a media layer just holds its last frame.
|
||||
AppLog.Write($"Video clip decode failed ({Path.GetFileName(_path)}): {ex.Message}");
|
||||
_running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_running = false;
|
||||
if (!ct.IsCancellationRequested)
|
||||
{
|
||||
AppLog.Write($"Video clip end: {Path.GetFileName(_path)} frames={frames} audioChunks={chunks}");
|
||||
|
||||
Reference in New Issue
Block a user