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
157 changes: 135 additions & 22 deletions src/ui/Logic/VideoPlayers/Ffmpeg/FfmpegPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,45 @@ public double Position
return duration > 0 ? Math.Min(value, duration) : value;
}

/// <summary>
/// State visible after a native seek failure. Only the request that actually failed may roll
/// back to the committed pipeline serial/position; a newer request that arrived while
/// av_seek_frame was blocked must remain pending.
/// </summary>
internal static (int RequestedSerial, double RequestedTarget) FailedSeekState(
int failedSerial,
int currentSerial,
int requestedSerial,
double requestedTarget,
double currentPosition)
{
return requestedSerial == failedSerial
? (currentSerial, currentPosition)
: (requestedSerial, requestedTarget);
}

internal static bool ShouldAutoRewindOnPlay(
bool hasOutstandingSeek,
bool endReached,
double duration,
double position)
{
return !hasOutstandingSeek &&
(endReached || (duration > 0 && position >= duration - 0.01));
}

internal static bool ShouldReachAudioOnlyEnd(
bool playing,
bool hasOutstandingSeek,
double duration,
double position)
{
return playing &&
!hasOutstandingSeek &&
duration > 0 &&
position >= duration;
}

public double Duration => _session?.Duration ?? 0;

public int VolumeMaximum => 100;
Expand Down Expand Up @@ -359,7 +398,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 +409,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 +448,11 @@ or AVSampleFormat.AV_SAMPLE_FMT_DBLP
or AVSampleFormat.AV_SAMPLE_FMT_S64P;
}

internal static bool CanPublishVideoFrame(int frameSerial, int currentSerial)
{
return frameSerial == currentSerial;
}

/// <summary>
/// avcodec_send_packet(EAGAIN) means the decoder rejected the input. The same packet may be
/// resent only after receive_frame made progress, and only while the serial is still current.
Expand Down Expand Up @@ -679,12 +727,12 @@ public void Play()
return;
}

if (_endReached || (Duration > 0 && Position >= Duration - 0.01))
var hasOutstandingSeek = HasOutstandingSeek();
if (ShouldAutoRewindOnPlay(hasOutstandingSeek, _endReached, Duration, Position))
{
Seek(0);
}

_endReached = false;
_playing = true;
_wallClockBase = _pausedPosition;
_wallClock.Restart();
Expand Down Expand Up @@ -736,10 +784,11 @@ public void Seek(double seconds)
_requestedSerial++;
_requestedTarget = seconds;
_seekPending = true;
_pausedPosition = seconds;
}

_endReached = false;
// Keep the committed playhead/end state unchanged until libavformat accepts the seek.
// Position reports _requestedTarget while the request is outstanding, so optimistic
// mutation here is unnecessary and would corrupt state if av_seek_frame fails.
_demuxWake.Set();
}

Expand Down Expand Up @@ -799,6 +848,14 @@ private double Clock()
return _wallClockBase + _wallClock.Elapsed.TotalSeconds * _speed;
}

private bool HasOutstandingSeek()
{
lock (_seekLock)
{
return _requestedSerial != _currentSerial;
}
}

/// <summary>True when a seek newer than the given serial has been requested (performed or not).</summary>
private bool SeekRequestedSince(int serial)
{
Expand All @@ -819,8 +876,11 @@ private void DemuxLoop()
{
if (TryTakeSeek(out var target, out var serial))
{
PerformSeek(target, serial);
eof = false;
if (PerformSeek(target, serial))
{
eof = false;
}

continue;
}

Expand Down Expand Up @@ -894,13 +954,15 @@ private bool TryTakeSeek(out double target, out int serial)
}
}

private void PerformSeek(double target, int serial)
private bool PerformSeek(double target, int serial)
{
var timestamp = (long)((target + _startTimeSeconds) * ffmpeg.AV_TIME_BASE);
var result = ffmpeg.av_seek_frame(_format, -1, timestamp, ffmpeg.AVSEEK_FLAG_BACKWARD);
if (result < 0)
{
System.Diagnostics.Debug.WriteLine($"ffmpeg seek failed: {FfmpegLibraries.ErrorText(result)}");
Se.LogError($"ffmpeg player: seek to {target:0.###} s failed ({FfmpegLibraries.ErrorText(result)})");
RollBackFailedSeek(serial);
return false;
}

lock (_seekLock)
Expand All @@ -920,11 +982,35 @@ private void PerformSeek(double target, int serial)
}
}

_endReached = false;
_videoPackets.Flush(serial, target);
_audioPackets.Flush(serial, target);
_videoFrames.Flush();
_audioSink.Reset();
_presentWake.Set();
return true;
}

private void RollBackFailedSeek(int serial)
{
// Seek no longer mutates the committed paused position, so this remains the actual
// position when paused. While playing, Clock() follows the still-current pipeline.
var currentPosition = _playing ? Clock() : _pausedPosition;

lock (_seekLock)
{
var state = FailedSeekState(
serial,
_currentSerial,
_requestedSerial,
_requestedTarget,
currentPosition);

_requestedSerial = state.RequestedSerial;
_requestedTarget = state.RequestedTarget;
}

_presentWake.Set();
}

// ---------------------------------------------------------------- video decode
Expand All @@ -940,6 +1026,7 @@ private void VideoLoop()
var swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
var outputWidth = 0;
var outputHeight = 0;
VideoFrame? lastDropped = null; // detached from the queue while seeking; return it on every exit

try
{
Expand All @@ -959,7 +1046,6 @@ private void VideoLoop()
var minimumReplaySerial = -1;
var dropUntil = -1.0;
var presentedForSerial = false;
VideoFrame? lastDropped = null; // kept so a target past the last picture still shows something

while (!_closing)
{
Expand Down Expand Up @@ -1171,6 +1257,8 @@ private void VideoLoop()
}
finally
{
_videoFrames.Return(lastDropped);

if (sws != null)
{
ffmpeg.sws_freeContext(sws);
Expand Down Expand Up @@ -1758,6 +1846,15 @@ private void PresentLoop()
continue;
}

if (SeekRequestedSince(frame.Serial))
{
// Keep the old EOS marker until the seek commits or fails. Without
// this guard a video-only stream can ReachEnd while av_seek_frame is
// still blocked and lose a Play issued for the requested destination.
_presentWake.WaitOne(20);
continue;
}

if (_hasAudio)
{
// Video ended first; let the audio play out before stopping. The wake
Expand Down Expand Up @@ -1868,7 +1965,7 @@ private void PresentLoop()

private void PresentAudioOnlyTick()
{
if (_playing && Duration > 0 && Clock() >= Duration)
if (ShouldReachAudioOnlyEnd(_playing, HasOutstandingSeek(), Duration, Clock()))
{
ReachEnd();
}
Expand All @@ -1885,15 +1982,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 @@ -1906,12 +2013,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
17 changes: 7 additions & 10 deletions src/ui/Logic/VideoPlayers/Ffmpeg/VideoFrameQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,19 @@ public void Return(VideoFrame? frame)
}
}

/// <summary>Drops all queued frames (back to the pool).</summary>
/// <summary>
/// Drops all queued frames after a seek. Queued frames are disposed rather than returned to
/// the pool: the presenter may still hold a reference obtained from Peek/PeekSecond before
/// this flush. Reusing such an object immediately would let the decoder overwrite its serial,
/// timestamp and pixel buffer while the presenter is still reading the stale reference.
/// </summary>
public void Flush()
{
lock (_lock)
{
while (_frames.Count > 0)
{
var frame = _frames.Dequeue();
if (frame.Data != System.IntPtr.Zero && frame.Width == _width && frame.Height == _height)
{
_pool.Push(frame);
}
else
{
frame.Dispose();
}
_frames.Dequeue().Dispose();
}

Monitor.PulseAll(_lock);
Expand Down
Loading
Loading