Commit Graph
13687 Commits
Author SHA1 Message Date
singhharsh1708 58ea7e3bb5 refactor: remove the FrameSkip control and its unread member refs #3570 2026-09-02 20:10:36 +05:30
Isaac Connor 9f6f8338e6 Merge pull request #5078 from AJ0070/fix/4215-report-monitor-delete-error
fix: report a failed monitor delete instead of silently ignoring it
2026-08-31 23:10:12 -04:00
Jash 461adbbd20 fix: show the events table error and clear loading on both paths refs #3301 2026-08-31 17:40:09 +05:30
Jash eff68f4103 fix: stop the events table loading forever when the query fails refs #3301 2026-08-30 01:52:22 +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 Connor aa30a5a069 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-08-29 14:03:48 -04:00
Isaac Connor 9c808fe14f fix: request a stream scale that matches the sized zone feed
setStreamScale was called once while the monitors were being started, before
changeScale had sized the feed, so it computed from an unsized frame, hit the
lower clamp and asked for scale=25. On a 1920x1080 monitor shown at 747px wide
that served a 480px image for the browser to enlarge, which looked soft.

changeScale now sets the scale from the width it just fitted, rounded down to
the nearest 5 the way scaleToFit does.
2026-08-29 14:02:53 -04:00
Isaac ConnorandClaude Opus 5 8044af78e2 refactor: derive the ZM_WEB_ bandwidth aliases from the profile prefix
The three profiles' settings were aliased by a switch with one arm per
profile, each repeating the same 18 define() calls against a different
ZM_WEB_<H|M|L>_ prefix. Sixty three lines in which only a single letter
differed, and nothing held the arms in step: a setting added to one and not
the others is undefined for two thirds of users, which is the same blank page
the missing default case caused, just narrower.

Name the settings once and build both sides from the prefix. The two settings
that carried a defined() guard keep it, as a separate list with the fallback
each one uses, so a genuinely absent setting is still distinguishable from a
mistyped one - the rest go through constant() and fail loudly.

Looking up an unknown profile now yields the low prefix instead of skipping
every define, so the skin config no longer depends on skin.php having clamped
the cookie first; that clamp remains the place a bad value is corrected.

Verified by diffing every resulting ZM_WEB_ constant against the previous
implementation for each of the three profiles: 49 constants, identical values.

The test drops the checks that only made sense against the switch and gains
ones for the new shape. It loads the skin config in a child process, once per
profile probed, because constants cannot be redefined and loading it is itself
what can fail. It now catches a mistyped setting name, a removed fallback, a
profile the whitelist does not know, and a setting dropped from the alias list
- the last by way of the per-profile config options, which are the authority
on which settings exist and are independent of the lists under test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-08-29 14:01:25 -04:00
Isaac ConnorandClaude Opus 5 465c3e2dfc fix: reject unrecognised zmBandwidth values instead of undefining every ZM_WEB_ constant
web/skins/classic/includes/config.php defines all 18 ZM_WEB_* constants inside
a switch on $_COOKIE['zmBandwidth'] with cases for high, medium and low and no
default. On any other value none of them are defined, and skin.js.php - emitted
in the footer of every page - reads ZM_WEB_VIEWING_TIMEOUT, ZM_WEB_AJAX_TIMEOUT
and ZM_WEB_REFRESH_NAVBAR. On PHP 8 an undefined constant is a fatal Error, so
every page including login stops rendering until the cookie is cleared, which
cannot be done from inside the interface.

skin.php only tested the value for empty, and nothing else validated it:

- the cookie is set client side by skin.js, so any value survives
- action=bandwidth put $_REQUEST['newBandwidth'] through validStr, which is
  only strip_tags, and persisted it
- ZM_BANDWIDTH_DEFAULT is a free-form string in ConfigData. The Options UI
  renders it as a select, but loadConfig lets a conf.d file override the
  database, so a typo there locks out everyone with no cookie yet

