From a8f1d0e4064c8292a1b73d36d47bdd58c2792ecc Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Sun, 13 Sep 2026 19:55:12 +0200 Subject: [PATCH] FFmpeg: prevent stale asynchronous loads from publishing --- .../Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs | 73 +++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs index fa70455f00..cad01b3d68 100644 --- a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs +++ b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs @@ -45,6 +45,8 @@ public sealed unsafe class FfmpegPlayer : IVideoPlayer, IDisposable private string _fileName = string.Empty; private bool _disposed; + private readonly Lock _loadLock = new(); + private int _loadGeneration; private Session? _session; private double _volume = 100; private double _speed = 1.0; @@ -130,16 +132,24 @@ public bool CopyCurrentFrame(IntPtr destination, int destinationStride, int widt public Task LoadFile(string fileName, double startPositionSeconds = 0) { - CloseFile(); - _fileName = fileName; + Session? previousSession; + int generation; + lock (_loadLock) + { + // Reserve this open before doing any slow native work. CloseFile or a newer LoadFile + // increments the generation, so this Task can never publish an obsolete session after + // the caller has already closed/replaced it. + generation = ++_loadGeneration; + previousSession = _session; + _session = null; + _fileName = fileName; + } + + previousSession?.Dispose(); + ClearCurrentFrame(); return Task.Run(() => { - if (_disposed) - { - return; - } - Session session; try { @@ -148,33 +158,54 @@ public Task LoadFile(string fileName, double startPositionSeconds = 0) catch (Exception exception) { Se.LogError(exception, $"ffmpeg player failed to open: {fileName}"); - _fileName = string.Empty; + lock (_loadLock) + { + if (generation == _loadGeneration) + { + _fileName = string.Empty; + } + } + return; } - if (_disposed) + lock (_loadLock) { - session.Dispose(); - return; - } + if (_disposed || generation != _loadGeneration) + { + session.Dispose(); + return; + } - _session = session; - session.Volume = _volume; - session.Speed = _speed; - session.Start(); + session.Volume = _volume; + session.Speed = _speed; + session.Start(); - // Always seek once: this is what decodes and shows the first picture (at the wanted - // position) while the player stays paused. - session.Seek(Math.Max(0, startPositionSeconds)); + // Always seek once: this is what decodes and shows the first picture (at the wanted + // position) while the player stays paused. + session.Seek(Math.Max(0, startPositionSeconds)); + _session = session; + } }); } public void CloseFile() { - var session = Interlocked.Exchange(ref _session, null); - _fileName = string.Empty; + Session? session; + lock (_loadLock) + { + _loadGeneration++; + session = _session; + _session = null; + _fileName = string.Empty; + } + session?.Dispose(); + ClearCurrentFrame(); + } + private void ClearCurrentFrame() + { lock (_currentFrameLock) { // The frame belonged to the session's pool, which is gone now.