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
49 changes: 37 additions & 12 deletions src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -370,6 +370,10 @@ private void Present(VideoFrame frame, VideoFrameQueue pool)

pool.Return(previous);
Interlocked.Increment(ref _frameVersion);
}

private void NotifyFrameReady()
{
FrameReady?.Invoke();
}

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