From 5153dc68ee32dad018676c3ad2452d914a3a6f99 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Thu, 14 May 2026 07:38:58 -0400 Subject: [PATCH] fix: flush decoder_queue on decoder thread exit to avoid stale latency offset across reconnect The decoder thread holds a raw AVCodecContext* obtained from camera->getVideoCodecContext() and pushes packet locks into Monitor's decoder_queue for each send_packet() that hasn't yet been matched by a receive_frame(). Monitor::PrimeCapture() used to call camera->PrimeCapture() (which Close()s the camera and frees the codec context) without first stopping the decoder thread. Two problems followed: 1. Use-after-free race between the decoder thread and the camera teardown. 2. The stale decoder_queue entries survived the reconnect. The new codec context produced frames in send-order, so we popped the oldest stale entries to attribute frames that actually came from packets sent later. The net effect was a permanent N-packet offset between capture and decode (~92 packets observed in the field). Analysis blocks on !packet->decoded for those packets, so the packetqueue saturates at max_video_packet_count and stays there, spamming the "max video packets in the queue" warning forever. Fix: - Stop+Join the decoder in Monitor::PrimeCapture() before tearing down the codec context. - Add Monitor::flushDecoderQueue() which marks in-flight packets decoded, notifies waiters, and clears the queue. - Call it at the end of DecoderThread::Run() so any Stop()+Join() (including the existing one in Pause()) naturally releases stale entries. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zm_decoder_thread.cpp | 6 ++++++ src/zm_monitor.cpp | 32 ++++++++++++++++++++++++++++++++ src/zm_monitor.h | 7 +++++++ 3 files changed, 45 insertions(+) diff --git a/src/zm_decoder_thread.cpp b/src/zm_decoder_thread.cpp index 6b08efd39..e5b6e61d9 100644 --- a/src/zm_decoder_thread.cpp +++ b/src/zm_decoder_thread.cpp @@ -42,4 +42,10 @@ void DecoderThread::Run() { } } } + + // Release any packets we sent to the codec but never received frames for. + // The codec context is about to be (or has been) torn down for Pause / + // reconnect; leaving stale entries would create a permanent latency + // offset against the next codec context on resume. + monitor_->flushDecoderQueue(); } diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 8bb33f4e2..89cc72b64 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -2892,6 +2892,21 @@ bool Monitor::applyDeinterlacing(std::shared_ptr &packet, Image *captu return true; } +void Monitor::flushDecoderQueue() { + // Called from DecoderThread::Run() as the decoder thread exits, so no + // concurrent access to decoder_queue: the thread that mutates it is us. + if (decoder_queue.empty()) return; + Debug(1, "Flushing %zu in-flight entries from decoder_queue", decoder_queue.size()); + for (auto &lock : decoder_queue) { + if (lock.packet_) { + lock.packet_->decoded = true; + lock.packet_->notify_all(); + } + } + decoder_queue.clear(); + packetqueue.notify_all(); // wake the analysis thread if it's waiting +} + bool Monitor::Decode() { AVCodecContext *context = camera->getVideoCodecContext(); ZMPacketLock packet_lock; @@ -3606,6 +3621,23 @@ unsigned int Monitor::Colours() const { return camera ? camera->Colours() : colo unsigned int Monitor::SubpixelOrder() const { return camera ? camera->SubpixelOrder() : 0; } int Monitor::PrimeCapture() { + // Stop the decoder before tearing the codec context down. The decoder + // thread holds a raw AVCodecContext* it got from + // camera->getVideoCodecContext(); camera->PrimeCapture() will Close() the + // camera (freeing that context) and OpenFfmpeg() a new one. Running the + // decoder against the dying context is unsafe; equally important, on + // exit the decoder thread releases the in-flight packet locks in + // decoder_queue (see DecoderThread::Run). Without that, stale entries + // survive the reconnect and create a permanent latency offset against + // the new codec context — the analysis thread blocks on + // !packet->decoded for those packets and the packetqueue fills to + // max_video_packet_count and stays there. + if (decoder) { + decoder->Stop(); + packetqueue.notify_all(); // wake the thread if it's blocked on wait_for + decoder->Join(); + } + int ret = camera->PrimeCapture(); if (ret <= 0) return ret; diff --git a/src/zm_monitor.h b/src/zm_monitor.h index d4daf6db2..4778d25e9 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -767,6 +767,13 @@ class Monitor : public std::enable_shared_from_this { RecordingOption Recording() const { return recording; } inline PacketQueue * GetPacketQueue() { return &packetqueue; } + + // Called by the decoder thread as it exits. Releases packet locks for + // anything it sent to the codec context but never received as a frame + // (codec context is about to be torn down for Pause/reconnect, so those + // packets will never produce output). Marks them decoded so the analysis + // thread can advance past them. + void flushDecoderQueue(); inline bool Enabled() const { return shared_data->capturing; }