diff --git a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs index 0eda2f7869..e01e1fc6c2 100644 --- a/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs +++ b/src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs @@ -427,6 +427,11 @@ internal static bool ShouldReplayHardwareSendFailure(int sendResult, bool hardwa sendResult != ffmpeg.AVERROR_EOF; } + internal static bool ShouldDropPacketBeforeHardwareReplay(int packetSerial, int minimumReplaySerial) + { + return minimumReplaySerial >= 0 && packetSerial < minimumReplaySerial; + } + private static double TimestampToSeconds(long timestamp, AVRational timeBase) { return timestamp == ffmpeg.AV_NOPTS_VALUE ? double.NaN : timestamp * ffmpeg.av_q2d(timeBase); @@ -951,6 +956,7 @@ private void VideoLoop() : 1.0 / 25.0; var serial = -1; + var minimumReplaySerial = -1; var dropUntil = -1.0; var presentedForSerial = false; VideoFrame? lastDropped = null; // kept so a target past the last picture still shows something @@ -962,6 +968,21 @@ private void VideoLoop() continue; } + if (ShouldDropPacketBeforeHardwareReplay(entry.Serial, minimumReplaySerial)) + { + var stalePacket = entry.Packet; + if (stalePacket != null) + { + ffmpeg.av_packet_free(&stalePacket); + } + + continue; + } + + // Equal is the replay seek itself; greater is a user seek that raced ahead. + // Either serial starts from a demux reposition and is safe for the fresh decoder. + minimumReplaySerial = -1; + if (entry.Serial != serial) { ffmpeg.avcodec_flush_buffers(codec); @@ -990,7 +1011,7 @@ private void VideoLoop() // feeding it the packet after the one that failed. FallBackToSoftware(ref codec, stream, sendResult, ref hardware); serial = -1; - Seek(Position); + minimumReplaySerial = RequestHardwareFallbackReplay(); } continue; @@ -1118,7 +1139,7 @@ private void VideoLoop() // the key frame. FallBackToSoftware(ref codec, stream, 0, ref hardware); serial = -1; - Seek(Position); + minimumReplaySerial = RequestHardwareFallbackReplay(); continue; } @@ -1172,6 +1193,21 @@ private void VideoLoop() } } + private int RequestHardwareFallbackReplay() + { + var target = Position; + Seek(target); + + // The seek request is asynchronous. Until the demux thread performs it, the video + // queue may still contain packets from the failed hardware serial. The fresh software + // decoder must not consume those packets because it has none of the old decoder's GOP + // reference state. A racing user seek has a higher serial and is safe to accept too. + lock (_seekLock) + { + return _requestedSerial; + } + } + /// /// Replaces the hardware decoder context with a software one. The caller's pointer is /// nulled before the new decoder is opened, so when OpenDecoder throws the caller's diff --git a/tests/UI/Logic/FfmpegPlayerTests.cs b/tests/UI/Logic/FfmpegPlayerTests.cs index ee4eea79e4..522c3bd496 100644 --- a/tests/UI/Logic/FfmpegPlayerTests.cs +++ b/tests/UI/Logic/FfmpegPlayerTests.cs @@ -264,6 +264,21 @@ public void ShouldReplayHardwareSendFailure_OnlyForFatalHardwareErrors() Assert.False(FfmpegPlayer.ShouldReplayHardwareSendFailure(0, hardware: true)); } + [Theory] + [InlineData(6, -1, false)] + [InlineData(6, 7, true)] + [InlineData(7, 7, false)] + [InlineData(8, 7, false)] + public void ShouldDropPacketBeforeHardwareReplay_RequiresOlderSerial( + int packetSerial, + int minimumReplaySerial, + bool expected) + { + Assert.Equal( + expected, + FfmpegPlayer.ShouldDropPacketBeforeHardwareReplay(packetSerial, minimumReplaySerial)); + } + [Theory] [InlineData(AVSampleFormat.AV_SAMPLE_FMT_U8, false)] [InlineData(AVSampleFormat.AV_SAMPLE_FMT_S16, false)]