405 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 5 c7deb3faa8 feat: score monitors on how loud their audio is
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
2026-09-11 20:13:02 -05:00
Isaac Connor eafdffcd95 feat: add per-monitor actions that drive a light or a speaker on alarm
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)
2026-09-11 20:13:02 -05:00
singhharsh1708 f4d64666c3 fix: remove the Function modal, nothing can reach it and it fatals 2026-09-06 11:45:57 +05:30
singhharsh1708 aad6c4bfcb refactor: retire the FrameSkip column refs #3570 2026-09-02 20:28:37 +05:30
Jash a0ef89d1f3 fix: report a failed monitor delete instead of silently ignoring it fixes #4215 2026-08-30 01:37:45 +05:30
Isaac ConnorandClaude Opus 4.8 92ab811720 feat: publish analysis images through a shared-memory ring
Replace the single alarm_image slot with an analysis_image_buffer ring of
image_buffer_count Images living in the already-reserved alarm_images SHM
region. Successive WriteAlarmImage calls rotate through the ring and
publish last_analysis_index last (after the bytes and per-slot format),
so a reader sampling last_analysis_index always sees a fully written
slot. GetAlarmImage returns that slot, syncing its AVPixelFormat from the
per-slot analysis_image_pixelformats array.

SharedData gains last_analysis_index and analysis_image_count (plus 8
bytes of padding to keep the 16-byte-multiple layout), making it 888
bytes. The Perl (Memory.pm) and PHP (Monitor.php) SHM readers are updated
in lockstep, and a static_assert(sizeof(SharedData)==888) in zm_monitor.h
guards the layout against silent drift.

This lets multiple in-flight analysis/annotated frames be buffered and
streamed in sync rather than always overwriting one slot, and gives the
AI object-detection work a place to publish annotated frames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:32:51 -04:00
Isaac ConnorandClaude Opus 4.8 e2ed9287d7 fix: correct PHP SHM SharedData/TriggerData field offsets
The C++ SharedData struct is naturally aligned, not packed, so the
compiler inserts a 4-byte pad before capture_fps (after state) and
another before the startup_time union (after audio_channels). Monitor.php
used the naive packed offsets, so every field from capture_fps onward
(capture_fps/analysis_fps, latitude/longitude, the time fields,
alarm_cause and all of TriggerData) was read from an address 4-8 bytes
too low, yielding garbage.

Correct the offsets to the real aligned layout (SharedData is 872 bytes,
TriggerData starts at 872), matching what ZoneMinder::Memory computes and
what the C++ writes. No struct change; this is a reader-side fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:30:23 -04:00
IgorA100 3983de5d4b Merge branch 'master' into patch-401263 2026-06-14 10:18:33 +03:00
IgorA100 c114625c53 Allow diacritics and non-ASCII digits (Monitor.php) 2026-06-13 16:59:42 +03:00
IgorA100 82d45575e6 Merge branch 'master' into patch-401263 2026-06-13 15:32:51 +03:00
SteveGilvarryandClaude Opus 4.8 81227683b2 fix: pass monitor Id through validCardinal in Janus_Pin shell command
Janus_Pin() interpolated $this->Id() directly into the zmu command
string passed to shell_exec(), while the sibling AlarmCommand() path
already routes the same value through validCardinal() before building
its command. Bring the two into line so every Id reaching a shell sink
is validated as a cardinal number.

Id is the monitor's integer primary key and is not user-controlled, so
this is a consistency and defense-in-depth change rather than a fix for
an exploitable issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:47:23 +10:00
SteveGilvarryandClaude Fable 5 a918735e9c fix: correct stale SharedData and TriggerData offsets in PHP shm map
The PHP shared memory map in web/includes/Monitor.php was not updated
when last_analysis_viewed_time was added to SharedData (zm_monitor.h,
offset +152), so every field from control_state onward was read 8 bytes
early: control_state, alarm_cause, video_fifo, audio_fifo and janus_pin.
The TriggerData base offset (864) had been updated, so only the
SharedData tail was skewed. The error goes undetected because connect()
only checks the 'valid' byte at +88, which sits in the unaffected
region, and never compares the 'size' field.