skin.php now validates both the cookie and ZM_BANDWIDTH_DEFAULT before falling
back to low, and the action rejects a value it does not recognise rather than
storing it.

tests/php/test_bandwidth_clamp.php checks the whitelist against the switch it
guards - the two must name the same profiles, since a value in one and not the
other reopens this - and that no arm of the switch defines a constant the
others do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-08-29 14:01:25 -04:00
Isaac Connor 5e80cfedcc feat: add a colour picker for the zone alarm colour
Put a native input type=color beside the R/G/B number inputs, the same
control monitor.php uses for WebColour and SignalCheckColour, seeded from
the zone's AlarmRGB.

The picker has no name, so it is neither submitted nor part of the change
detection snapshot; the R/G/B inputs stay the source of truth for
validateForm and submitForm. alarmColourFromInputs keeps the swatch and
its disabled state in step with those inputs, and is called from
applyZoneType and resetChanges; alarmColourToInputs writes the picked
colour back into them and re-runs the dirty check.
2026-08-29 13:33:16 -04:00
Isaac Connor a80069ee00 feat: move the zone edit buttons into the header toolbar
Move Save, Reset and Cancel out of the column below the video and into a
float-right div in the header, as btn-normal icon buttons with tooltips,
matching the snapshot and report views. Back and Refresh stay on the left.

saveChanges now takes the form from document.zoneForm rather than
element.form, because the Save button no longer sits inside the form.

changeScale uses the stream controls as the scaleToFit bottom element now
that the button row is gone, and sets their width and left margin from the
video, so their left edge lines up with the image.

Drop the right margin from #zonePoints. It is full width inside the settings
panel, so the margin pushed it past the panel and gave the panel a
horizontal scrollbar.
2026-08-29 13:28:39 -04:00
Isaac Connor 8357a22aed feat: rework the zone edit layout so the buttons stay on screen
Widen the settings column from 480px to 540px and move the zone point
table into it, below the settings table, so the left hand column holds
only the video, the stream controls and the Save/Reset/Cancel buttons.

Size the feed with scaleToFit(), the same helper the watch view uses,
passing the button row as the bottom element. skin.js already calls a
global changeScale() from its resize handler, so the video re-fits when
the window changes size instead of pushing the buttons out of the
#page overflow.

Give the settings table a fixed layout with wrapping row headers, and
narrow the pixel inputs, so its three columns stay within 540px. Add
pointIndex/pointX/pointY/pointAction classes to the point table cells,
both in the template and in drawZonePoints, and give the action column a
fixed 80px so the + and - buttons do not wrap onto separate lines.

Columns still stack below 900px wide.
2026-08-29 12:40:02 -04:00
Isaac Connor 268dab15dc Merge pull request #5073 from AJ0070/fix/4850-show-progress
fix: honour ZM_WEB_SHOW_PROGRESS in the event view
2026-08-29 11:26:33 -04:00
Isaac Connor 08f241c0c0 feat: add reset button and changed-point highlighting to zone edit view
Add a Reset button beside Save/Cancel in the zone edit view. It stays
disabled until the form differs from the state loaded from the database,
and restores every field, the zone points and the derived disabled states
when clicked.

Change detection serialises all named form fields, including the
per-point X/Y inputs, and compares against a snapshot taken at the end of
initPage, so manually undoing an edit disables the button again. Number
inputs are compared with parseFloat because updateX/limitPointValue
rewrite 100.00 as 100, which otherwise reads as a change.

Two snapshots are kept: initialValues holds the raw values used to
restore field formatting, initialState holds the normalised string used
for comparison. Point inputs are excluded from the restore because
drawZonePoints re-creates them from the restored points array.

