From a12cac0738b7af5ed160363011a4426c6fe7a00d Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Thu, 17 Sep 2026 09:42:45 -0500 Subject: [PATCH] Wait for a seek from the end to take effect before playing Pressing play on a clip that has finished often left the video frozen on its first frame while the transport reported playback. Seeking back from the end and pressing play was worse, and froze almost every time. Flyleaf's Play() returns without doing anything while the player's status is Ended. A seek issued from Ended only moves the status back to Paused later, from the background task that performs the seek. PlayAsync handles a finished clip by seeking to the start and playing back to back, so Play() usually lands before that task has run, is dropped, and the controller then reports playback that never started. FlyleafCameraPlayer.SeekAsync now notes whether the player was Ended when the seek was issued, and if so waits for the status to leave Ended before returning. Player.Status reads the live field rather than a dispatcher-updated copy, so the flip is visible the moment the seek task makes it, and the wait is normally a few milliseconds. A 1s cap keeps a seek that never runs from stalling the caller. Seeks from any other state return immediately, exactly as before. Measured on the real stack with FlyleafLib 3.11.5, 30 iterations each, requiring the player's own clock to advance rather than trusting IsPlaying: play after the clip finishes 16/30 before 30/30 after seek back from the end, then play 0/30 before 30/30 after seek just before the end, play immediately 15/30 before 30/30 after play from just before the end 30/30 before 30/30 after The bug predates this change and the Flyleaf upgrades: with the FlyleafLib 3.10.4 that shipped in v1.0.0, the first two froze 8 of 20 and 16 of 20. The integration harness passes 140/140 with the fix, including a replay check that now requires movement, since the old one passed on a clip frozen at zero. This is Flyleaf behavior, which the fakes in the unit suite do not model, so it has no unit test. 374 tests green. --- SentryDeck/Playback/FlyleafCameraPlayer.cs | 25 +++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/SentryDeck/Playback/FlyleafCameraPlayer.cs b/SentryDeck/Playback/FlyleafCameraPlayer.cs index 9409258..abdef0f 100644 --- a/SentryDeck/Playback/FlyleafCameraPlayer.cs +++ b/SentryDeck/Playback/FlyleafCameraPlayer.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Diagnostics; using System.Windows.Media; using FlyleafLib; using FlyleafLib.Controls.WPF; @@ -115,17 +116,28 @@ public Task CloseAsync() return Task.Run(StopAndClose); } - public Task SeekAsync(TimeSpan position, bool accurate = true) + /// + /// How long a seek issued from the end of a clip waits for Flyleaf to leave its Ended state before returning anyway. + /// The flip normally lands within a few milliseconds; the cap only keeps a seek that never runs from stalling the caller. + /// + private static readonly TimeSpan LeaveEndedTimeout = TimeSpan.FromSeconds(1); + + public async Task SeekAsync(TimeSpan position, bool accurate = true) { ThrowIfDisposed(); if (!_isOpen) { - return Task.CompletedTask; + return; } var milliseconds = (int)Math.Clamp(position.TotalMilliseconds, 0, int.MaxValue); + // Flyleaf's Play() silently does nothing while the player is Ended, and a seek issued from the end only moves it back to Paused later, from the background task that performs the seek. + // Returning before that happens lets the very next Play() land on Ended and be dropped, leaving the video frozen while the transport reports playback. + // That is exactly what pressing play on a finished clip does, since PlayAsync seeks to the start and plays back to back. + var seekingFromEnded = _player.Status == Status.Ended; + if (accurate) { _player.SeekAccurate(milliseconds); @@ -137,7 +149,14 @@ public Task SeekAsync(TimeSpan position, bool accurate = true) _player.Seek(milliseconds, forward: false); } - return Task.CompletedTask; + if (seekingFromEnded) + { + var started = Stopwatch.GetTimestamp(); + while (!_isDisposed && _player.Status == Status.Ended && Stopwatch.GetElapsedTime(started) < LeaveEndedTimeout) + { + await Task.Delay(5); + } + } } ///