- When calling `monitorStream.manageAvailablePlayers()`, now pass the candidate player as an argument instead of determining the player from stored cookies inside `manageAvailablePlayers()`. This is necessary because cookies could belong to either the Watch page or the Zone page.
- On the Zone page, load `hls.min.js` if Go2RTC or RTSP2Web is being used for the monitor.
- `monitorStream.setPlayer()` is now executed within `monitorStream.manageAvailablePlayers()`. Previously, `manageAvailablePlayers()` only handled the assignment of `monitorStream.player`.
- Added a new function, `this.setRTSP2WebType()`, to avoid code duplication. This ensures that `RTSP2WebType` is checked and set even if `setPlayer()` is not called on the page.
- Minor optimization regarding variable usage.
- If `hls.min.js` is not loaded, attempting to play via HLS will now trigger a "The HLS library is not loaded" warning in the browser console and initiate a stream restart. Previously, this would result in an error.
- The `janus` variable is no longer global; it now belongs to `MonitorStream`.
- In the `Janus.init` callback, do not perform any actions if the monitor is in a stopping state or is already stopped.
- Check for the existence of the `destroy` method before calling `janus.destroy()`.
- Allow the Janus player to be selected in `this.selectPlayer` regardless of the `streamChannel` on the Montage page. This will not cause visual confusion, as the Montage page does not allow selecting a `streamChannel` (it uses the default one).
However, if the default `streamChannel` is a secondary one and the default player is set to Janus, the Janus player would never be selected on the Montage page. I believe this is incorrect, as we should allow for more flexibility.
A boolean option set in zm.conf or conf.d wins over the Config table, and a
person editing a file writes yes or off rather than 1 or 0. The C++ daemons
now read those words (ConfigFileValue, 4d7609d36), but PHP and Perl took the
value as written, and a non-empty string is true in both. So
ZM_STRICT_VIDEO_CONFIG=off in conf.d left the option on in the web UI and the
Perl scripts while zmc turned it off.
Read a file value for a boolean option the same way in all three: yes, true
and on are 1; no, false and off are 0; anything else stays as written. Only
booleans are read this way, so a string option such as a title keeps "Yes".
PHP defines the file values as constants before it reads the Config table,
since it needs the database settings to connect, and only the table says
which options are booleans. So a word now waits until the table has been
read, apart from ZM_DB_* settings, whose words database.php reads itself.
Perl reads the type with each row.
Tested on 1.38's zm138 database with a conf.d file setting
ZM_OPT_FAST_DELETE=yes (0 in the table), ZM_STRICT_VIDEO_CONFIG=off (1)
and ZM_WEB_TITLE=Yes. The installed PHP and Perl gave 'yes', 'off' and
'Yes'; these give '1', '0' and 'Yes'. tests/php/test_config_file_booleans.php
checks the rule.
refs #5289
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zm.conf and conf.d are set by the owner of the box; the Config table by the
ZoneMinder admin through the Options page. The owner has to be able to limit
the admin, so a file setting must win. PHP and Perl already let it.
process_configfile() kept only a fixed list of names (database credentials,
paths, server identity, stream socket) and dropped the rest, so zmc, zms and
zmu read every other option from the database alone.
So with ZM_OPT_USE_AUTH=1 in conf.d and 0 in Options, the web UI asked for a
login while nph-zms served streams and snapshots to anyone. The same split
applied to every option, ZM_AUTH_RELAY and ZM_AUTH_HASH_IPS among them.
process_configfile() now records every name = value in
StaticConfig::file_options, later files winning, and zmLoadStaticConfig()
clears it first so a reload forgets a removed line. Config::Load() applies
those values over the database ones, also when the database can't be read.
A boolean may be written yes/no, true/false or on/off as well as 1/0
(ConfigFileValue), since the Config table's 1/0 convention is not what
people type into a file. Overrides are logged by name only, as the value may
be a secret.
Tested live on 1.38: with ZM_OPT_USE_AUTH=1 in conf.d and 0 in the database,
an unauthenticated nph-zms snapshot request answered 200 with an image
before and 403 after. Without the override it still answers 200.
refs #5289
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
checkvars looped over the values of the five variables, unquoted, so an
empty one dropped out of the list and was never checked: the build went
on with a malformed VERSION/RELEASE instead of stopping with exit 98.
Loop over the variable names and test each value with indirect
expansion. A test that empties each variable in turn shows checkvars
now stops for every one of them; with all five set it continues as
before.
release-source-tarball pasted ${{ inputs.tag }},
${{ github.event.release.tag_name }} and the step outputs derived from
them straight into the run: scripts. GitHub substitutes expressions
into the script text before bash parses it, so a tag containing shell
syntax ran as code in a job with contents: write. Only people who can
publish a release or dispatch the workflow can set the tag, so this is
hardening, not an exposure.
Pass each value through env: and read it as a shell variable, as
GitHub's security hardening guide recommends. Normal tags produce the
same outputs and tarball name as before; the `with:` inputs are not
shell and are unchanged.
An event that closes on a keyframe ends in a fragment holding that frame
alone. The muxer gives the file's last sample the packet's duration, which
camera packets leave at 0, so the last sidx reference would last 0 ticks
and build_sidx_region refused the whole index ("Could not index ...; it
will play, but a player must read it all before the first frame").
Fold a zero-length last reference into the one before it. That reference
still points at a real moof, so seeking is unchanged. A lone zero-length
fragment, or one mid-file, is still refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MonitorLink::score() reported an alarm whenever the linked monitor's
last_event_id changed (4356a6dea), to catch an alarm that went from ALARM
through ALERT to IDLE between two checks. A new event is not an alarm,
though. A monitor recording continuously starts a new event at every section
boundary, so each one gave its linked monitors a "Linked" alarm, and an
on-demand one a camera reconnect, with no motion anywhere. A monitor with
Recording=None never opens events, so for it the latch did nothing.
Report alarm status only: the frame score in ALARM, and 1 in ALERT so a linked
event stays open through the source's post-event frames (8fc1992e4). The state
stays in shared memory until the source analyses its next frame, and the
receiving monitor checks the link on every packet (every 100 ms while an
on-demand one is paused), so an alarm is still seen.
tests/zm_monitorlink_score.cpp points a MonitorLink at a local SharedData and
checks IDLE, ALARM and ALERT, and that a new event while idle scores 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A monitor with Capturing=Ondemand and LinkedMonitors never recorded a linked
alarm unless someone happened to be watching it. Links are evaluated in
Analyse(), per captured packet, and zmc pauses an on-demand camera, stopping
its analysis thread, 10 s after the last viewer. While paused nothing looked
at the links, so every alarm on the linked monitor was missed. This is the
low-bandwidth setup of a substream that analyses and a main stream that
records on demand.
zmc's on-demand decision moves into Monitor::CheckOndemand(), around a
DecideOndemand() table that can be tested:
- While paused, the links are checked on every pass. The analysis thread is
stopped then, so the capture thread reads them without racing it. An alarm
reopens the camera.
- An unwatched monitor keeps capturing while it is alarmed or in its
post-event frames, instead of pausing in the middle of the event.
- A link only reports an alarm while it lasts, and reopening the camera can
take longer than that (2.5 s against a local camera). So the wake is
remembered for up to 30 s, and the first packet Analyse() scores counts it
as a linked alarm, then clears it.
Tested live on 1.38 with a paused on-demand passthrough monitor linked to an
analysing one. Before: an 8 s forced alarm on the source recorded nothing.
After: the monitor woke within 0.1 s, alarmed 2.5 s later and recorded the
event. A 1 s alarm, over before the camera reopened, was also recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dbQuery()'s two error paths for a prepare() or query() that returns false
logged $pdo->errorInfo. $pdo is not defined there, so PHP warned twice and
the message ended without the error. They also read dbLastError() from the
connection only after ZM\Error() had logged; with a database log target that
inserts into Logs through the same connection, which replaces its errorInfo,
so dbLastError() came back as "00000" instead of the error.
Read the error once, before logging, and use it for both. The query() path
also said "Error preparing" for a statement it ran directly.
These paths run when PDO's error mode doesn't throw; dbConnect() sets
ERRMODE_EXCEPTION, so normally the catch below handles the failure.
tests/php/test_database_error_paths.php drives both paths with
ERRMODE_SILENT and checks for no PHP warning and the real error text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dbConnect() turns emulated prepares off, so every query of the web UI, the
ajax calls and the includes the API runs (config, servers, users,
permissions) is a server-side prepare: prepare, execute and close, three
round trips to the database where one would do. From PHP 8.1, pdo_mysql
returns integers and floats as PHP ints and floats with emulated prepares
too, so values keep the types they have now. Emulate prepares there and keep
server-side prepares on older PHP, where every value would become a string.
Emulated prepares quote parameters bound as strings, which breaks LIMIT ?.
dbQuery() now binds integer values as integers (everything else as strings,
as execute() did), and the log and watch ajax calls cast their validated
offset and limit to int. With emulation, PDO would also run several
statements given in one string; MYSQL_ATTR_MULTI_STATEMENTS is turned off so
that is refused as before. CakePHP's own connection is not changed: it
already uses PDO's default, emulated prepares.
tests/php/test_database_emulated_prepares.php compares every column of every
table read through both kinds of prepare (types and values), checks
aggregates, LIMIT with positional and named integers, string and null
parameters, and that two statements in one string are refused.
#5276 re-parsed LinkedMonitors in Reload() because nothing else did until
the next reconnect. Since #5239, every reload is a reconnect: zmc Close()s
and disconnect()s the monitor, calls Reload(), then connect(), and
connect() already calls ReloadLinkedMonitors(). The call in Reload()
queried the database once per linked monitor for an expression connect()
then threw away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A YUV420P chroma sample covers 2x2 luma pixels. Image::Crop started the
chroma planes at lo_x >> 1 and lo_y >> 1, so a crop starting on an odd
column or row had its chroma half a pixel off its luma. nph-zms zoom picks
any position, so a zoomed live view could show colour fringes.
For planar formats, move the crop window back to the chroma grid before
copying, keeping its size. Moving left or up can't leave the image.
Also use the existing ceil_rshift() for the chroma copy sizes instead of
writing the same calculation inline, moving it above Crop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Monitor::CheckAction() handled the RELOAD action (zmu --reload, or a script
setting it through ZoneMinder::Memory) by calling Reload() in place. That
runs on zmc's capture thread, and Monitor::Load() rewrites settings such as
label_format, orientation and the image sizes while the decoder and analysis
threads read them unlocked.
SIGHUP already reloads safely: zmc leaves its capture loop, Close()s and
disconnect()s each monitor, which stops those threads, then Reload()s and
reconnects. Have the action set zm_reload, as the SIGHUP handler does, so
both take that path.
zmc reloads every monitor it runs this way, not only the one the action was
sent to; zmc normally runs one.
Tested live on 1.38 with zmu --reload against a running zmc: the event closes,
the same process reloads and reconnects, capture resumes and a new event
opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
std::stoul returns an unsigned long, and Token assigned it to an unsigned
int monitor or zone id. On 64-bit, 99999999999 parsed and was truncated
to 1215752191, so a LinkedMonitors value could quietly link a different
monitor than the one typed. It only failed because that id didn't exist.
Move the parsing into Token::parse_monitor_link(), which rejects an id
above UINT_MAX like any other value that isn't a number, so the token is
ignored with the same warning. The function is static so the test can
check the parsed ids without a database.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused nph-zms request (no or bad credentials, or a monitor or event
the user may not view) wrote only the raw NPH status line,
HTTP/1.0 403 Forbidden. Apache passes it through, but behind nginx and
fcgiwrap it is not taken as the status: the client got 200 with an empty
body, so an app could not tell a refusal (say, an expired token) from an
answer. Under the plain zms name Apache answered 500 for the same line.
Write a Status: 403 header as well in NPH mode, and only that header
otherwise, so every server answers 403.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>