Coordinate inputs of a point that differs from the loaded zone get a
'changed' class with a yellow background, so dragging a point shows which
coordinates moved.
2026-08-29 10:58:00 -04:00
Isaac Connor d73c7b18da feat: let the monitor filters decide the Monitor term's options
The monitor attribute filters and the event filter terms were unrelated
pieces of UI that happened to sit next to each other. They are not
independent: Name, Capturing, Analysing, Recording, Status and Source do
not filter events at all, they narrow which monitors exist to choose
between - which is to say they decide what the Monitor term should offer.
Anything they exclude cannot appear in the events either, so offering it
in the term is offering a choice that returns nothing.

Filter carries the restriction rather than the widget taking it as an
argument: monitor_options() is a get/set in the same shape as terms(), so a
view states the rule on the filter and then asks that filter for a widget.
Unset, the Monitor term offers every monitor the user can view, so
events.php and any other caller is unchanged.

montagereview sets the monitors that survived its filter bar.

Verified on the install this repo matches: the events view still lists all
monitors in its Monitor term, montage review renders its event filters as
Monitor, Date Time, Date Time, Archive Status, Tags, Notes with Name,
Capturing, Analysing, Recording, Status and Source collapsed, and the
events, console and filter views all still load.
2026-08-28 23:07:49 -04:00
Isaac Connor a229f04d0e Make actions can ono longer be performed with POST a debug 2026-08-28 22:31:29 -04:00
Isaac Connor 0e88922676 feat: collapse the monitor attribute filters, keep the selection on show
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
2026-08-28 21:09:36 -04:00
Isaac Connor 4d82e1d385 fix(api): answer 400 naming the field instead of 500 on an unknown filter
FilterComponent::buildFilter() checked the operator against an allow list
but never the field, so an unrecognised name went straight into the SQL:

  GET /api/events/index/MonitorName =:Monitor-1.json
  PDOException SQLSTATE[42S22]: Column not found: 1054
    Unknown column 'MonitorName' in 'where clause'   -> HTTP 500

A caller filtering on a monitor-scoped attribute (MonitorName, Monitor,
Rack, Experiment, Server, Storage) or simply mistyping a column got an
opaque 500 with a stack trace and nothing naming the offending field.

Take an optional list of permitted fields and reject anything else with
BadRequestException.  EventsController::index() passes the Events columns
from the model schema plus the two names it resolves itself: DateTime, the
pseudo-attribute it turns into an overlap test, and GroupId, served by the
Groups_Monitors join.  A field written Model.Field is checked on the column.
The two other callers of buildFilter pass no list and are unchanged.

The invalid-argument and invalid-operator paths threw a plain Exception,
which was also a 500 for what is bad input; they throw BadRequestException
now too.

Verified against a live install:

  MonitorName / Rack / NotAColumn     400, "Unknown filter field: <name>"
  DateTime >=/<= over a window        200, 7 rows, overlap intact
  Cause, Event.Cause, GroupId         200
  ?limit=2&sort=...&direction=desc    200, 2 rows, pagination unaffected

Plus an assert script over the validation itself, including that a caller
passing no list keeps the previous pass-through behaviour.

Filters sent as a query string rather than named parameters go through a
different branch, on raw $_REQUEST, and are deliberately left unvalidated:
that array also carries auth and cache-busting parameters, so rejecting
unknown names there would break existing callers.  ?MonitorName=x is still
a 500 and wants its own change.
2026-08-28 19:54:07 -04:00
Isaac Connor 8358e42832 fix: stop rewriting DateTime to StartDateTime, it disabled the overlap test
a3b1ab558 translated the DateTime filter term to StartDateTime before
putting it in the events API URL, on the belief that DateTime was not a
real column and was being silently ignored.  That was wrong.

EventsController::index() treats DateTime as a pseudo-attribute meaning
"the event was running then" and rewrites a window over it into an overlap
test, using an effective end date that falls back to StartDateTime + Length
and then to NOW() for an event with no EndDateTime (0d7abadb61).
StartDateTime is a plain column, so the same window becomes a containment
test and every event that was already recording when the window opened is
dropped.  With continuous recording the window nearly always opens
mid-event, so montage review began with a blank stretch of timeline.