Also corrects the TriggerData showtext offset: 'text' is 256 bytes at
912, so 'showtext' starts at 1168, not 1268.

The Perl map (Memory.pm.in) computes offsets from the field sequence
and is unaffected.

Tests: php -l passes; offsets verified field-by-field against the
SharedData struct in src/zm_monitor.h on master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:06:38 +10:00
IgorA100 245111c956 Merge branch 'master' into patch-401263 2026-06-06 23:24:08 +03:00
IgorA100 671e2bc58a Restored the missing <canvas> for audioVisualization (Monitor.php)
Although audioMotion-analyzer.js can create its own <canvas>, we use our own <canvas> because it speeds up performance and avoids some issues.
2026-06-04 20:27:42 +03:00
Isaac ConnorandClaude Opus 4.8 fd83672a11 feat: add status-aware Light control capability and control-command response path refs #4869
Add a CanLight control capability rendering a single status-aware Light toggle button. The ADC2W white light is driven via CoaxialControlIO.control (Type 1, numeric IO); the button queries live state and reflects it (amber when on).

To get device state to the browser, add an opt-in two-way response path to the control protocol: zmcontrol writes a JSON result back only when a request sets wants_response (fire-and-forget commands unchanged, SIGPIPE-safe); Monitor::sendControlCommandWithResponse and ajax/control.php return it.

Also adds get_config/set_config/probe to Dahua_RPC for characterising cameras, the CanLight column (migration zm_update-1.39.12.sql), edit-UI checkbox, and a config unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:00:00 -04:00
IgorA100 60a8a59585 Merge branch 'master' into patch-401263 2026-06-01 12:49:50 +03:00
IgorA100 1acc859f9a "Id" is now public, not protected. (Monitor.php) 2026-05-29 16:14:04 +03:00
IgorA100 ad269916f3 - Monitor name validation is now performed via an AJAX request in a PHP file.
- Added translations for English and Russian.
- Added a method to the Monitor class for retrieving default settings. This is necessary to retrieve the 'filter_regexp' of the monitor name, so as not to re-specify 'filter_regexp' during an AJAX request.
- Added the \web\ajax\monitor.php file.
2026-05-26 16:23:39 +03:00
IgorA100 823f737fb8 Allow spaces in the monitor name. 2026-05-02 23:16:16 +03:00
IgorA100 ac13c1aa96 I modified the regular expression in JS to validate the monitor name input, taking into account national Unicode characters.
In PHP, I'll simplify the regular expression.
2026-05-02 23:10:55 +03:00
IgorA100 e4a96d191e Missing character (Monitor.php) 2026-05-02 20:19:37 +03:00
IgorA100 44ca4abc89 Allow national Unicode characters in monitor name (Monitor.php) 2026-05-02 20:17:35 +03:00
IgorA100 f575a8975c Analyzing AUDIO_MOTION_ENABLED and checking $whatDisplay using strtolower (Monitor.php) 2026-04-19 20:26:28 +03:00
IgorA100 3aa78accce Added analysis of the AUDIO_MOTION_ENABLED constant (Monitor.php) 2026-04-19 18:49:49 +03:00
Isaac ConnorandCopilot b7a0c1dad1 Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-12 08:48:43 -04:00
IgorA100 89a983984e Merge branch 'ZoneMinder:master' into patch-435415 2026-03-19 12:33:45 +03:00
Isaac ConnorandClaude Opus 4.6 5561829450 fix: include username in auth relay and fix stale auth in stream restart
- Add user= parameter to get_auth_relay() so zms can use the indexed
  Username column instead of iterating all users to validate the hash
- Apply the same fix to Event.php getStreamSrc() and getThumbnailSrc()
- Tighten Monitor.php from isset() to !empty() for consistency
- In MonitorStream.js start(), check if the auth hash in the img src
  matches the current auth_hash before resuming via CMD_PLAY. If stale,
  fall through to rebuild the URL with fresh auth_relay. This prevents
  long-running montage pages from spawning zms with expired credentials.
