From 6cfde218dfd95e43e7e12077178e95b6969a57c4 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Tue, 15 Sep 2026 23:55:01 +0200 Subject: [PATCH] Fence FFmpeg close cleanup by load generation --- .../Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs | 21 ++++++++++----- tests/UI/Logic/FfmpegPlayerTests.cs | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs index eba791906eb..9acb44f0052 100644 --- a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs +++ b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs @@ -205,24 +205,33 @@ public Task LoadFile(string fileName, double startPositionSeconds = 0) public void CloseFile() { - Interlocked.Increment(ref _loadGeneration); + var generation = Interlocked.Increment(ref _loadGeneration); var session = Interlocked.Exchange(ref _session, null); _fileName = string.Empty; session?.Dispose(); + // Dispose can wait on stubborn workers. If another load became current meanwhile, this + // older close must not clear the newer session's decoder badge or presented frame. + TryClearOwnerMediaStateForGeneration(generation); + } + + internal bool TryClearOwnerMediaStateForGeneration(int loadGeneration) + { lock (_currentFrameLock) { - // Any worker that survives the teardown timeout belongs to an older load generation - // and may no longer publish UI state after this point. - _decoderName = string.Empty; + if (loadGeneration != Volatile.Read(ref _loadGeneration)) + { + return false; + } - // The frame belonged to the session's pool, which is gone now. + _decoderName = string.Empty; _currentFrame?.Dispose(); _currentFrame = null; + Interlocked.Increment(ref _frameVersion); } - Interlocked.Increment(ref _frameVersion); FrameReady?.Invoke(); + return true; } public void Play() diff --git a/tests/UI/Logic/FfmpegPlayerTests.cs b/tests/UI/Logic/FfmpegPlayerTests.cs index 6416aa0a64a..546e722c0c8 100644 --- a/tests/UI/Logic/FfmpegPlayerTests.cs +++ b/tests/UI/Logic/FfmpegPlayerTests.cs @@ -247,6 +247,33 @@ public void StaleSessionCannotOverwriteDecoderBadgeAfterClose() Assert.Equal("ffmpeg", player.Name); } + [Fact] + public void StaleCloseCleanupCannotClearNewerGenerationMediaState() + { + using var player = new FfmpegPlayer(); + var queue = new VideoFrameQueue(1); + var serial = 0; + + player.CloseFile(); // generation 0 -> 1 + + Assert.True(player.TrySetDecoderNameFromSession(1, "current-hardware")); + var frame = queue.Rent(4, 4, 0, ref serial)!; + Assert.True(player.TryPresentFrameFromSession(1, frame, queue)); + var currentVersion = player.FrameVersion; + + Assert.False(player.TryClearOwnerMediaStateForGeneration(0)); + Assert.Contains("current-hardware", player.Name); + Assert.Equal((4, 4), player.CurrentFrameSize); + Assert.Equal(currentVersion, player.FrameVersion); + + Assert.True(player.TryClearOwnerMediaStateForGeneration(1)); + Assert.Equal("ffmpeg", player.Name); + Assert.Equal((0, 0), player.CurrentFrameSize); + Assert.Equal(currentVersion + 1, player.FrameVersion); + + queue.Close(); + } + [Fact] public void StaleSessionFrameIsReturnedInsteadOfPublishedAfterClose() {