Measured against a live install, one monitor, 2026-06-23 16:05 to 17:05:

  DateTime      >=/<=   7 rows, earliest 16:01:28, includes the straddler
  StartDateTime >=/<=   6 rows, earliest 16:11:39, straddler dropped

The evidence that led to a3b1ab558 was misread: a DateTime query returning
rows from over a year earlier looked like an ignored filter, but those are
crash-orphaned events with no EndDateTime and no Length, whose effective
end is NOW() and which therefore genuinely do overlap every window.

Keep the Monitor to MonitorId rewrite and the local-variable approach from
df6f48334; only the DateTime line goes.
2026-08-28 19:34:51 -04:00
Isaac ConnorandClaude Opus 5 f2424bcf59 fix: stop AUDIT log entries turning the navbar Log link red
logState() counted every row with Level < INFO and folded anything at or
below PANIC into the FATAL bucket. AUDIT is -5, so audit entries were
counted as fatals and pushed the state to alert/alarm.

Bound the count query at PANIC so AUDIT and NOLOG are left out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAFKf86P78WqniPEP2b45J
2026-08-28 17:29:00 -04:00
Isaac Connor df6f48334f fix: translate filter attrs for the API without rewriting the form
a3b1ab558 translated DateTime to StartDateTime by assigning to attr.value,
which edits the filter form itself, not just the query being built.  The
form is submitted whenever a filter changes, and montagereview.php decides
whether the range terms are already there with

  if (!$filter->has_term('DateTime', '>='))   (montagereview.php:193)
  if (!$filter->has_term('DateTime', '<='))   (montagereview.php:196)

so a form arriving with StartDateTime failed both checks and PHP appended a
second pair.  Selecting a monitor left the filter bar showing four date
terms.

Verified against a live install by submitting the same filter both ways:

  terms as StartDateTime -> 7 terms, 4 date terms
  terms as DateTime      -> 5 terms, 2 date terms

Compute the API attribute into a local and leave attr.value alone.  The
pre-existing Monitor to MonitorId rewrite moves too: it has the same shape
and montagereview.php has its own 'Monitor' handling.

The URL is unchanged, so the DateTime fix still holds; only the side effect
on the submitted form is gone.
2026-08-28 12:52:04 -04:00
Isaac ConnorandClaude Opus 5 cbd68bb323 fix: bind the EventStream status poll to the connkey it belongs to
Montage review created 86 stream connkeys in five minutes while only two of
them were ever polled, and each stream painted a single frame before being
replaced about 1.3s later.

The status poll for a connkey whose zms had exited returned result=Error,
recover() restarted the stream under a new connkey, and the reply already
queued for the old one arrived afterwards and restarted the new stream too.
Every restart painted its first frame, which reset consecutiveErrors and
recoveryDelay in img.onload, so the backoff never grew and the attempt limit
was never reached.

- Tag each ajax/stream.php exchange with the connkey it was issued for and
  ignore a reply, success or failure, once that connkey has been replaced.
  stream.php does not echo the connkey back, so the client tracks it.
- Restart only on reason=no_socket, the one class that means zms is gone,
  matching streamErrorIsFatal() in MonitorStream.js. The helper is duplicated
  as EventStream.errorIsFatal() because montagereview.php does not load
  MonitorStream.js.
- Start the poll timer next to the src that created the connkey instead of in
  img.onload, which is not a dependable per-restart signal for a
  multipart/x-mixed-replace img, and let the first query wait a full interval
  so a stream that is still starting is not read as a missing socket.
- Reset the recovery counters only on a successful status reply, so the
  exponential backoff and the attempt limit both work.

Also stop appending a second '?' to UrlToZMS, which already carries
'?monitor=N', so the logs no longer show monitor=24?source=event.