- Downgrade zms auth failure from Error to Warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 10:01:47 -04:00
IgorA100 524ab91cc3 Added audio visualization 2026-03-15 00:50:35 +03:00
IgorA100 c405832390 Merge branch 'master' into patch-435415 2026-03-14 23:04:35 +03:00
Isaac ConnorandClaude Opus 4.6 3dfe185260 fix: resolve role-based group permissions not granting stream access
The C++ User class (used by zms for streaming) had no awareness of
roles. It only checked user-direct permissions from Monitors_Permissions
and Groups_Permissions tables, completely ignoring Role_Monitors_Permissions,
Role_Groups_Permissions, and User_Roles base permissions. This caused
users who received camera permissions via Roles to be denied live stream
access, even though the PHP web interface (which has its own role-aware
checks in visibleMonitor()) showed the monitors correctly.

Changes:
- Add role_id to C++ User class, loaded via COALESCE(RoleId, 0) in all
  SQL queries (find, zmLoadTokenUser, zmLoadAuthUser)
- Add loadRoleBasePermissions() to merge role's Stream/Events/Monitors/
  etc. as fallback when user's own permission is PERM_NONE
- Add findByRole() to Group_Permission and Monitor_Permission classes
  to query Role_Groups_Permissions and Role_Monitors_Permissions tables
- Extend User::canAccess() to check role monitor and group permissions
  after user-direct permissions, matching the PHP visibleMonitor() logic
- Fix Monitor::canView() in PHP to also check role permissions when
  called for a user other than the global $user
- Fix off-by-one in zmLoadTokenUser where dbrow[10] read TokenMinExpiry
  out of bounds (was at index 9); adding RoleId shifts it to index 10

Fixes #4692

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:07:14 -04:00
Isaac ConnorandClaude Opus 4.6 419846c875 fix: sanitize monitor Device path to prevent command injection (GHSA-g66m-77fq-79v9)
The Device field from the Monitors table was interpolated directly into
shell commands (qx(), backticks, exec()) without sanitization, allowing
authenticated users with monitor-edit permissions to execute arbitrary
commands as www-data via the Device Path field.

Defense in depth:
- Input validation: reject Device values not matching /^\/dev\/[\w\/.\-]+$/
  at save time in both web UI and REST API
- Output sanitization: use escapeshellarg() in PHP and quote validated
  values in Perl at every shell execution point

Affected locations:
- scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm (control, zmcControl)
- scripts/zmpkg.pl.in (system startup)
- web/includes/Monitor.php (zmcControl)
- web/includes/functions.php (zmcStatus, zmcCheck, validDevicePath)
- web/includes/actions/monitor.php (save action)
- web/api/app/Model/Monitor.php (daemonControl, validation rules)
- web/api/app/Controller/MonitorsController.php (daemonStatus)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:19:03 -04:00
Isaac ConnorandClaude Opus 4.6 44acdd9b95 fix: guard ModelId access with property_exists in Monitor::Model()
Prevents 'Undefined property' PHP warning when the Monitor object was
loaded from a database that doesn't yet have the ModelId column. Matches
the existing property_exists pattern used later in the same method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:50:40 -05:00
Isaac ConnorandClaude Opus 4.6 a90a3bccea fix: auto-detect and convert pixel zone coordinates to percentages in web layer
When zone coordinates are stored as pixel values (e.g. from a missed DB
migration), the web layer now detects values > 100 and converts them to
percentages using the monitor's dimensions, mirroring the existing C++
detection logic in zm_zone.cpp. This prevents limitPoints() from clamping
pixel values to 0-100 and zones rendering incorrectly in SVG overlays.

- Add convertPixelPointsToPercent() helper in functions.php
- Call conversion before limitPoints() in zone.php and zones.php
- Update Zone::svg_polygon() to accept monitor dimensions and convert
- Pass ViewWidth/ViewHeight to svg_polygon() from Monitor::getStreamHTML()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:49:14 -05:00
Isaac Connor 9ebf48da35 Add Create to canEdit 2026-02-26 07:22:31 -05:00
Isaac ConnorandClaude Opus 4.6 c0016fa00b feat: store zone coordinates as percentages for resolution independence
Convert zone coordinates from absolute pixel values to percentages
(0.00-100.00) so zones automatically adapt when monitor resolution
changes. This eliminates the need to manually reconfigure zones after
resolution adjustments.

