Skip to content

fix: ONVIF events record 0 alarm frames for momentary alarms (DetectedSound) refs #5301 - #5305

Merged
connortechnology merged 2 commits into
ZoneMinder:masterfrom
connortechnology:5301-onvif-alarm-latch
Oct 20, 2026
Merged

connortechnology merged 2 commits into
ZoneMinder:masterfrom
connortechnology:5301-onvif-alarm-latch

Conversation

@connortechnology

@connortechnology connortechnology commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Fixes ONVIF events (notably AudioAnalytics/DetectedSound) intermittently recording 0 alarm frames (refs #5301).

Two commits

1. Latch the ONVIF alarm. The alarm was a level-sampled atomic bool. A momentary pulse (DetectedSound goes active then inactive between two analysis frames) was raised and cleared before any frame sampled it, so the event recorded no alarm frames. A motion alarm persists for seconds and so was always sampled. The latch holds a pulse until a frame has observed it.

2. Release the latch only when a video frame records it. The latch was released at the isAlarmed() sample in Monitor::Analyse(), before the packet was committed. A not-yet-decoded video packet makes Analyse() return at the decode gate and retry the same packet, so the release ran on every retry and spent a momentary pulse before any decoded frame scored it — which is why raising AlarmFrameCount and the earlier minimum-hold attempt (#5310, now closed) didn't help. An audio packet could also consume it, but alarm_frames counts video frames. The release is now deferred to the point a video packet is committed with its score.

Also keeps the last activation's topic/value so a momentary pulse's event note names the cause (e.g. AudioAnalytics/Audio/DetectedSound/true) instead of a bare ONVIF.

This also addresses the reported "no recording while watching via Go2RTC" case: go2rtc widens the decode-wait window, so with AlarmFrameCount=1 nearly every pulse was consumed before a video frame committed.

Testing

tests/zm_onvif_alarm_latch.cpp covers the latch. zmc builds clean; all [onvif] cases pass. The Analyse() release path needs a live packet pipeline, so it's verified by build + field retest rather than a unit test.

🤖 Generated with Claude Code

…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>
@IgorA100

IgorA100 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for the prompt fix.
I have tested this PR, and here are my observations:

  • It is much better, though not perfect.
  • In about 1 out of every 10 events, there are still 0 alarm frames.
  • In about 1 out of every 20 events, there is 1 alarm frame.
  • For the remaining events, there are 20–50 alarm frames. The duration from the start to the end of an ONVIF event is 0.5–1 second. FPS = 25, Key frames = 50.
  • The first alarm frame appears approximately 0.5–1 second after the actual sound occurs.

I think everything is fine, but why do we still occasionally see 0 alarm frames? According to the new algorithm, that shouldn't be happening.

@connortechnology

Copy link
Copy Markdown
Member Author

Thanks for the careful testing — that breakdown (most events 20–50 frames, ~1/10 at 0, ~1/20 at 1) is exactly the shape I needed to see.

Here's where I've got to tracing it in code:

  • Event::AddFrame records a frame as an alarm frame whenever score > 0 (zm_event.cpp), independent of monitor state or AlarmFrameCount.
  • The event-writer runs in its own thread and waits for each packet to be analyzed before adding it, so there's no race between scoring a packet and writing it — the writer always sees the final score.
  • The latch guarantees the pulse is sampled on at least one analysis frame, so a frame is being scored.

So the residual 0-cases aren't "the frame wasn't scored" — they're about which event the scored frame lands in. With a continuous/Mocord recording and event-close-on-alarm, when the alarm begins ZM closes the running continuous event and opens a fresh one. For a sub-second pulse the scored frame(s) land in the closing event, and the new "alarm" event opens just after them with none — so the event you're looking at shows 0, while its immediate neighbour holds the alarm frame. The 1-frame cases are the same boundary landing a single frame on the other side.

Two things would confirm this quickly:

  1. For a 0-alarm-frame event, does the immediately preceding event contain the alarm frame(s)?
  2. What is the monitor's Function (Modect/Mocord), AlarmFrameCount, and event Close Mode? And if you can, a zma debug log (LOG_DEBUG, level 3) covering one 0-frame event — I want to see the IDLE→ALARM transition and any event close/open around it.

If that confirms the boundary theory, the fix is to hold the ONVIF alarm "on" for a small minimum number of frames (so a momentary pulse behaves like a short real alarm and its scored frames continue into the new event after the split), rather than releasing it after a single observation as this PR does. I can add that on top of this change for you to retest.

… refs ZoneMinder#5301

The latch was released in Analyse() at the isAlarmed() sample, before the
packet was committed. Two paths then spent a momentary DetectedSound pulse
without recording it:

- A video packet not yet decoded makes Analyse() return at the decode gate
  before packet->score is set, and the same packet is retried. The sample --
  and the release -- ran on every retry, so the pulse was gone by the time the
  packet was decoded and scored, and the event recorded no alarm frame. This is
  why raising AlarmFrameCount or the earlier hold did not help: the retries of
  one undecoded packet consumed the latch.
- An audio packet (these cameras alarm on AudioAnalytics/DetectedSound, so audio
  packets are present) could sample and release the latch, but alarm_frames
  counts video frames, so the pulse was again recorded on no counted frame.

Defer the release to the point where a video packet is committed with its
score, so the latch is spent only on a frame the event actually writes.

Also keep the topic/value of the last activation and report it from
SetNoteSet() when the alarms map has already been cleared, so a momentary
pulse's event note still names the cause (e.g. AudioAnalytics/Audio/
DetectedSound/true) instead of a bare "ONVIF".

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

Copy link
Copy Markdown
Member Author

@IgorA100 thanks — your details pinned it, and it's a different cause than either of my earlier guesses. I've closed #5310 (the hold) and pushed the real fix here.

What was happening, in Monitor::Analyse(): the alarm latch was released at the moment it was sampled, before the frame was committed. Two paths then threw the pulse away:

  • A video frame not yet decoded makes Analyse() bail out at the decode gate and retry the same frame — and the release ran on every retry. So a momentary DetectedSound pulse was consumed during the decode wait, and by the time the frame was decoded and scored, the alarm was already gone → 0 alarm frames. This is exactly why it fired ~1s into recording (decode lag) and why the earlier hold didn't help (the retries ate it).
  • An audio packet could also consume the pulse, but alarm_frames only counts video frames — so again it landed on nothing counted.

This also explains your Go2RTC observation: watching live via go2rtc widens the decode-wait window, so with AlarmFrameCount=1 nearly every pulse was consumed before a video frame committed and no event opened at all. It should behave now — please retest that path too.

The fix releases the latch only when a video frame is actually committed with its score. I also made the event note keep the triggering topic for a momentary pulse, so the low-alarm-frame events should now show tns1:AudioAnalytics/Audio/DetectedSound/true in the Cause instead of a bare ONVIF.

Updated branch is ready to build when you have a chance.

@connortechnology connortechnology changed the title fix: latch ONVIF alarms so a momentary pulse is not missed fix: ONVIF events record 0 alarm frames for momentary alarms (DetectedSound) refs #5301 Oct 20, 2026
@IgorA100

Copy link
Copy Markdown
Contributor

@connortechnology
I will check the new changes within a few hours.
Thanks!

@IgorA100

Copy link
Copy Markdown
Contributor

I installed this version for testing.
When using Go2RTC on the "Watch" page, events are still not being saved. However, when I leave the "Watch" page or switch to a different player, the events are saved.
I will provide feedback on other aspects later, as some time needs to pass first.

@IgorA100

Copy link
Copy Markdown
Contributor

Everything is working great so far, with one exception: if you open the stream via Go2RTC on the "Watch" page, event recording does not take place.

@IgorA100

Copy link
Copy Markdown
Contributor

Oh, I’m completely stumped...
It turns out that the issue where events aren’t saved when viewing via Go2RTC on the "Watch" page affects at least several Dahua cameras. This happens not only with "DetectedSound" events but also with "MotionAlarm" (motion detection) events.
Furthermore, ONVIF Device Manager doesn’t register any alarm events from the camera, even though they should be coming through.
This is likely a separate issue that needs to be investigated.
As far as I can tell, this PR is now working correctly.
How Go2RTC could possibly interfere with the generation of alarm events by the camera is a complete mystery.

@connortechnology

Copy link
Copy Markdown
Member Author

What happens if you use go2rtc but with the rtsp re-streamer? Claude thinks the Dahua cameras have a concurrent access limitation and the second stream is killing them.

@connortechnology
connortechnology merged commit f9dc886 into ZoneMinder:master Oct 20, 2026
11 checks passed
@IgorA100

Copy link
Copy Markdown
Contributor

@connortechnology
Everything is great so far!
However, there is one point that’s not entirely clear. For some reason, AlarmFrames is sometimes 1, and at other times it’s between 20 and 200.
It’s never between 2 and 19 :)
It’s not critical, but there’s likely a small flaw somewhere in the algorithm.

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