tests/js/eventstream-connkey.test.js covers the stale-reply rules, the fatal
classification and the URL separator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YEe1M3PWVMqYWihUeAa5N
2026-08-27 22:39:45 -04:00
Isaac Connor b4eaa98478 fix: abort every in-flight event query, not just the last one
loadEventData() fires one request per monitor but assigned them all to a
single `ajax` variable, so only the last was ever abortable.  The
`if (ajax) ajax.abort()` guarding re-entry cancelled one of them; the rest
stayed in flight, landed after the new query, and drew events for a window
the user had already scrubbed past.  This was the //FIXME sitting above
that line.

Track the requests in an array and abort all of them on re-entry.  Also
stop reporting an abort as a failure: jQuery calls the error handler with
textStatus 'abort' for a request we cancelled ourselves, which would
otherwise log one console error per monitor on every scrub.

Checked with an assert script: twelve tracked requests all abort and the
list clears, the old single-variable pattern aborts only one of twelve,
an 'abort' status is not logged while a real error still is, and the list
re-arms for the next query.
2026-08-27 21:12:35 -04:00
Isaac Connor a3b1ab558b fix: translate the DateTime filter term to StartDateTime for the events API
loadEventData() builds the API URL straight from the filter form's terms
and translated only one attribute name, Monitor to MonitorId.  But
montagereview.php expresses both of its time bounds as the DateTime
pseudo-attribute, which is a filter-engine concept rather than an Events
column: Filter.php resolves 'DateTime' and 'StartDateTime' to the same
E.StartDateTime, and groups them together everywhere else it special-cases
date attributes.

The API takes real column names and silently ignores a term it cannot
resolve, so the two bounds were dropped and the view got page one of every
event ever recorded instead of the requested window.  Both requests return
200, which is why nothing surfaced.  Measured against a live install for
the 2026-08-28 00:00:00 to 01:00:00 window:

  DateTime      >=/<=  ->  757 rows, first event 2025-04-26 16:29:55
  StartDateTime >=/<=  ->  300 rows, 00:00:59 to 00:59:59

Translate DateTime the same way Monitor is already translated.  Rewriting
the form element is safe for the PHP path too, since Filter.php treats the
two names identically.
2026-08-27 20:37:34 -04:00
Isaac Connor 56030724ed fix: stop the js log endpoint 500ing on a Firefox line:column location
Firefox reports an error location as "line:column", for example 1500:28.
ajax/log.php passed that through validInt(), which is

  preg_replace('/[^\-\d]/', '', $input)

so it only removes the colon and splices the two numbers into 150028.
Logs.Line is smallint(5) unsigned, max 65535, so the insert died with

  PDOException SQLSTATE[22003]: Out of range value for column 'Line'
    includes/logger.php:438 <- ajax/log.php:81

and the request returned 500.  The endpoint failed on the very error it
was sent to report, so a js error surfaced as "AJAX execution error:
Internal Server Error [500]" instead of a log entry.  Any Firefox error
past roughly line 655 hits this, on any view.

Parse the leading line number instead and clamp it to what the column
holds.  validInt() is left alone; it is used widely and its callers rely
on the strip-everything behaviour, the defect is handing it a line:column
string.

Checked with an assert script over 1500:28, 1789:8, a plain 1500, an
over-range 70000 and 99999:1, empty, non-numeric, and array input; the
first three give the line, the next two clamp to 65535, the rest stay NULL.
2026-08-27 19:54:42 -04:00
Isaac Connor d8f1a7d2d4 fix: stop CORSHeaders warning about empty and same-origin requests
Two cases produced a Warning for a request that needs no CORS headers at
all, so the log filled with noise that pointed at nothing wrong.

An empty Origin header satisfies isset(), so CORSHeaders() walked the
servers list, matched nothing, and logged " is not found in servers list."
with no value to print.  Treat an empty Origin the same as no Origin.

Browsers also send Origin on same-origin POST and fetch.  Such a request
needs no headers, but not finding the host in the Servers table still
warned, so any install reached on a hostname the Servers table does not
list warned on ordinary use.  Log that at Debug instead.  Genuine
cross-origin requests still warn.