Changes:
- Add DB migration (zm_update-1.37.81.sql) to convert existing pixel
  coords to percentages, recalculate area, and update Units default
- Add Zone::ParsePercentagePolygon() in C++ to parse percentage coords
  and convert to pixels at runtime using monitor dimensions
- Backwards compat: C++ Zone::Load() checks Units column and uses old
  pixel parser for legacy 'Pixels' zones
- Update PHP coordsToPoints/mapCoords/getPolyArea for float coords,
  replace scanline area algorithm with shoelace formula
- Update JS zone editor to work in percentage coordinate space with
  SVG viewBox "0 0 100 100" and non-scaling-stroke for consistent
  line thickness
- Position zone SVG overlay inside imageFeed container via JS to align
  with image only (not status bar)
- Support array of zone IDs in Monitor::getStreamHTML zones option
- Update monitor resize handler: percentage coords don't need rescaling,
  only threshold pixel counts are adjusted
- Add 8 Catch2 unit tests for ParsePercentagePolygon

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:19:20 -05:00
Isaac ConnorandClaude Opus 4.5 eae89025ee refactor: rename RTSP2WebStream to StreamChannel
Rename applies to Go2RTC, Janus, and RTSP2Web streaming options.
Update enum values from Primary/Secondary to Restream/CameraDirectPrimary/CameraDirectSecondary.

- Add db migration zm_update-1.37.79.sql to rename column and migrate data
- Update C++ enum StreamChannelOption and member stream_channel
- Update PHP getStreamChannelOptions() method
- Update all JavaScript references
- Auto-select CameraDirectPrimary when Restream option becomes disabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:37:08 -05:00
Isaac Connor 02685bff7e Pick up missed update for Restream and RTSP_User 2026-01-31 18:54:14 -05:00
Isaac ConnorandClaude Opus 4.5 010959c9d4 fix: only show monitor edit button if user has edit permission
The edit button overlay on the montage view stream is now conditionally
displayed based on the user's edit permission for that monitor.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 13:56:03 -05:00
Ben Dailey c80542343a Remove Preview Rate Control. Disable BuiltIn Video controls. 2026-01-29 11:18:30 -05:00
Isaac Connor e6c5bdb85d Use modern string interpolation style 2025-12-22 19:43:05 -05:00
Isaac Connor 254fd58c40 Use css style= to set width of video tag, and if height is specified, also set it. Fixes streams not having height. 2025-12-19 12:03:29 -05:00
Isaac Connor 6c2ad8d906 Use the same code for ImageStill and for ImageStream. Use ImageStream when mode == paused. Fixes #4491 2025-12-19 11:10:32 -05:00
Isaac Connor 736610d349 Simplify monitor state html, mostly to include the stream-info into the part that gets hidden with the other state info 2025-10-29 12:15:32 -04:00
Isaac Connor 9932c1f690 Add missing EncoderHWAccel fields 2025-10-27 09:40:06 -04:00
Isaac Connor ecd37ae6c7 Revert change to assign 4 streams per port. It caused more trouble than it was worth 2025-10-16 12:13:53 -04:00
Isaac Connor 83800e8753 Test for existend of type in defaults to avoid warning 2025-10-10 13:26:57 -04:00
Isaac Connor b5a10ca1ce Add +1 because we will now use 30000 instead of 30001.. so some existing configs may break. 2025-10-09 11:01:08 -04:00
Isaac Connor 1fc27261e1 Add initial_scale property 2025-10-07 21:02:30 -04:00
Isaac Connor e04d38f83e When using MIN_SPREAMING_PORT, divide by 5 so we get 5 streams per port 2025-10-01 21:01:29 -04:00
Isaac Connor 3373fb71aa Simply zms option let's just assume that that we can stream 2025-09-25 17:36:13 -04:00