Skip to content

fix: hold a momentary ONVIF alarm past the close-on-alarm event split refs #5301 - #5310

Closed
connortechnology wants to merge 2 commits into
ZoneMinder:masterfrom
connortechnology:5301-onvif-alarm-minimum-hold
Closed

connortechnology wants to merge 2 commits into
ZoneMinder:masterfrom
connortechnology:5301-onvif-alarm-minimum-hold

Conversation

@connortechnology

Copy link
Copy Markdown
Member

Builds on #5305. This branch contains that latch commit plus the follow-up below, so @IgorA100 can check out this one branch and test the combined behaviour.

What this adds

#5305 reported a pulse on a single analysis frame. Testing showed that was much better but still left ~1 in 10 events with 0 alarm frames. Tracing it: any frame with score > 0 is recorded as an alarm frame, and the event-writer always sees the final score, so the frame is scored — the question is which event it lands in. With close-on-alarm and continuous recording, ZoneMinder closes the running event and opens a fresh one when the alarm begins. A one-frame pulse whose scored frames all precede that split leaves the new event with none, so the event you look at shows 0 while its neighbour holds the frame.

This gives the AlarmLatch a configurable minimum hold and sets it to alarm_frame_count + 1 from the monitor. Reaching ALARM takes alarm_frame_count scored frames, which is where the event is split; holding one frame longer leaves at least one scored frame for the new event. The default hold stays one frame, so nothing else changes.

Testing

tests/zm_onvif_alarm_latch.cpp gains four cases for the counted hold: a pulse reported across several samples, an active level outlasting a spent hold, re-raise restoring the full hold, and a zero hold clamped to one. All 19 [onvif] cases pass (98 assertions); clean build.

@IgorA100 — if you test this branch, does the 0-alarm-frame rate drop? And it'd still help to know your monitor's Function, AlarmFrameCount, and event Close Mode so we can confirm this is the mechanism.

🤖 Generated with Claude Code

connortechnology and others added 2 commits October 18, 2026 18:09
…inder#5301

The ONVIF alarm was a plain atomic bool that the poll thread set and the
analysis thread sampled once per frame. A momentary alarm -- a DetectedSound
pulse the camera reports active then inactive between two analysis frames --
was raised and cleared on the raw level before any frame sampled it, so the
recorded event had no alarm frames. A motion alarm stays active for seconds
and so was always sampled, which is why MotionAlarm never showed the problem.

Replace the bool with an AlarmLatch: raise() marks the level active and that
an activation is waiting to be seen, clear() drops the level, and the analysis
thread calls observeAlarm() once it has sampled the alarm. isAlarmed() stays
true from a raise until the level is down and the activation has been observed,
so a pulse is reported on at least one frame however short it was. A
still-active alarm is unaffected. The latch is released only on a frame that
scored, so a pulse arriving just after the isAlarmed() check is kept for the
next frame rather than lost.

The activation guards in WaitForMessage() now test the raw level (alarm_.active())
instead of isAlarmed(), so a new topic still raises the level while an earlier
pulse's latch is still pending.

Add Catch2 coverage for AlarmLatch (tests/zm_onvif_alarm_latch.cpp), testable
without a gSOAP context like the other ONVIF free-function tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… refs ZoneMinder#5301

The latch reported a one-frame pulse on a single analysis frame. With
close-on-alarm and continuous recording the running event is closed and a new
one opened when the alarm begins, so a pulse whose scored frames all precede
the split leaves the new event with no alarm frames.

Give the latch a configurable minimum hold and set it to alarm_frame_count + 1
from the monitor. Reaching ALARM takes alarm_frame_count scored frames, which
is where the event is split; holding one frame longer leaves at least one
scored frame for the new event. The default hold is one frame, so the latch's
other callers and the existing behaviour are unchanged.

Add Catch2 cases for the counted hold: reporting a pulse across several
samples, an active level outlasting a spent hold, re-raise restoring the full
hold, and a zero hold clamped to one.

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

Copy link
Copy Markdown
Contributor

I will test this PR a bit later.
AlarmFrameCount = 1
EventCloseMode = system

@IgorA100

Copy link
Copy Markdown
Contributor

Unfortunately, there are no significant improvements.
When alarmFrame = 0, the event itself occurs approximately 1 second after the recording begins. In other words, it is not an issue where the event happens right at the end of the recording.
However, I’ve discovered another interesting and very strange detail! If I am on the "Watch" page and viewing the stream via Go2RTC (using either the WebRTC or MSE type), event recording does NOT take place.
When using ZMS or Rtsp2Web (with either the WebRTC or MSE type), there are no such issues; recording always starts as expected.

@IgorA100

Copy link
Copy Markdown
Contributor

Another observation:
When I review events on the Events page, for events with only 1–2 alarmFrame entries, the "Cause" column simply displays:

ONVIF
Motion: ONVIF

However, for events with a large number of alarmFrame entries (e.g., 20, 30, or 100), the "Cause" column shows more detailed and accurate information:

ONVIF
Motion: ONVIF, tns1:AudioAnalytics/Audio/DetectedSound/true

@connortechnology

Copy link
Copy Markdown
Member Author

Superseding this. @IgorA100's testing showed the event-split theory behind this PR was wrong — the alarm fires ~1s into the recording, not at the boundary, and the minimum-hold made no difference.

The real cause is in Monitor::Analyse(): the latch was released at the isAlarmed() sample, before the packet was committed. A not-yet-decoded video packet makes Analyse() return at the decode gate and retry the same packet, and the release ran on every retry — so a momentary pulse was spent before any decoded frame scored it. (That's also why the hold didn't help: the retries consumed it.)

Fix is on #5305 now (release the latch only when a video frame is committed), so closing this.

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.

2 participants