Headers are unchanged; this only affects logging and the empty-Origin
short circuit.  The comparison ignores the scheme, so http:// against an
https-served HTTP_HOST counts as same-origin - that only suppresses a log
line, no header is emitted either way.

Checked with a standalone assert script over same-origin with and without
a port on both schemes, differing port, differing host, a suffix near-miss
(hamburg.local vs hamburg) and a missing HTTP_HOST; only the first three
go quiet.
2026-08-27 18:34:54 -04:00
Jash a2ac2eaa0c fix: honour ZM_WEB_SHOW_PROGRESS in the event view fixes #4850 2026-08-26 20:07:00 +05:30
Isaac ConnorandClaude Opus 5 712994b196 fix: only store sessions for clients that carry them
Every request through index.php starts a session and always dirties it:
zm_session_set_remote_addr() writes remoteAddr, and index.php stores skin,
css and navbar_type. ZMSessionHandler::write() then persisted that session
unconditionally, so any request arriving without a ZMSESSID cookie left a
Sessions row behind that nothing would ever load again.

Viewing an event polls the event's server every ZM_WEB_REFRESH_STATUS
seconds via monitorUrl, which is absolute when the monitor has a Server
row. Those cross-origin ajax polls carry auth in the URL and no cookie, so
each one added a Sessions row every few seconds. Bot scans of the login
page did the same.

Persist a session only when the client presented our cookie, or when
zm_session_persist() marks it as one we are issuing: login, and the
postLoginQuery stashed before redirecting to the login page.

Verified on a live install by logging row counts from the save handler:
three cookieless requests skipped the write and left the count unchanged,
while a cookie-jar run wrote on the request that returned the cookie.
php -l clean on both files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAFKf86P78WqniPEP2b45J
2026-08-25 17:36:31 -04:00
Isaac ConnorandClaude Opus 5 bde61e7af6 fix: rotate logs on SIGWINCH instead of SIGHUP fixes #5063
SIGHUP means reload. zmc responds by closing its events, disconnecting the
camera and reconnecting; the perl daemons respond by exiting so zmdc restarts
them. zmdc.pl logrot hupped every managed process, so the nightly logrotate
run cost about 8 seconds of capture on a default install.

Rotating a log file only needs the daemon to drop its file handle, so use a
separate signal for it. SIGWINCH is otherwise unused, is ignored by default and
exists on every supported platform.

- Logger (C++) installs a SIGWINCH handler beside its USR1/USR2 handler. The
  handler only sets a flag; the next logPrint closes the file and the write
  reopens it at the original path. This covers every C++ binary without
  touching any daemon's main loop.
- Logger.pm registers WINCH alongside HUP in logSetSignal, which logInit
  already calls, so the scripts that install their own HUP handler still
  rotate.
- zmdc.pl logrot sends WINCH. The logrotate config is unchanged - it still
  calls zmpkg.pl logrot.

Filter.pm and FilterTerm.php justified MAX_EVENT_DAYS by events not outliving
the nightly HUP, which is no longer what bounds them; cite SectionLength.

Tests: tests/zm_logger_rotate.cpp and tests/perl/test_log_rotate_signal.pl both
log, rename the file out from under the process, confirm writes still land in
the renamed file, signal WINCH and confirm the original path is written again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDhTBPj9xEaT52pRmAufaP
2026-08-22 15:30:46 -04:00
IgorA100 7b4a22d657 Removed the unused MonitorStream.sessionActive() function.
Removed sessionActive method from MonitorStream class.
2026-08-22 13:44:45 +03:00
IgorA100 47c1e72037 Merge branch 'master' into patch-329060 2026-08-22 13:36:51 +03:00
IgorA100 abd00e1a71 - MonitorStream.sessionActive() will be replaced with streamSessionActive() for better compatibility. This way, we won't be tightly coupled to MonitorStream and won't need additional analysis when calling functions.
- onopen and onclose will be race-protected.
2026-08-22 13:33:46 +03:00
IgorA100 e448d9db6b Update audioMotionAnalyzer.js 2026-08-22 00:58:32 +03:00
IgorA100 d61f2cbc38 Update video-stream.js 2026-08-22 00:54:40 +03:00
IgorA100 336e00ac69 Update skin.js 2026-08-22 00:50:40 +03:00
IgorA100 77c7292ca5 Fix: Eslint (MonitorStream.js) 2026-08-22 00:35:45 +03:00
IgorA100 94c2857ff2 Added MonitorStream.sessionActive() function (MonitorStream.js) 2026-08-22 00:25:46 +03:00
IgorA100 5c83800828 Continued fixing of race conditions when switching monitors (MonitorStream.js) 2026-08-21 20:18:13 +03:00
IgorA100 0444db55a4 Fix: If a codec for Rtsp2Web playback is missing, always log the error as a fatal error, not just when players are automatically selected.
This will ensure more correct code execution. This is especially true for switching to the next monitor in PR https://github.com/ZoneMinder/zoneminder/pull/4980
2026-08-21 16:13:39 +03:00
Isaac Connor 326181ea4f Merge pull request #5059 from IgorA100/patch-371115
The "rand" parameter change for SRC has been separated into a separate function, and a function for replacing SRC has been added.
2026-08-20 22:59:03 -04:00
IgorA100 fb2be9d28a Merge branch 'master' into patch-483073 2026-08-20 20:34:31 +03:00
IgorA100 b036b65b86 Reverted the missing src definition to stream.src. 2026-08-20 16:47:52 +03:00
IgorA100 32f0e37583 - Changing the "rand" parameter for SRC has been separated into a separate function, and a function for replacing SRC has been added.
- Instead of Math.random(), we'll use Date.now().
This will make the code cleaner and more understandable.
- We also now use refreshStreamSrc() when stopping stream playback.
2026-08-20 16:42:28 +03:00
IgorA100 a740e84def Update MonitorStream.js 2026-08-20 12:57:49 +03:00
IgorA100 04ebede7a7 Added setSrc() function for cache busting when SRC changes 2026-08-20 12:47:18 +03:00
Isaac Connor 022410c299 fix: stop abandoned events matching every DateTime window, and index the query
A DateTime lower bound was written as

  COALESCE(E.EndDateTime, '9999-12-31 23:59:59') >= T1

reading "an event that has not ended yet never ends". EndDateTime is NULL for
an event that is still recording, but also for one zmc was killed part way
through, and that stays NULL forever - so every abandoned event matched every
window from then on. Wrapping the column in a function also meant no index
could range over it: with the only MonitorId key being MonitorId alone, every
montage review request read all of that monitor's events and filtered them in
memory, however narrow the window.

Length tells the two cases apart. It is flushed during recording, so a live
event's effective end keeps advancing while an abandoned one's is frozen at
whatever was recorded. Use the same CASE the SELECT list in ajax/events.php
already uses, and bound it below by StartDateTime.

The lower bound now emits three conjuncts:

  E.StartDateTime >= DATE_SUB(T1, INTERVAL 1 DAY)
  AND (E.EndDateTime IS NULL OR E.EndDateTime >= T1)
  AND <effective end> >= T1

The floor bounds the scan for a window in the past and drops events abandoned
more than a day earlier - events do not outlive the nightly logrotate SIGHUP,
which stops and restarts them, so a day is comfortably beyond any real event.
The middle conjunct is implied by the third and exists only to give the
optimiser a second indexable handle: it is narrow exactly when the floor is
wide, so between them a window at either end of the retention period has
something cheap to range over. Verified the optimiser picks correctly for both,
unprompted. The third is the residual that gets the semantics right.

Add the two composite keys those conjuncts need. Two range columns cannot both
narrow one B-tree, and these are mirror images - EndDateTime >= T1 is
open-ended upwards, StartDateTime <= T2 downwards:

  Events_MonitorId_StartDateTime_idx (MonitorId, StartDateTime)
  Events_EndDateTime_MonitorId_idx   (EndDateTime, MonitorId)

