Skip to content
Open
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
53 changes: 53 additions & 0 deletions FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ public unsafe partial class DecoderContext : PluginHandler
*/

#region Properties
/// <summary>
/// [Local patch 2026-07-20, MultiAngleVideoPlayer stepfix] Serializes
/// stopped-state decode operations that touch the codec/format contexts
/// from DIFFERENT threads: the player's seek task (Seek + GetVideoFrame)
/// versus frame stepping (VideoDecoder.GetFrame/GetFrameNext via
/// Player.ShowFrame/ShowFrameNext/ShowFramePrev). The step functions take
/// no internal locks (see the VideoDecoder TBR header) and relied on
/// "locks at higher level" — but the seek task runs OUTSIDE
/// Player.lockActions, so a step's Flush/decode could run concurrently
/// with the seek task's avcodec_send_packet on the SAME AVCodecContext,
/// corrupting native state (0xC0000005 / 0xC0000374 / 0xC0000409 —
/// reproducible with 3-4 players rapid-stepping while seeks were still
/// in flight). This lock is always the OUTERMOST in both paths, so the
/// pre-existing (inverted) internal lock orders — DecoderContext.Seek
/// takes codecCtx→fmtCtx while GetVideoFrame takes fmtCtx→codecCtx —
/// are never composed across threads.
/// </summary>
public readonly object StepSeekLock = new();

public object Tag { get; set; } // Upper Layer Object (eg. Player, Downloader) - mainly for plugins to access it
public bool EnableDecoding { get; set; }
public new bool Interrupt
Expand Down Expand Up @@ -506,6 +525,10 @@ public void GetVideoFrame(long timestamp = -1)
if (ret != 0)
{
av_packet_free(&packet);
// [Local patch eofdrain] Demuxer EOF with the target still
// ahead: the remaining frames (incl. the FINAL frame) sit
// in the codec's delay pipeline — drain before giving up.
DrainVideoFrame(timestamp);
return;
}
}
Expand Down Expand Up @@ -605,6 +628,36 @@ public void GetVideoFrame(long timestamp = -1)

return;
}

/* [Local patch eofdrain 3.10.4.3] GetVideoFrame's EOF tail: the demuxer is
* exhausted but the accurate-seek target lies within the codec's delay
* pipeline (always true for the stream's FINAL frame). Without this, an
* end-of-stream seek presented NOTHING while every readback claimed
* success. Mirrors the frame-step path's drain (VideoDecoder.
* DecodeFrameNext) with GetVideoFrame's own acceptance test. Caller
* already holds lockFmtCtx + lockCodecCtx. */
private void DrainVideoFrame(long timestamp)
{
if (VideoDecoder.SendDrainAVPacket() != 0)
return;

while (VideoDemuxer.VideoStream != null && !Interrupt)
{
if (VideoDecoder.RecvAVFrame() != 0)
return; // fully drained (EOF), needs input (EAGAIN) or critical

// Accurate seek with +- half frame distance (same test as above).
if (timestamp != -1 && !VideoDemuxer.IsLive && (long)(VideoDecoder.frame->pts * VideoStream.Timebase) - VideoDemuxer.StartTime + (VideoStream.FrameDuration / 2) < timestamp)
{
av_frame_unref(VideoDecoder.frame);
continue;
}

int ret = VideoDecoder.FillEnqueueAVFrame();
if (ret == 0 || ret == -1234)
return; // success or critical — done either way
}
}
public new void Dispose()
{
shouldDispose = true;
Expand Down
24 changes: 24 additions & 0 deletions FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,19 @@ internal int SendAVPacket(AVPacket* packet)

return AVERROR_EAGAIN;
}
internal int SendDrainAVPacket()
{ /* [Local patch eofdrain 3.10.4.3] Enters codec draining (send_packet
* with null) so the delay-pipeline tail — including the stream's FINAL
* frame — becomes receivable. GetVideoFrame's accurate seek otherwise
* returns at demuxer EOF with those frames stuck in the codec, so an
* end-of-stream target presented NOTHING (while CurTime echoed the
* seek and SeekCompleted fired). The frame-step path already drains
* (DecodeFrameNext); this gives the seek path the same ability. The
* next Flush/Seek's avcodec_flush_buffers resets draining state.
* Bypasses SendAVPacket deliberately: its key-packet validation
* dereferences the packet, and a drain request has none. */
return avcodec_send_packet(codecCtx, null);
}
internal int RecvAVFrame()
{ /* Receives frame from the decoder (avcodec_receive_frame) | Should be used as tied to Run Loop (vPackets[] / Frames[])
* - Key Frame Validation
Expand Down Expand Up @@ -1092,6 +1105,17 @@ public VideoFrame GetFrame(int frameNumber, bool backwards = false)
/// <returns>The next VideoFrame</returns>
public VideoFrame GetFrameNext()
{
// [Local patch stepfix] ENFORCE the documented contract instead of
// trusting callers: after a seek, VideoDemuxer.Start() leaves the
// demuxer thread filling queues, and its RunInternal shares fmtCtx AND
// the demuxer's single packet field with GetNextPacket — concurrent
// av_read_frame on both corrupts native state (AV in av_read_frame,
// reproducible with 3-4 players forward-stepping right after seeks).
// GetFrame() already pauses both; GetFrameNext was the missing one
// (see the TBR header: "Missing locks (e.g. GetFrameNext)").
demuxer.Pause();
Pause();

checkExtraFrames = true;

if (DecodeFrameNext() == 0)
Expand Down
15 changes: 15 additions & 0 deletions FlyleafLib/MediaPlayer/Player.Extra.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ public void ShowFrame(int frameIndex)
lock (lockActions)
{
Pause();
// [Local patch stepfix] Serialized against the seek task (see
// DecoderContext.StepSeekLock).
lock (decoder.StepSeekLock)
{
dFrame = null;
sFrame = null;
Renderer.SubsDispose();
Expand All @@ -112,6 +116,7 @@ public void ShowFrame(int frameIndex)
Renderer.RenderRequest(vFrame);
UpdateCurTime(vFrame.Timestamp);
reversePlaybackResync = true;
}
}
}

Expand All @@ -133,6 +138,10 @@ public void ShowFrameNext()
UI(() => Status = status);
}

// [Local patch stepfix] Serialized against the seek task (see
// DecoderContext.StepSeekLock).
lock (decoder.StepSeekLock)
{
shouldFlushPrev = true;
decoder.RequiresResync = true;

Expand Down Expand Up @@ -162,6 +171,7 @@ public void ShowFrameNext()
Renderer.RenderRequest(vFrame);
UpdateCurTime(vFrame.Timestamp);
reversePlaybackResync = true;
}
}
}
public void ShowFramePrev()
Expand All @@ -179,6 +189,10 @@ public void ShowFramePrev()
UI(() => Status = status);
}

// [Local patch stepfix] Serialized against the seek task (see
// DecoderContext.StepSeekLock).
lock (decoder.StepSeekLock)
{
shouldFlushNext = true;
decoder.RequiresResync = true;

Expand Down Expand Up @@ -206,6 +220,7 @@ public void ShowFramePrev()

Renderer.RenderRequest(vFrame);
UpdateCurTime(vFrame.Timestamp);
}
}
}

Expand Down
15 changes: 13 additions & 2 deletions FlyleafLib/MediaPlayer/Player.Playback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,20 @@ private void Seek(int ms, bool forward, bool accurate)
}
else
{
// [Local patch stepfix] Serialized against the frame-step
// paths (see DecoderContext.StepSeekLock). SeekCompleted
// fires OUTSIDE the lock — a subscriber calling back into
// a lockActions-taking API must not deadlock against a
// step holding lockActions and waiting for this lock.
int? seekCompletedMs = null;
lock (decoder.StepSeekLock)
{
decoder.PauseDecoders();
ret = decoder.Seek(seekData.accurate ? Math.Max(0, seekData.ms - 3000) : seekData.ms, seekData.forward, !seekData.accurate); // 3sec ffmpeg bug for seek accurate when fails to seek backwards (see videodecoder getframe)
if (ret < 0)
{
if (CanWarn) Log.Warn("Seek failed");
SeekCompleted?.Invoke(this, -1);
seekCompletedMs = -1;
}
else if (!ReversePlayback && CanPlay)
{
Expand All @@ -310,8 +318,11 @@ private void Seek(int ms, bool forward, bool accurate)
SubtitlesDemuxer.Start();
DataDemuxer.Start();
decoder.PauseOnQueueFull();
SeekCompleted?.Invoke(this, seekData.ms);
seekCompletedMs = seekData.ms;
}
}
if (seekCompletedMs is int completedMs)
SeekCompleted?.Invoke(this, completedMs);
}

Thread.Sleep(20);
Expand Down