From 08673d06291878824df779144d4fb9a009d0d716 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 31 Oct 2026 11:14:10 -0400 Subject: [PATCH] fix: allow for the codec's delay in the decoder_queue limit Decode() flushes the codec once decoder_queue holds more than 2 x the keyframe interval while the codec still wants input (#5363). The codec legitimately holds frames back before returning one: has_b_frames for reordering, and one per extra thread with frame threading. For an all-intra stream (keyframe interval 1, so a limit of 2) decoded with a raised thread_count that delay alone passes the limit, and the codec was flushed every few frames with a Warning each time. Add the delay to the limit in DecoderQueueLimit(). refs #5362 Co-Authored-By: Claude Opus 5.5 --- src/zm_monitor.cpp | 16 +++++++++++++++- src/zm_monitor.h | 3 +++ tests/zm_monitor_decoder_queue.cpp | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 7d9aaaabe..19550070f 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -3577,6 +3577,20 @@ std::list::iterator Monitor::MatchDecodedFrame(int64_t frame_pts) return match; } +// 2 x the keyframe interval, plus the frames the codec holds back before it +// returns one: reordering (B-frames) and one per extra frame thread. Without the +// delay, an all-intra stream (keyframe interval 1) decoded with frame threads +// would pass the limit with nothing wrong and be flushed every few frames. +size_t Monitor::DecoderQueueLimit(int max_keyframe_interval, const AVCodecContext *context) { + size_t delay = 0; + if (context) { + if (context->has_b_frames > 0) delay += context->has_b_frames; + if ((context->active_thread_type & FF_THREAD_FRAME) && (context->thread_count > 1)) + delay += context->thread_count - 1; + } + return 2 * static_cast(max_keyframe_interval) + delay; +} + bool Monitor::Decode() { AVCodecContext *context = GetVideoCodecContext(); ZMPacketLock packet_lock; @@ -3651,7 +3665,7 @@ bool Monitor::Decode() { // Throttle: don't queue too many packets in the decoder int max_keyframe = packetqueue.get_max_keyframe_interval(); if ((max_keyframe > 0) && - (decoder_queue.size() > 2 * static_cast(max_keyframe))) { + (decoder_queue.size() > DecoderQueueLimit(max_keyframe, context))) { // Only reached when receiving above came back EAGAIN: the codec waits for // input, so holding it back would stop decoding for good. Start over. Warning("Decoder queue full (%zu packets) and the codec wants more input, flushing it", decoder_queue.size()); diff --git a/src/zm_monitor.h b/src/zm_monitor.h index a18df0ba5..1ba6eec87 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -811,6 +811,9 @@ class Monitor : public std::enable_shared_from_this { // The decoder_queue entry a decoded frame with this pts came from; releases // the entries before it that the codec consumed without returning a frame. std::list::iterator MatchDecodedFrame(int64_t frame_pts); + // How many packets decoder_queue may hold, waiting on the codec, before + // Decode() gives up on it and flushes. + static size_t DecoderQueueLimit(int max_keyframe_interval, const AVCodecContext *context); packetqueue_iterator *analysis_it; std::unique_ptr analysis_thread; packetqueue_iterator *decoder_it; diff --git a/tests/zm_monitor_decoder_queue.cpp b/tests/zm_monitor_decoder_queue.cpp index 63388c98e..e7596c431 100644 --- a/tests/zm_monitor_decoder_queue.cpp +++ b/tests/zm_monitor_decoder_queue.cpp @@ -34,6 +34,7 @@ void EnsureConfig() { class DecoderQueue : public Monitor { public: DecoderQueue() : Monitor() {} + using Monitor::DecoderQueueLimit; // Packets sent to the codec, in decode order, by pts. std::vector> Send(const std::vector &pts) { @@ -135,3 +136,28 @@ TEST_CASE("Monitor decoder queue: without a pts the oldest entry is used", "[Mon CHECK(q.Receive(99999) == 3600); CHECK(q.Queued().empty()); } + +TEST_CASE("Monitor decoder queue: the limit allows for the codec's own delay", "[Monitor][decoder]") { + CHECK(DecoderQueue::DecoderQueueLimit(25, nullptr) == 50); + + AVCodecContext *context = avcodec_alloc_context3(nullptr); + REQUIRE(context); + CHECK(DecoderQueue::DecoderQueueLimit(25, context) == 50); + + context->has_b_frames = 2; + CHECK(DecoderQueue::DecoderQueueLimit(25, context) == 52); + + // All-intra stream (keyframe interval 1) decoded with 8 frame threads: the + // codec holds 7 frames before returning one, more than 2 x 1. + context->has_b_frames = 0; + context->thread_count = 8; + context->active_thread_type = FF_THREAD_FRAME; + CHECK(DecoderQueue::DecoderQueueLimit(1, context) == 9); + + // Slice threads add no delay + context->active_thread_type = FF_THREAD_SLICE; + CHECK(DecoderQueue::DecoderQueueLimit(1, context) == 2); + + avcodec_free_context(&context); +} +