From 3ba1b3f8d222d3c4d37fdadc0db9d65a6e74f82d Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Tue, 15 Sep 2026 20:21:25 +0200 Subject: [PATCH 1/2] Fence FFmpeg frame publication by seek serial --- .../Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs index 4754bc0d0a..68fe24a89a 100644 --- a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs +++ b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs @@ -359,7 +359,7 @@ public void Dispose() CloseFile(); } - private void Present(VideoFrame frame, VideoFrameQueue pool) + private void PublishFrame(VideoFrame frame, VideoFrameQueue pool) { VideoFrame? previous; lock (_currentFrameLock) @@ -370,6 +370,10 @@ private void Present(VideoFrame frame, VideoFrameQueue pool) pool.Return(previous); Interlocked.Increment(ref _frameVersion); + } + + private void NotifyFrameReady() + { FrameReady?.Invoke(); } @@ -405,6 +409,11 @@ or AVSampleFormat.AV_SAMPLE_FMT_DBLP or AVSampleFormat.AV_SAMPLE_FMT_S64P; } + internal static bool CanPublishVideoFrame(int frameSerial, int currentSerial) + { + return frameSerial == currentSerial; + } + private static double TimestampToSeconds(long timestamp, AVRational timeBase) { return timestamp == ffmpeg.AV_NOPTS_VALUE ? double.NaN : timestamp * ffmpeg.av_q2d(timeBase); @@ -1782,15 +1791,25 @@ private void ReachEnd() private void ShowFrame(VideoFrame frame) { - var popped = _videoFrames.Pop(); - if (!ReferenceEquals(popped, frame)) - { - _videoFrames.Return(popped); - return; - } - + var published = false; lock (_seekLock) { + // The initial PresentLoop check is only advisory: a successful seek can advance + // _currentSerial before this frame is popped. Claim and publish the frame while + // holding the same lock that commits seek serials, so an old serial can never be + // published after a newer seek has committed. + if (!CanPublishVideoFrame(frame.Serial, _currentSerial)) + { + return; + } + + var popped = _videoFrames.Pop(); + if (!ReferenceEquals(popped, frame)) + { + _videoFrames.Return(popped); + return; + } + if (frame.Serial > _restartSerial) { _restartSerial = frame.Serial; @@ -1803,12 +1822,18 @@ private void ShowFrame(VideoFrame frame) { _pausedPosition = frame.Pts; } + + // Publish frame state before releasing the seek lock. The callback itself is + // deliberately deferred until after the lock so UI/user code never runs under it. + Interlocked.Exchange(ref _lastRestartTimestamp, Stopwatch.GetTimestamp()); + _owner.PublishFrame(frame, _videoFrames); + published = true; } - // Timestamp after the serial so HasPlaybackRestartedSince never sees a new - // timestamp with an old serial. - Interlocked.Exchange(ref _lastRestartTimestamp, Stopwatch.GetTimestamp()); - _owner.Present(frame, _videoFrames); + if (published) + { + _owner.NotifyFrameReady(); + } } // ---------------------------------------------------------------- teardown From 10c80c025d96d3b079e0bf279d05c8265c925071 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Tue, 15 Sep 2026 20:21:34 +0200 Subject: [PATCH 2/2] Test FFmpeg frame publication serial fence --- tests/UI/Logic/FfmpegPlayerTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/UI/Logic/FfmpegPlayerTests.cs b/tests/UI/Logic/FfmpegPlayerTests.cs index fcca6e1559..19671df14b 100644 --- a/tests/UI/Logic/FfmpegPlayerTests.cs +++ b/tests/UI/Logic/FfmpegPlayerTests.cs @@ -106,6 +106,14 @@ public void VideoFrameQueue_PeekSecond_SeesTheFrameBehindTheHead() queue.Close(); } + [Theory] + [InlineData(7, 7, true)] + [InlineData(7, 8, false)] + public void CanPublishVideoFrame_RequiresCurrentSeekSerial(int frameSerial, int currentSerial, bool expected) + { + Assert.Equal(expected, FfmpegPlayer.CanPublishVideoFrame(frameSerial, currentSerial)); + } + [Fact] public void VideoFrameQueue_SizeChange_DropsOldPool() {