MonitorId leads the first because it is the equality; past a range column later
columns can no longer narrow the scan, and (StartDateTime, MonitorId) measured
3.5x slower for the same rows. EndDateTime leads the second so it also covers
zmaudit's hunt for events that were never closed, which has no monitor to scope
it - 9 rows with a key against a 22,948 row table scan without.

Both replacements are added before the keys they supersede are dropped:

  Events_MonitorId_idx (MonitorId) is now a leftmost prefix of the new key.
  Events_EndDateTime_DiskSpace (EndDateTime, DiskSpace) existed for the scan
  that hunted events with no DiskSpace set; DiskSpace is set when the event is
  finalised in C++, so nothing scans for it any more.

Net index count on Events is unchanged.

Measured on a 7,167 event monitor. Default one hour window: 7,055 rows
examined / 25.4ms -> 173 rows / 4ms. Window scrubbed a week back, which
persists in the zmFilter_StartDateTime cookie: 6,995 rows / 28ms -> 19 rows /
0.15ms.

Adds migration db/zm_update-1.39.22.sql. Extends t/filter_sql.t; the PHP and Perl were
checked to emit identical SQL, and the semantics checked against a table of
seven event shapes - abandoned long ago, abandoned recently, overlapping,
inside, before, still recording, and still recording for 17 hours.
2026-08-19 22:06:21 -05:00
Isaac Connor 831c4ce6b6 fix: lock filter events one at a time instead of locking the whole batch
A filter with LockRows set selected its entire result set FOR UPDATE inside one
transaction, so every lock the per-event work went on to take was held until
the run committed:

  Events[Id] -> Events_Hour/Day/Week/Month[EventId] -> Event_Summaries[MonitorId] -> Storage[Id]

Because the locks accumulated across events, two filters deadlocked: one held
Event_Summaries for a monitor while waiting on a Storage row, the other held
that Storage row while waiting on Event_Summaries for its next event. No
per-event lock ordering can fix that while both rows stay locked for the length
of the batch.

Holding Event_Summaries for the whole run also blocked zmc from opening a new
event on any monitor the filter had touched, since creating an event updates
that row. The transaction spanned ffmpeg encodes, uploads and executed commands
as well, so it could be held open for minutes.

zmfilter now claims one event at a time in Events_Lock and releases it when it
is done with that event, so no InnoDB lock is held across the work. The
per-event body moves into checkFilterEvent.

skip_locked now adds NOT EXISTS over Events_Lock to the filter query rather
than SKIP LOCKED. The exclusion has to happen in the query: a filter whose
whole result set was held elsewhere would otherwise fill its LIMIT with events
it could only skip, and make no progress. It no longer depends on MariaDB 10.6
/ MySQL 8.0.1, so the UI no longer disables the option on older servers.

Also drops the two dbh->commit() calls in the AutoCopy branch. With no
transaction open they would warn, and before this they were silently ending the
batch transaction mid-loop, so AutoCopy filters never had the guarantee
LockRows was supposed to give them.

filterdebug.php was appending a bare ' SKIP LOCKED' after the LIMIT, which is
not valid SQL; it now renders the real clause in the right position.

Adds t/event_lock.t and t/filter_sql.t.
2026-08-19 22:06:02 -05:00
IgorA100 bbb4ecdf76 Replaced Russian text with English. 2026-08-19 18:57:54 +03:00
IgorA100 4e8cf0f6d7 - We'll assign the listener for 'zm:tracksReceived' to stream instead of "document."
- When closing a socket, check the session before clearing the WebSocket, not after.
- Execute resetCountStreamErrors() when playback starts successfully.
- Recheck the session inside videoEl.onplay after the 500ms timeout.
2026-08-19 18:53:36 +03:00
IgorA100 2e5f23d2f0 Fix Eslint MonitorStream.js 2026-08-19 00:16:22 +03:00