Skip to content

fix: end the event on a backward dts step instead of patching every packet - #5212

Draft
nabbi wants to merge 1 commit into
ZoneMinder:masterfrom
nabbi:fix/4242-dts-capture-break
Draft

nabbi wants to merge 1 commit into
ZoneMinder:masterfrom
nabbi:fix/4242-dts-capture-break

Conversation

@nabbi

@nabbi nabbi commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Automated PR, tested in a zm-holodeck sandbox. An AI agent (Claude Opus 5.5) reproduced the bug on a disposable ZoneMinder built from upstream master 25dba3267 with simulated cameras, wrote this fix and ran the checks below on the same lab. Minimal human review before opening: please review it as you would any contributor's PR.

TL;DR

  • Refs: 1.36.35 floods logs with "non increasing dts, fixing." warnings #4242: when a camera's clock steps back by less than 10 s (NTP sync, clock reset), "non increasing dts, fixing." is logged for every packet until the event ends.
  • An alternative to fix: rate limit the non increasing dts warning #5068, following @connortechnology's review there. Instead of rate-limiting the warning, the input side treats the jump as a short break in capture: the event ends there and a new one begins, without re-priming the capture, so the recording never sees the non-increasing run. Opened as a draft for discussion; the threshold and the open questions below are your call.
  • Change: FfmpegCamera marks a video packet whose dts steps back more than 1 s (steps of 10 s or more keep their existing handling); analysis closes the event on that packet, and the next event never starts before it. 6 source files, +52/-7, plus 2 test files (+92).
  • Risk: touches event open/close and the choice of an event's first packet. Unit tests, smoke, resilience and the events e2e (record, mocord, linked, pre-event reload, filters, purge) pass. Users see more events: one per step back.
  • Tested: with a file source stepping back 8.9 s, master logged 3,905 warnings in 120 s (step on a keyframe) and 6,085 (step on a non-keyframe); this branch logged 0, with the events split at each step. 100 ms jitter: 0 warnings and no extra events, on both builds.
Details: reproduction, change, behaviour changes, checks

Before

Upstream master 25dba3267, clean build. An Ffmpeg monitor reads a local FLV file (loop=1,realtime=1, Recording=Always, passthrough, section length 60 s) whose clock steps back 8.933 s every 12 s of video, under the 10 s "jumped back in time too far" cut-off.

non increasing dts, fixing. our dts -48000 stream 0 last_dts 94928 last_duration 1056 (8.933 seconds back). reorder_queue_size=0
step on a keyframe:      3905 of these in the first 120 s (3 events)
step on a non-keyframe:  6085 in the first 120 s (3 events)
100 ms jitter:           0 (the reorder queue sorts it)

VideoStore::write_packet (src/zm_videostore.cpp:1614-1617) replaces each such dts with last_dts + last_duration. The camera's clock stays behind that value, so every later packet in the event warns again, until a new event starts its timestamps over. The reorder queue only absorbs short-lived jitter. On the input side, FfmpegCamera::Capture (src/zm_ffmpeg_camera.cpp:364) only handles steps of more than 10 s.

