diff --git a/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs b/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs index baf69678..c469fecb 100644 --- a/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs +++ b/FlyleafLib/MediaFramework/MediaContext/DecoderContext.cs @@ -28,6 +28,25 @@ public unsafe partial class DecoderContext : PluginHandler */ #region Properties + /// + /// [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. + /// + 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 @@ -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; } } @@ -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; diff --git a/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs b/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs index e3a17c6d..47e9316e 100644 --- a/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs +++ b/FlyleafLib/MediaFramework/MediaDecoder/VideoDecoder.cs @@ -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 @@ -1092,6 +1105,17 @@ public VideoFrame GetFrame(int frameNumber, bool backwards = false) /// The next VideoFrame 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) diff --git a/FlyleafLib/MediaPlayer/Player.Extra.cs b/FlyleafLib/MediaPlayer/Player.Extra.cs index d4252d08..149e9836 100644 --- a/FlyleafLib/MediaPlayer/Player.Extra.cs +++ b/FlyleafLib/MediaPlayer/Player.Extra.cs @@ -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(); @@ -112,6 +116,7 @@ public void ShowFrame(int frameIndex) Renderer.RenderRequest(vFrame); UpdateCurTime(vFrame.Timestamp); reversePlaybackResync = true; + } } } @@ -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; @@ -162,6 +171,7 @@ public void ShowFrameNext() Renderer.RenderRequest(vFrame); UpdateCurTime(vFrame.Timestamp); reversePlaybackResync = true; + } } } public void ShowFramePrev() @@ -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; @@ -206,6 +220,7 @@ public void ShowFramePrev() Renderer.RenderRequest(vFrame); UpdateCurTime(vFrame.Timestamp); + } } } diff --git a/FlyleafLib/MediaPlayer/Player.Playback.cs b/FlyleafLib/MediaPlayer/Player.Playback.cs index d76260c6..bd76fe49 100644 --- a/FlyleafLib/MediaPlayer/Player.Playback.cs +++ b/FlyleafLib/MediaPlayer/Player.Playback.cs @@ -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) { @@ -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);