Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1118,7 +1139,7 @@ private void VideoLoop()
// the key frame.
FallBackToSoftware(ref codec, stream, 0, ref hardware);
serial = -1;
Seek(Position);
minimumReplaySerial = RequestHardwareFallbackReplay();
continue;
}

Expand Down Expand Up @@ -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;
}
}

/// <summary>
/// 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
Expand Down
15 changes: 15 additions & 0 deletions tests/UI/Logic/FfmpegPlayerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading