Commit Graph
2 Commits
Author SHA1 Message Date
nicandClaude Opus 5.5 ba0844915e fix: free decoded frames and images once no consumer can read them (#5185)
* fix: free decoded frames and images once no consumer can read them

Packets stay in the packet queue for passthrough's keyframe alignment and
for pre-event frames, but each also kept its decoded AVFrame until it left
the queue, and with SaveJPEGs set (the default) its Image too: ~15 MB per
packet at 5MP. The frames also pinned libavcodec's buffer pool at the
queue's depth. With on-demand decoding and viewers, or a keyframe interval
longer than MaxImageBufferCount, zmc grew to several GB.

- Analyse() releases in_frame, and the y_image view into it, once the
  packet has been analysed; later readers use packet->image. It is kept
  when an encoder has no image to work from.
- PacketQueue::releaseDecoded() frees every decoded copy of packets that
  all iterators have passed and that lie before where an event could still
  start (openEvent's pre-event walk back, to a keyframe for passthrough).
  The compressed packets stay queued.

Event output (capture/analysis JPEGs, snapshot, alarm, video) is unchanged.

refs #4860

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: say which reload can widen the window under releaseDecoded refs #4860

The comment said the release boundary can't move back because events only
open on the analysis thread. That holds for events, but a RELOAD action
(zmu --reload) runs Monitor::Reload() on zmc's main loop and can widen
pre_event_video_packet_count or set keep_keyframes without clearing the
queue. Say so, and what an event right after it loses: capture JPEGs for
released passthrough frames; on encode those packets are already gone.
Raised in review of #5185.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: never start an event on packets whose decoded data was released refs #4860

releaseDecoded() frees the images and decoder frames of packets before the
earliest point an event could start, computed with the current pre-event
window. A RELOAD action (zmu --reload) can widen that window, or turn on
keep_keyframes, without clearing the queue. On passthrough the queue still
holds the previous GOP until the next keyframe trims it, so the next event
could walk back into packets already released: its video was complete, but
those frames had no capture JPEGs. On a 15 fps camera with a 30-frame GOP,
widening PreEventCount 5 -> 100 left frames 1-30 without JPEGs in about a
third of the events right after the reload.

get_event_start_packet_it() now treats the first packet at or after
released_before_ as the front of the queue and warns when it stops there.
For passthrough that packet is the keyframe releaseDecoded() stopped on;
after a reload from encode to passthrough it may not be, so the start
moves forward to the next keyframe. With an unchanged window the walk
never reaches the boundary. After a widening reload the next event gets up
to one GOP fewer pre-event frames than before #4860's change, instead of
frames without images.

Raised in review of #5185.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:33:20 -04:00
Isaac ConnorandClaude Opus 5 99ae7b8e98 fix: free the event start iterator when openEvent cannot lock its packet fixes #5127
PacketQueue::get_event_start_packet_it() allocates a packetqueue_iterator and
registers it in PacketQueue::iterators. Every path out of Monitor::openEvent()
hands that pointer to the Event, which releases it in ~Event via free_it, except
the one that fails to lock the starting packet. That path returned nullptr and
left the iterator registered for the life of the process.

clearPackets() takes min_iterator_queue_index across all registered iterators and
stops removing at the first packet whose queue_index reaches it. An abandoned
iterator pins that to the front packet, so the trimmer removes nothing on every
invocation, and deletePacket() advances registered iterators onto the packet
after the one it removed, which drags the orphan onto each new front packet so it
can never age out. clear_packets_pending_ then latches true, downgrading the gate
so the futile scan re-runs on every video packet rather than only on keyframes.

The only removal path left is the emergency one-GOP eviction in queuePacket, so
the queue settles into refill a GOP, exceed max_video_packet_count, warn, evict a
GOP, repeat. It is pinned at its cap by construction, which is why the monitor
warns once per GOP at a constant rate for as long as zmc runs and no amount of
idle time or extra headroom recovers it.

Our trigger was an RTSP stream EOF mid-event during a reconnect on an ONVIF
camera. It is a race, so it leaks only when openEvent loses it, and each
occurrence leaks one more iterator.

Add tests covering both halves: that free_it takes an event start iterator back
out of the queue, and what an abandoned one does to trimming until it does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:49:34 -04:00