Add JanusEnabled, Go2RTCEnabled, RTSP2WebEnabled and RTSPServer to the
per-monitor telemetry, so we can tell whether Janus and RTSP2Web are still
in use before deciding whether to deprecate them. Nothing in the existing
payload says.
Bring the privacy statement's list of collected monitor fields up to date
too. It listed Function, which no longer exists, and left out Capturing,
Analysing and Recording, and the source path, which is sent with its user
name, password and host replaced. Only en_gb is updated; the other
translations keep their own lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: show "map unavailable" on the map view when Leaflet is missing
Leaflet is an optional install: map.php loads js/leaflet/leaflet.js only
if it exists, and nothing in the build or the packages puts it there.
Without it views/js/map.js still ran new ZMMap(), which calls L.map(),
so the view stayed blank with "L is not defined" in the console.
Check for window.L first, as monitor.js and event.js already do for
their location maps, and show a translated warning in the map area
instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: recommend libjs-leaflet in the Debian/Ubuntu package and link it
The map view and the location maps of the monitor and event views load
Leaflet from js/leaflet/ in the web root, which nothing installs. Debian
and Ubuntu ship it as libjs-leaflet under /usr/share/javascript/leaflet.
Recommend libjs-leaflet and link /usr/share/zoneminder/www/js/leaflet to
it. Without the package the link dangles and the views behave as now
(map.php and output_script_if_exists() check that the file exists).
fixes#5260
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Display tab holds skin, css and navbar choices kept in a cookie for
the session looking at the page. They are not stored configuration and
do not affect anyone else, but the tab was named as though they were the
system's display settings. The settings that genuinely are the system's
display configuration -- the navbar, titles, thumbnails, event sorting,
animations, alarm popups -- sit on a tab labelled Web, which says where
they apply rather than what they do.
So the labels swap to match: the cookie tab becomes This Session, and
the Web tab becomes Display. A short note at the top of This Session
says the settings are kept in a cookie rather than saved on the server,
since a name alone will not carry that. The note goes inside the form
rather than beside it, because #optionsContainer is a flex row and two
siblings there split the width between them.
Labels only. The category keys are untouched, so ?tab=display and
?tab=web keep working, no configuration entry moves, and no migration is
needed. The one-off version/Versions case that was inline in the menu
builder becomes a small map beside the other two, applied at both places
that build the item list.
Only en_gb gains the new strings. Language files are loaded with en_gb
as the base and the chosen language layered over it, so the other 27
show the English until somebody translates them. Display and Web are
already translated in 22 of them, and those translations stay correct:
the word Display now labels the tab that actually holds display
settings.
Verified on a running install: the sidebar renders This Session for
tab=display and Display for tab=web, the note spans the full width above
the fields, all three tabs return 200 with their entry counts unchanged
and no php diagnostics.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
Follows the daemons: no monitor can be type Libvlc any more, so the ui
should not offer it or carry text describing it.
Drops the source-type option and its deprecation notice, the Libvlc arm
of the Source Path and console source-column branches, the entry in the
control-capability type list, and the value from the api's Type
validation list. The OPTIONS_LIBVLC help block goes from all 25 language
files: monitor.php builds that key as OPTIONS_ plus the monitor type, so
it became unreachable the moment the type did. The Libvlc source-type
label goes from the 22 files that carried it.
SourceTypeDeprecated and Deprecated lose their last caller and are
removed. RemoteRtspDeprecated and Removed stay -- the rtsp protocol
option is still shown to monitors already set to it.
The WEB_FILTER_SOURCE help listed the affected source types; corrected
in ConfigData.pm.in and in the one translation that had localised it.
All 25 language files and the four edited php files parse clean under
php -l, and the config module parses under perl -c. No javascript
changed, so eslint has nothing to say here -- it cannot run in this
worktree anyway, which has no node_modules.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
NVSocket spoke a bespoke socket protocol to one vendor's nvr. It has a
single known user, no way for anyone else to test it, and nothing it
reaches that ffmpeg cannot reach over rtsp or http, so it is removed
rather than carried further.
Drops RemoteCameraNVSocket, the NVSOCKET camera type and its parse and
construction arms, the source-type option and settings panel in the web
ui, and the stray ja_jp translation key. The preceding migration moves
any existing rows off the type first, so Monitor::Load cannot meet a
type it no longer recognises -- that path is Fatal and would stop zmc
rather than skip the monitor.
Built clean; unit suite passes (147 cases, 12531 assertions, run from
tests/ where the font fixtures' relative paths resolve). eslint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
Both reach cameras that ffmpeg reaches. Libvlc does it through a second
streaming library with its own build dependency, threading and bugs; NVSocket
speaks a bespoke socket protocol for one vendor's nvr that we have no way to
test. Neither earns the maintenance, and the same monitors work as Type Ffmpeg
with the camera's rtsp or http url.
Deprecated, not removed. A monitor using either still captures; it logs a
warning at startup naming what to change it to, and the form marks the type and
explains. That follows what Remote/RTSP did before it was removed, which is the
shape that gives people a release to move in.
The warnings were verified firing rather than assumed: switching a monitor to
each type in turn produces the expected message naming that monitor.
php -l and eslint clean; the three pre-existing duplicate lang keys are
unchanged. Suite 147 cases / 12531 assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
RTSPDescribe chose whether to take the media url from the DESCRIBE
response rather than the configured path. That was a quirk of
ZoneMinder's own rtsp implementation; ffmpeg decides it itself. Removing
that client left the setting with nothing behind it -- the monitor form
still drew the checkbox and the save path still wrote the column, but no
code read it, so the only thing it could do was mislead whoever ticked
it.
Drops the form row and the Remote-only block that existed to hold it,
the javascript that showed and hid it as the protocol changed, the field
in the php and perl monitor models, the default in the save action, the
column from the schema, and the label and help text from all the
language files carrying them.
The 1.39.37 migration drops the column behind an information_schema
guard scoped to DATABASE(), so the file stays re-runnable.
Verified by building a pre-upgrade database from upstream/master's own
schema and applying the migration: the column goes, and Monitors matches a fresh install of this
branch column for column. Migration applied three times with no error.
php -l clean on the four edited files and all 28 language files, perl -c
clean, build clean, 154 test cases pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
With the rtsp client gone, Type=Remote with Protocol=rtsp cannot capture, so
the monitor form no longer offers it and the camera probes no longer suggest
it.
A monitor already saved as rtsp still sees the option, because dropping it
outright would mean opening such a monitor to read the warning silently
re-saved it as http -- swapping one broken configuration for another. For those
the option is labelled removed and the warning now says the monitor will not
capture until it is changed, rather than that it will stop working in a future
release.
The three probes that suggested Remote/rtsp -- ACTI in both probe
implementations, and Vivotek -- now suggest Type Ffmpeg with the equivalent
rtsp:// Source Path, which is the same stream by a maintained route.
Type=Remote keeps http. mjpeg and snapshot-jpeg cameras are what it is for.
php -l clean on all four files; eslint clean; the three pre-existing duplicate
lang keys are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
The Options page admits System=View and wrote every Config value into
its inputs, only disabling them for users who cannot edit. The Private
flag was never consulted, so a read-only administrator received
ZM_AUTH_HASH_SECRET, which is enough to sign a token for any user, and
the other Private credentials on their tabs.
For a Private option, show its value only to System editors; others see
that it is hidden. The note about a value overridden in zm.conf, which
printed the overriding value, is skipped for those options too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverts the previous commit's always-on measurement. Decoding audio for every
monitor that has it spends CPU on a number almost nothing reads, which is the
wrong trade even though it did solve the chicken and egg of picking a
threshold without ever seeing a level.
Measure when something is actually going to use the reading instead:
- AudioDetection is on, as before, so nothing changes for a monitor that
scores on audio; or
- somebody asked. SharedData gains audio_level_until, a wall clock second
the capture thread keeps measuring up to. The monitor editor's new level
meter pushes it forward while it is on screen and the measurement lapses a
few seconds after the page is left, so nothing has to send a stop and a
crashed browser cannot leave a monitor decoding forever.
When the reading stops being wanted the decoder is released and the published
level and peak are cleared, so a stale number is not left looking current and
an old peak does not land on the next frame row written.
audio_level_until is carved out of analysis_pad rather than appended, so
SharedData stays 888 bytes and no existing offset moves; the static_asserts,
Memory.pm and Monitor.php are updated together and all three now agree the
field is at +880.
The meter itself is on the audio settings, shown whether or not
AudioDetection is checked, because the level is what you need in order to
choose a threshold. It draws the threshold currently in the input as a mark on
the bar so a reading can be judged against it before saving, and a monitor
whose zmc is not running reads "no reading" rather than a confident 0, which
would be indistinguishable from silence.
Frames.AudioLevel is therefore 0 again on monitors that do not score on audio.
That is what the graph already treats as "no audio data", so it draws no line
rather than a flat one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpiSWBmtQkR5bcgpHWY4ME
The library is AGPL-3.0-or-later so it is not shipped, and the admin installs
it at skins/<skin>/assets/audioMotion-analyzer/src/audioMotion-analyzer.js.
The instructions for doing that had drifted from what the code loads:
- help.txt and the OPTIONS_WHATTODISPLAY help gave the bare
https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL, which resolves
to the package's "main" entry, the minified UMD bundle dist/index.js. The
install path is the package's src/ ES module, so the URL needs the explicit
/src/audioMotion-analyzer.js suffix. Same for the download links in the
AudioMotionVersionNotInstalled and AudioMotionVersionWrongVersion messages.
- The install path was written as /skins/MySkin/..., a placeholder that does
not correspond to any skin.
- RequiresAudioMotionEnabled named only the file, not where it goes.
- assets/version documented every other asset in that directory but not this
one, leaving no explanation for the otherwise empty directory.
help.txt no longer restates the required version, so 4.5.4 stays declared only
by SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION as intended, and assets/version
points at that constant rather than duplicating it.
Add tests/js/audiomotion-paths.test.js to hold the PHP feature probe, the
dynamic import, help.txt and both lang catalogues to the same path, the same
download URL and the same version.
Also gitignore the installed library so a local install is not committed back
into a GPL-2.0 tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpiSWBmtQkR5bcgpHWY4ME
The audio settings were inserted between Analysis FPS and the reference blend
percentages, splitting the motion settings in half. The tab now reads as three
groups, each opened by a heading row with a rule above it: motion detection,
audio detection, then the settings that belong to neither - linked monitors and
the Amcrest API toggle.
A heading row rather than fieldset/legend, which is what user.php uses, because
these tabs are a flat <ul> of <li> rows and a <ul> may only contain <li>. The
heading gives the same separation without breaking the list or disturbing the
existing row layout.
The audio labels were whole sentences where every other row has a short one,
which is what made the label column so wide. They are now Audio Detection,
Audio Threshold and Audio Alarm Score, with the descriptions moved to form-text
help under each input, matching how Capturing and Decoding already describe
their options. The threshold help now also says to measure the device's quiet
level first, since setting it too low is what made a monitor alarm constantly.
The audio heading hides with its group when Motion Detection is None, since the
whole group is inert then.
Verified in the browser: the three groups render with their rules, the label
column is back to a normal width, and the help text sits under each input.
ESLint 0 problems, tests/js 154 assertions, php -l clean, and the new CSS
checked to be at top level rather than inside one of the media queries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
Alarm turns a light on when motion starts; nothing turned it back off. The
existing EventEnd is not the answer: the alarm ends when the monitor leaves the
alert state, while the event carries on recording for the rest of its section,
which on a continuous-recording monitor is up to ten minutes later.
Analyse() leaves the alarm condition by three paths and all three fire it: the
normal ALERT to IDLE, a signal change, and the trigger being turned off. The
abnormal two matter most - a light switched on by an alarm must not stay on
because the camera lost signal.
That needs the firing to be paired, so alarm_actions_fired is set when Alarm
fires and cleared by EndAlarmActions. It guards both directions:
- the three call sites can run back to back, so without it one alarm could
fire AlarmEnd several times
- signal loss and trigger off also run on monitors that never alarmed, and an
unpaired AlarmEnd would switch a light off for an alarm that never happened
- the ALERT to ALARM re-trigger inside one incident deliberately does not
re-fire Alarm, so a single AlarmEnd still has to balance it
The migration MODIFYs the enum and appends the value, so stored rows keep their
meaning; appending to an enum does not renumber what is already there.
Tested: 8 assertions over the pairing rule, run standalone because the real
class needs a database and a camera - the ordinary pair, repeated calls firing
once, an unpaired call firing nothing, two alarms pairing independently, and
the re-trigger case. The action test now also requires every trigger name to
round trip through the TriggerOn enum, so an action saved by the editor cannot
load with a trigger the C++ does not recognise and fire at the wrong moment.
Build clean at 1.39.33, Perl 15 files / 249 assertions, ESLint 0 problems,
tests/js 154 assertions, php -l clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
A deprecation warning without a version attached is one people learn to
scroll past. Both notices now say deprecated as of 1.40, removed in 1.41.
Removal is 1.41 rather than 1.40 because 1.39.x is the development series.
Anyone running stable is on 1.38 and will never see a 1.39.x warning, so
removing in 1.40 would take the feature away in the same release that first
mentions it - no notice at all for the people it affects. Warning in 1.40 and
removing in 1.41 gives stable users one release to migrate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
ZoneMinder carries its own RTSP/RTP implementation - zm_rtsp.cpp,
zm_rtp_ctrl.cpp, zm_rtp_source.cpp, zm_rtp_data.cpp, zm_rtp.cpp and
zm_remote_camera_rtsp.cpp, about 66KB in total. It is hand-written parsing of
untrusted network input, which is the least rewarding kind of code to own, and
Ffmpeg already does the same job for more cameras and is maintained upstream.
This marks it deprecated. It does not remove anything and does not change how
any existing monitor captures.
The capture log now names the replacement instead of only saying the method is
going away:
Monitor 7 (Driveway): the Remote/RTSP capture method is deprecated and will
be removed. Change this monitor to Type 'Ffmpeg' with Source Path
rtsp://10.0.0.5:554/live
Monitor::RtspUrlFromRemote builds that URL from the Host, Port, Path, User and
Pass the monitor already stores. It is pure so it can be tested without a
camera, and it is the piece a conversion migration will need when the code is
finally deleted.
The URL is passed through remove_authentication() before being logged, the
same helper zm_ffmpeg_camera.cpp uses. A deprecation notice that wrote camera
passwords into /var/log/zm would be a bad trade.
Credentials are percent-encoded. A password containing @ or : otherwise splits
the authority in the wrong place and yields a URL pointing at a different host,
which fails in a way that is hard to read.
In the editor the protocol is now labelled "RTSP (deprecated)" and selecting it
explains what to change to. Only the label moved: htmlSelect still emits
value="rtsp", so updateMethods and the stored value are untouched.
No auto-converting migration. Deprecating and removing are separate steps, and
silently rewriting a working monitor to Ffmpeg could break a camera that Ffmpeg
handles worse - which is the reason to warn a release ahead of removing.
Tested: 16 assertions over the URL builder, run standalone because ctest still
cannot build against the Catch2 v2 installed here - empty credentials producing
no dangling userinfo, user without password, @ : / ? # and space in passwords,
a username needing escaping, missing leading slash, empty path, absent port,
query strings surviving, and that the masked form drops the password while
keeping the host. Full build clean, Perl 15 files / 249 assertions, ESLint 0
problems, tests/js 154 assertions, php -l clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
ZoneMinder has carried audio for years without ever listening to it: the
packets go into the event and nothing reads them. AudioDetector decodes
them in the capture thread and reports a 0-100 level, which Analyse()
turns into score alongside motion, ONVIF and Amcrest.
The level is dBFS-derived, not a raw amplitude ratio. Linear RMS is
unusable as a setting: ordinary speech sits at 1-3% of full scale, so
every sound worth catching would be crammed into the bottom two points of
the range and no operator could tune it. Mapping -60..0 dBFS onto 0..100
puts speech around 43 instead.
Three columns on Monitors: AudioDetection to enable it, AudioThreshold
for the level to alarm at, and AudioAlarmScore for what it contributes.
AudioAlarmScore defaults to 9, matching what an ONVIF or Amcrest alarm
already adds. A threshold of 0 means off, so enabling detection without
choosing a threshold cannot alarm on silence -- a plain level >= threshold
test would alarm on every packet in that state.
The level and the alarm flag go in SharedData's two spare bytes, renamed
from reserved1/reserved2. Offsets and sizes are unchanged, so the
888-byte cross-process layout and the Memory.pm and Monitor.php offset
tables all still agree; the readers are renamed in the same commit so the
level is available to the web UI.
The decoder is opened lazily on the first audio packet rather than at
camera setup, so a monitor with detection off never carries one and a
stream that gains audio on reconnect still gets picked up. Scoring reads
the capture thread's most recent level rather than scoring per audio
packet, because the score belongs to a video frame and audio packets do
not arrive in step with them.
Tested: 242 assertions over the pure helpers - the RMS of both sample
formats, the dB scale's monotonicity and endpoints, full-scale clamping
of decoder overshoot, and the threshold-0 case. The repo's Catch2 harness
needs v3 and only v2 is installed here, so tests/zm_audio_detector.cpp
was compiled against a v2 shim to check it, and an identical set of
assertions was run standalone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
A camera that detects motion should be able to sound a speaker, and the
speaker is rarely the camera: it is a separate device with its own address,
credentials, stream and Controls entry. Model it as a monitor and let a
monitor's alarm drive actions on other monitors.
Add Monitors.DeviceClass enum('Camera','Speaker'). This is what the device
is, as distinct from Type, which selects the capture backend - an IP speaker
still captures over Ffmpeg like any other RTSP device, so Type could not
carry the distinction.
Add the MonitorActions table: MonitorId is the monitor that triggers,
TargetMonitorId the device acted on, and the two are frequently different.
TriggerOn covers EventStart, EventEnd, Alarm and Manual.
Which actions a device is offered is decided by its Controls row - CanLight,
CanIndicatorLight, CanAudioPlay - so a device can only be asked to do what it
has been measured to do. The editor filters on this and the save path
re-checks it, because the request is not to be trusted.
Execution goes straight to the target's zmcontrol socket rather than forking
zmcontrol.pl per action: the daemon already accepts a line of JSON there, and
it is the same path the control panel uses. ActionCommandName maps the DB
enum onto a method name as a whitelist, so nothing out of the database
reaches the control daemon uninspected. Actions are fire-and-forget - a
speaker that is offline is logged and skipped, never allowed to hold up event
handling.
Alarm actions fire only on the genuine entry into alarm, not on the
ALERT->ALARM re-entry, which would re-sound a speaker within one incident.
EventEnd runs on the calling thread before the event is handed to the closing
thread, which does not capture `this`.
Manual actions appear as buttons on the watch page, and are the reason the
control panel is now shown for a monitor that has actions but no control of
its own. Firing one sends only the action id; the command and target are
rebuilt server side, and Control rights are required on the target device and
not merely on the monitor the action hangs off.
Also add --file to zmcontrol.pl, without which audioPlay could not be driven
from the command line. The web path was unaffected as it bypasses GetOptions.
Tests: tests/zm_monitor_action.cpp covers the command whitelist, the message
format (including that file id 0 is a real id and that a stale AudioFile is
never passed to a command that takes none), trigger names, and that every
value of the ActionType enum maps to a command.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
(cherry picked from commit b561341e988af69c3a46db854da410309f7cfa82)
An IP speaker is controllable but has none of the capabilities the Controls
table models: nothing moves, focuses or lights up. It plays a sound file it
already holds, selected by a numeric id, and carries an output volume.
Add CanAudioPlay, MinAudioFile, MaxAudioFile and CanAudioVolume to Controls,
with migration zm_update-1.39.21.sql. CanAudioPlay renders one button per
sound id plus a stop button; MinAudioFile/MaxAudioFile bound that range;
CanAudioVolume renders the volume pair. The sound id travels in the command
name (audioPlay12), the way presets already do, because a control button has
no way to attach a separate parameter.
Add ZoneMinder::Control::IPSpeaker driving the device's own HTTP interface,
and a Controls entry for it. Playback goes over the vendor interface rather
than ONVIF because the ONVIF audio output service on this firmware only
describes the output and offers no way to start a stored file.
Measured against a device reporting ONVIF Manufacturer "IPSpeaker" and
firmware CS20-V3.3.45N:
- File ids fall in two windows, 10-14 built-in and 20-30 operator uploads.
Anything else is refused with result -2, and an id in a window with no file
uploaded with result -3, so valid_fileid refuses only the former: an empty
slot is a legitimate id the operator may fill later.
- config=audio.set replaces the whole audio section, so a partial write
reverts the microphone, codec list and echo-cancellation settings. Every
volume change reads the section and echoes it back with the new level.
audio.get also returns outmute, which audio.set rejects, so it is excluded
from the field list.
- The volume reads back and round-trips, but the device's RTSP audio is a
pre-volume tap of the playback signal: it is unchanged with outvolume at 0,
so it cannot confirm the loudspeaker's acoustic output. CanAudioVolume is
set on the strength of the setting persisting, and both the POD and the
seed row say the acoustic effect is unconfirmed.
The seed row covers the built-in window only, because a fresh device has
nothing uploaded and the firmware refuses an empty slot.
Adding columns to Controls extends the 43 positional INSERTs in
db/controls.sql, which carry no column list and so must match the column
count exactly.
Tests: scripts/ZoneMinder/t/ip_speaker.t, 32 assertions covering file id
windows, volume clamping and stepping, request building and the whole-section
write. All pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
(cherry picked from commit d07e552a3c58373f7f0ccebf9aa28adb4d51baa3)
The four points from the review on #5076 that the first round left open.
Persist a ratio as the number that will be read back. zone.php reloads these
cookies through validInt(), which strips everything that is not a digit, so 70.5
was stored and returned as 705 and 1e2 as 12, while the live calculation took
the same field through parseFloat and used it as typed. The ratio therefore
changed meaning on reload. Round and range-check before storing, and write the
result into the field so what is shown, what is calculated and what is stored
are one number.
Clear the tool when the page's Reset is pressed. Reset restores every field from
initialValues and redraws the polygon, so the measurement Undo would revert is
already gone, but undoSnapshot, lastBox, the highlighting and the rectangle all
survived it. The handler is wrapped rather than joined by a listener because
ours has to run first: resetChanges calls updateArea, which now re-derives.
Re-derive when the zone area changes. The thresholds are stored as percentages
of the zone, so moving, adding or removing a vertex changes what they mean in
pixels and the saved numbers stop describing the object that was measured.
updateArea is the one funnel those edits go through, so wrap it and re-run the
derivation from the box still on screen. apply() only reaches
updateAllPixelDisplays, so there is no way back into updateArea from there.
Draw with pointer events, and give the tool a keyboard-operable equivalent.
pointerdown/move/up with pointer capture replaces the mouse-only gesture, which
touch and pen could not drive at all, and touch-action is suppressed while armed
so the browser does not claim the drag for a scroll. pointercancel ends the drag
too, since a pointer taken away by the browser never sends pointerup. For the
keyboard there is now an object size in capture pixels beside the ratios: it
derives through the same path, draws the same box centred in the frame, and
raises the same filter warning. A finished drag writes its size back into those
fields, so the two ways in stay in step.
Also re-check measurability at pointerdown. The type listener added in the
first round covers the select, but applyPreset writes Type directly and fires
no change event, so a preset that makes the zone Inactive or Privacy left the
tool armed and apply() would re-enable the fields applyZoneType had disabled.
tests/js/zone-object-size.test.js covers the two new pure functions:
46 assertions pass. ESLint clean on the module and the test; php -l clean on
zone.php, zone.js.php and en_gb.php.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Montage review drew the monitor attribute filters inline - Name, Capturing,
Analysing, Recording, Status, Source - taking rows of the filter bar for
controls that choose which cameras appear and are rarely touched once set.
Put them behind their own control, collapsed by default.
The block keeps its place in the layout rather than being display:none:
skin.js applies hidden-shift from data-initial-state-icon, which moves it
off screen while leaving it measurable so Chosen still initialises the
selects inside it. applyChosen runs when it is opened.
The monitor selection itself is the result of those filters rather than one
of them, so it stays visible with the event filter terms. buildMonitorsFilters()
now also returns the selection on its own and the attribute filters without
it; filterBar is unchanged, so the other five callers are unaffected. The
selection is spliced into simple_widget()'s own flex row so it sits on the
same line as Date Time and the rest; emitted as a sibling it would take a
row of its own. If that markup ever changes, the replace count is checked
and it falls back to its own row with a logged warning rather than
disappearing.
A monitor selector was also being drawn twice: _monitor_filters.php renders
MonitorId[] for the session selection, and montagereview.php added the same
selection again as a Monitor IN filter term, which the filter widget drew as
a second control. Its sibling branch added a run of MonitorId = terms and
was marked "this should be redundant" when written. Both are gone; neither
is needed for the query, since the bar has already reduced $displayMonitors
and loadEventData() asks for those ids.
Verified on the install this repo matches:
event filters row Monitor, Date Time, Date Time, Archive Status, Tags, Notes
collapsed Name, Capturing, Analysing, Recording, Status, Source
one monitor selector, on the same row as the first term, starting collapsed
Adds a measuring tool to the zone editor. Drag a box around the smallest
object that should trigger an alarm and the detection thresholds are derived
from its area, replacing the guesswork of picking pixel counts by hand.
Sizing follows docs/userguide/definezone.rst. [J] gives the rule directly, a
minimum Alarmed Area of "25% of the area that an object of interest takes up
in the Zone region", along with a worked example - 30% of the zone giving
7.5% - which the tests assert. The filtered and blob minimums step down from
there by 70% and 86%, the ratios every shipped Blobs preset uses whatever its
sensitivity, so the steps are a property of the pipeline rather than a
sensitivity dial. All three are editable in a table beside the zone settings.
Each stage is measured against the one it is drawn from: filtered pixels are a
subset of alarmed ones, and a blob is built from pixels that survived
filtering. Taking the blob threshold as a share of the filtered count rather
than the alarmed count makes MinBlob <= MinFilter <= MinAlarm hold by
construction, so no clamp is needed to keep validateForm satisfied whatever
ratios are entered.
Thresholds are computed as pixel counts and converted to percentages once, at
the point of storage. Deriving the filtered and blob values from an
already-rounded alarm percentage rounded twice, which for a 1104px box in a
100000px zone put the filtered minimum a whole step out.
CheckMethod is set to Blobs, since MinBlobPixels is the only threshold tested
per blob - the other two are zone-wide sums that several small movers can
reach together. applyCheckMethod() is called explicitly because assigning to a
select does not fire onchange, and the blob fields would otherwise stay
disabled and never submit.
Every Max is left blank. MaxAlarmPixels, MaxFilterPixels and MaxBlobs set
overload_count, blinding the zone for OverloadFrames frames, and exceeding
MaxBlobPixels deletes the blob outright. One rectangle cannot imply a ceiling
when the same object is far larger close to the camera.
MinPixelThreshold and FilterX/Y are camera properties rather than object
sizes, so they are kept whenever already set, from a preset or by hand, and
filled from the documentation's defaults only when blank.
Interaction:
- thresholds track the box live during the drag, so nothing on the mousemove
path blocks
- the tool stays armed and each new box replaces the last, all measured
against the state before the first, so undo and highlighting stay anchored
- mouseup redraws at the release point before measuring it, so the visible
rectangle is always the one the numbers came from
- a label on the box reports its size in capture pixels
- rewritten fields are marked in the khaki the point rows already use, and the
marks clear when a preset or a hand edit takes over
- a still image can stand in for the live view, read locally as a data URL and
drawn in exactly the box the stream occupies so coordinates still map
Ratio defaults live only in zone.php, which reads the cookies and renders each
field with its stored value as the player selector does with zmZonePlayer.
Reuses maxX/maxY, constrainValue, streamCmdPause/Play, getCookie/setCookie and
validInt rather than reimplementing them, and shares the existing highlight
colour and Editing stroke width. zone.js is unchanged.
Tests: tests/js/zone-object-size.test.js, 35 assertions, node+assert matching
the existing tests/js pattern. Browser interaction is not covered.
Deleted monitors are excluded from every monitor listing, so once a monitor
is deleted there is no way to find it again from the ui - which matters
because deleting is reversible, the monitor edit form has an undelete
checkbox for exactly that.
Add Deleted as a pseudo status in the Status filter. Selected on its own it
lists only the deleted monitors; selected alongside real statuses it adds
them to that selection rather than intersecting with it, which would always
be empty; not selected, listings stay restricted to live monitors as before.
Deleted is deliberately not matched against Monitor_Status. Whatever row a
deleted monitor left behind is stale - its daemons were stopped when it was
deleted - so filtering on it would drop the monitors we are trying to find.
For the same reason a deleted monitor is reported as Deleted rather than the
status on that row, is drawn with the error dot, is labelled in the list,
and does not get a link to a stream that is not running.
The three queries that hardcoded Deleted=false now share one function, so
the console page, the console ajax endpoint and getFilteredMonitorIds()
cannot disagree about what the filter means. Each passes its own status
column expression, which differ: the ajax endpoint coalesces a WebSite
monitor to Running.
tests/php/test_monitor_status_filter.php covers the sql and the bind value
ordering for all four cases, including a bare string from a cookie written
before the filter became a multi-select. Verified against a live install:
13 deleted and 19 live monitors return 19 with no filter, 13 for Deleted,
and 20 for Deleted plus NotRunning.
Give the Add/Delete buttons on the AI Datasets, Models and Classes
option tabs the same treatment as the console: an add_circle icon on
the add button and a delete (trash) icon on the delete button, each
with a labelled text span.
Add the missing AddNewClass/AddNewDataset/AddNewModel language strings
so the add buttons read "Add New Class/Dataset/Model" instead of the
raw translation keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The secsToTime() function can now accept the "round" argument.
- Minutes in the secsToTime() function are now displayed as integers, not fractional values.
- The "autoRefresh" button now responds to clicks, allowing you to temporarily disable automatic refresh.
- Display a linear progress bar when deleting logs.
- Added the necessary translation and removed the unused translation for JS
- Other improvements
We analyze the previous autoRefresh state and, if it was enabled before the AJAX request and disabled when the previous request was aborted, also enable autoRefresh after the request successfully completes.
If it was Aborted and autoRefresh=false, then simply display the message "Aborted" in the browser, not "AJAXRequestForLogsWasAborted."
Add a CanIndicatorLight capability and status-aware Indicator toggle button. The indicator LED on the ASH21-B, ADC2W and ASH42-B is controlled via the LightGlobal config (configManager get/setConfig); add indicatorLightOn/Off/Status to Dahua_RPC and a model-specific 'Amcrest ASH42-B RPC' Controls row, with the capability also enabled on the ASH21-B/ADC2W/generic rows. Migration zm_update-1.39.13.sql adds the column.
Add Dahua_RPC keepAlive (global.keepAlive) wired into a 30s zmcontrol idle tick, plus session-expiry re-login retry in set_config and the status queries, so the long-running control daemon does not silently fail after the ~60s session timeout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>