Change

  • src/zm_ffmpeg_camera.cpp/.h: IsDtsCaptureBreak() is true for a step back of more than 1 s (kCaptureBreakDtsStep), checked on the video stream only, after the existing 10 s check. On a break, capture logs one Info line ("Stream dts jumped back N seconds, treating it as a break in capture") and sets ZMPacket::capture_break. The capture is not re-primed.
  • src/zm_packet.h/.cpp: the capture_break flag, initialised and copied.
  • src/zm_monitor.cpp (Analyse): a flagged packet with an open event logs Info "Closing event N, capture break" and calls closeEvent(), next to the signal-loss close. The existing if (!event) path then opens the next event on that packet (at once for Recording=Always, at the next alarm otherwise). Cause and notes unchanged.
  • src/zm_packetqueue.cpp (get_event_start_packet_it): looking back for pre-event frames or a keyframe stops at a flagged packet, as it already stops at released_before_; otherwise a continuous event reaches back to the keyframe before the step and records the step again. When the flagged packet isn't a keyframe, start_on_keyframe moves forward to the next keyframe; its "no keyframe" message is Debug, not Warning, in that case.
  • Tests: tests/zm_ffmpeg_camera.cpp covers the threshold (equal dts, 100 ms and exactly 1 s are not a break; 1.048 s, the report's 94320 ticks at 90 kHz, and 8.933 s are). tests/zm_packetqueue.cpp covers "an event never starts before a capture break" for passthrough continuous, passthrough with pre-event frames, encode with pre-event frames, and no break. They pass on this branch; not run against unpatched master.

The equal-dts case (the packet after a keyframe carrying the keyframe's dts) is unchanged: still Debug, dts + 1.

Behaviour changes

Change Who sees it Intended / collateral Alternatives looked into Chosen, why Tested by
An event ends and a new one starts at each video dts step back of more than 1 s and less than 10 s event lists and counts, filters, storage (more, shorter events); event start/end commands and MQTT; zmeventnotificationNg, which alerts on a new last_event while the monitor is in ALARM/ALERT, so a step during an alarm gives a second notification intended (the review on #5068) rate-limit the warning (#5068); shift timestamps on the input side without a split the event's timeline starts over cleanly probe event lists, events e2e
Frames between a step on a non-keyframe and the next keyframe are not recorded (passthrough) recordings collateral, as after a reconnect keep the old event open until the next keyframe (warns for up to one GOP) split at the step: no warnings, at most one GOP lost ffprobe of the probe's events
The per-packet Warning is gone for this case; each step logs 2 Info lines logs, log-based alerting intended — — probe counts
A camera that steps back often produces short events event lists collateral apply min_section_length, or limit how often events split open question not tested

No client (pyzmNg, zm-py/ha-zoneminder, zmNinjaNg, zmeventnotificationNg) matches these log texts. API and schema unchanged. Side fixes: none.

After

Branch at 4d13aa63d (base 25dba3267), build head 4d13aa63d7c9, dirty false:

step on a keyframe:      0 warnings in 120 s, 22 events, each closed with "capture break"
step on a non-keyframe:  0 warnings in 120 s, 42 events
100 ms jitter:           0 warnings, 3 events (60 s section ends, as on master)
INF Stream dts jumped back 8.93 seconds, treating it as a break in capture
INF jump: 090 - Closing event 13, capture break
INF Opened new event 16 Continuous
recordings (ffprobe): first packet a keyframe at dts 0, no non-increasing dts

(The probe source reads faster than real time after a step, so a split event holds more video than its wall-clock length; that is the probe, not the fix.)

Check Result
holodeck checks for the diff unit tests 252 cases / 14,963 assertions; script tests PASS=30; smoke PASS=11 XFAIL=1 (known ONVIF probe); resilience PASS=5; events PASS=10

Not run: audio (the probe has no audio track); a step during an alarm on OnMotion/Mocord; RTSP cameras (the probe goes through the same FfmpegCamera path from a file); the zmeventnotificationNg e2e; clients and contract (no API change). release-1.38 has the same 10 s handling and per-packet warning, but its packet queue has no released_before_ stop, so the start-packet part would need rework; not attempted.

Open questions

  1. Threshold: 1 s here; the step in 1.36.35 floods logs with "non increasing dts, fixing." warnings #4242 is 1.048 s, just over it. Lower (500 ms?), relative to the frame duration or keyframe interval, or a monitor option?
  2. Frequent steps: should splitting respect min_section_length or be rate-limited, so a camera that keeps stepping back doesn't fill the event list instead of the log?
  3. Steps on a non-keyframe: run the old event on to the next keyframe, keeping those frames but warning for up to one GOP?
  4. Audio: only video marks a break. With RecordAudio on, the new event can still get audio packets from before the audio step and warn on that stream. Split once both streams have stepped?
  5. Event note: add a note like signal loss's "Lost"/"Reacquired" to say why the event split? None now, to keep the change minimal.
  6. Steps of 10 s or more: treat those as a break too, instead of dropping packets and reconnecting after 6? This branch leaves that path alone.

refs #4242

🤖 Generated with Claude Code

…acket refs ZoneMinder#4242

When a camera's clock steps back (NTP sync, clock reset) by more than the
recording's reorder queue can sort out, VideoStore::write_packet patches each
dts to last_dts + duration. The source stays behind, so every following packet
of the event logs "non increasing dts, fixing." until the event ends.

Treat such a step on the video stream (more than 1 s back, below the existing
10 s "jumped back in time too far" handling) as a short break in capture:
FfmpegCamera marks the packet, analysis closes the event there and the next one
starts on that packet, with its own timeline. The capture is not re-primed.
get_event_start_packet_it() does not reach back past a marked packet for
pre-event frames or a keyframe, so the new event does not hold the step again.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant