Commit Graph
652 Commits
Author SHA1 Message Date
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
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 d61f2cbc38 Update video-stream.js 2026-08-22 00:54: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 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 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
IgorA100 b4778f1492 - Added isActive to MonitorStream.
- Exit this.img_onload if the monitor is already stopped.
- Assign the 'zm:tracksReceived' listener only after stream playback has started, not during start().
- Additional processing of MonitorStream.playbackSessionId
- Calling stream.load() inside stop() for the rtsp2web type MSE is redundant and can lead to unnecessary warnings. We do everything necessary in stopMse().
- When restarting a stream, call updatePlayerControls() instead of streamCmdStop() to avoid duplicating the stop() command (relevant for the Watch page).
- Now, to switch to the next player, we won't call MonitorStream.selectNextPlayer() in different places in the code. Instead of selectNextPlayer(), we will always call restart(), which will select the next player. This will allow us to more accurately clear all unnecessary objects. To achieve this, we have slightly modified restart(). - For go2rtc, execute playbackSessionId = generateUUID() when changing the SRC, not when selecting a player, since go2rtc itself can change src.
- Avoid looping when executing selectNextPlayer() during errors in the ZMS player.
- Added the "fatal" argument to streamErrorRegistration(), which prevents unnecessary relaunches of the same player that was previously playing in the event of a fatal playback error.
- Added a setter for src to the VideoStream class for go2rtc.
- Added support for monitorStream.isActive for the Watch page.
- Added the updatePlayerControls() function for the Watch page to manage the state of the player buttons. In the future, we need to create a single function for managing the state of the player buttons. It's a bit of a mess right now. We'll do this in the next PR to avoid breaking anything.
- To optimize performance, part of the code from getTracksFromStream() has been moved directly to the listener in MonitorStream.
2026-08-19 00:04:02 +03:00
IgorA100 09909081f2 Merge branch 'master' into patch-483073 2026-08-18 21:05:56 +03:00
Isaac Connor edb4d7c202 fix: stop an applied filter coming up empty on the events list fixes #5026
Applying a named filter and clicking LIST MATCHES landed on Events showing
"No matching records found" until a manual refresh. Two independent defects
produce that, and both are fixed here.

Stored filter selections overriding the applied filter
------------------------------------------------------
The zmFilter_* cookies remember what was last selected in console, montage and
montagereview. They are a convenience for the default, filter-less page, but
they were also applied on top of a filter the request had specified, silently
widening or narrowing it: the applied filter was ANDed with a date range left
over from an earlier visit to another view, so it matched nothing. Refreshing
appeared to fix it because the bar's reset control blanks those inputs.

The cause was a layering one. Filter::simple_widget() refilled any empty term
value from that term's cookie as it rendered the input, so it overrode the value
addTerm() had been given, from below the layer that knows what the request asked
for. An earlier attempt to fix this in events.php could not hold for that
reason: it blanked the values and they were refilled during rendering, and
ajaxRequest() sends the live inputs rather than the URL.

Filter now resolves nothing. It renders the value it was handed, and the six
cookie fallbacks are gone; terms keep their cookie name so edits still persist
client-side. The callers that build those terms decide instead, next to
getFilterSelection(), which already resolved request-before-cookie this way.
Each resolves its value into a local before the addTerm() calls, testing
$use_stored in the same statement that reads the cookie, so the rule is visible
at the point it applies.

montagereview needs a window to draw whatever happens, so with a filter present
it derives one from the filter's own terms and otherwise defaults to the last
hour, rather than reaching for the stored window; its Notes term no longer seeds
from a cookie inside the branch that already has an explicit filter.

Tables never retrying a request skipped while hidden
----------------------------------------------------
The table views skip their ajax request while the page is hidden so a background
tab does not poll. Bootstrap-table calls that function on init as well as on
refresh, though, and a skipped request was never re-issued: the table rendered
"No matching records found" over a result it never asked for, with nothing to
bring it back. A page can be hidden for the whole of its load - opened in a
background tab, restored, or behind another window - and the same guard is in
seven views: events, console, log, frames, reports, snapshots and watch.

deferTableRequestWhileHidden() skips the request as before and records the table,
so it is refreshed the moment the page becomes visible. The queue is drained
before refreshing, because refresh() calls the ajax function synchronously and
would otherwise re-add a table that is still hidden.

Tests
-----
tests/audit-filter-cookies.php checks both halves of the first rule: that Filter
resolves nothing, and that every stored-selection read in a caller tests
$use_stored. It works a statement at a time, joining continuation lines, since a
value and its guard often span a line break. Anything wider is too coarse: the
enclosing block holds other guarded reads and would mask one that lost its own.

tests/js/table-helpers.test.js covers the deferral queue, including a table
re-deferred during its own refresh.

Verified against a live instance with the stale cookies still set: a "last hour"
named filter queried 0 of 4 matching events before and 12 of 12 after; the
filter-less page still restores the stored date range; and a table deferred while
hidden repaints on becoming visible.
2026-08-12 22:48:20 -04:00
Isaac Connor ea4f0c5f9a fix: return empty from zmAuth.applyTo for a blank stream src
applyTo appended the relay to an empty src, yielding a bare '?auth=...'.
Callers that treat a blank src as "nothing to load" would then set it on an
<img>, where it resolves against the current page and loads the surrounding
HTML as an image. montagereview's loadImage2Monitor is one such caller.

Introduced with ZMAuth; the code it replaced returned the src untouched.
2026-08-09 16:11:29 -04:00
Isaac ConnorandClaude Opus 5 12cb7d9601 fix: don't clear onerror/onload on the go2rtc video-stream element in kill() refs #5025
MonitorStream.kill() unconditionally did `stream.onerror = null` and
`stream.onload = null` on whatever element the monitor was using.  That was
written for the zms <img>, whose onerror/onload are inherited event-handler
accessors.

With go2rtc the element is <video-stream>, where onerror is a method on
VideoRTC.prototype (video-rtc.js) overridden by VideoStream (video-stream.js).
Assigning null there finds a writable data property on the prototype chain and
so creates an *own* property on the instance, shadowing the method for as long
as the element lives.  replaceDOMElement() returns the same node when the tag
already matches, so select_go2rtc() handed the poisoned element back on the
next start, and the listener VideoRTC.onconnect() registers,

  this.ws.addEventListener('error', (ev) => this.onerror(ev));

threw "TypeError: this.onerror is not a function" on the next websocket
failure.  Any kill()-then-start() path reached it: switching monitors on watch,
the stop/play buttons, montage viewport handling.  It also meant the restart
that VideoStream.onerror performs was silently dead after a kill().

Guard the assignments on the element actually being an IMG.

Add tests/js/monitorstream-kill.test.js, which evaluates the real
MonitorStream.js in a vm context and checks that kill() leaves a prototype
onerror callable on a <video-stream>, adds no own onerror/onload to it, and
still clears both on an <img>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MXtwyhzssj24Xwjmx8EA2z
2026-08-09 16:11:29 -04:00
Isaac Connor 40129a5564 refactor: replace the auth_hash/auth_relay globals with one ZMAuth credential
The page kept two copies of the same secret: auth_relay, the query fragment
every AJAX call is authenticated with, and auth_hash, the bare hash stamped
into stream <img> URLs. Different responses updated different copies, so
they could drift, and a drifted auth_hash produced stream URLs that zms
rejects.

ZMAuth stores only the relay and derives the hash from it, so the two cannot
disagree. Its helpers cover the four shapes the call sites used:

  zmAuth.hash          derived, '' under the plain/none relay forms
  zmAuth.update(data)  absorb the auth fields of any response
  zmAuth.appendTo(url) authenticate a url, no-op when auth is off
  zmAuth.applyTo(src)  point a stream url at the current credential

appendTo also removes the `x ? '&'+x : ''` guard repeated at every call
site, some of which had omitted it and emitted a dangling '?'.

Migrates all call sites across web/js and the classic skin, and drops both
globals from skin.js.php.

Tests: tests/js/auth-helpers.test.js, 44 passing.
2026-08-09 10:29:46 -04:00
Isaac Connor 15b913725d Merge pull request #5038 from connortechnology/fix-connkey-regeneration
fix: stop orphaning zms when a stream command fails
2026-08-07 19:44:50 -04:00
Isaac ConnorandClaude Opus 5 ae8c7db488 fix: stop orphaning zms when a stream command fails refs #5029
getStreamCmdResponse() responded to every ajax/stream.php failure the same way:
mint a fresh connkey and reload the img src. ajaxError() returns HTTP 200 with
result=Error, so these arrive in jQuery's done() rather than fail(), and all
twelve error paths in stream.php took that branch.

Only one of them means zms is gone. For the rest the process is still running
and streaming, and replacing the connkey makes it unaddressable: CMD_STOP,
CMD_QUIT and mode=single all then go to the new key, so nothing can reach the
old process and only SIGPIPE can stop it, which we know is unreliable. That is
why the reports of lingering zms after switching monitors were unaffected by
changes to what the stop path sends.

The timeout path made this routine rather than rare. On select() expiry
ajaxError is commented out, so the script carries on to socket_recvfrom() on a
now non-blocking socket. That returns false, and false == 0 under switch's loose
comparison, so a merely slow zms was reported as 'No data to read from socket'
and torn down.

stream.php now classifies each failure as no_socket, timeout, transient or
invalid, and sends it as 'reason'. The client restarts the stream only for
no_socket. A missing reason is still treated as fatal, so a php that predates
this keeps the old behaviour.

Before replacing the connkey the client now sends CMD_QUIT to the old one, so
the process we are about to lose track of is asked to exit. That is deliberately
not routed through streamCommand(): it must name its target explicitly, since
this.connKey is about to change, and its response must not feed back into
getStreamCmdResponse(), or a QUIT that also failed would re-enter the error path
and loop.

ajaxError() takes the classification as a third argument, named $reason because
$code is already the HTTP status, and only includes it when set, so the other
131 callers are unaffected.

Tests: tests/js covers the fatal/non-fatal decision including the no-reason
fallback, tests/php pins the classification mapping and the switch(false)
semantics the timeout branch depends on. Both verified to fail when the
behaviour is reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 06:43:11 -04:00
IgorA100 ebeb1bd5c6 Added a missing line break. 2026-08-03 18:00:30 +03:00
IgorA100 cfaaae9689 - Removed an extra opening parenthesis.
- Removed an extra argument passed when calling waitUntil().
2026-08-03 17:54:49 +03:00
IgorA100 5426d5571a - ConnectAudioMotion(mid) execution has been moved to the MonitorStream.js and event.js listeners.
- If a video track is missing when it is required, we generate the event:
dispatchTracksReceived(videoFeedStream, {
status: 'aborted',
reason: 'playback-videoTrack-missing'
});
2026-08-03 17:44:34 +03:00
IgorA100 6872200217 Merge branch 'master' into patch-530122 2026-08-03 16:55:06 +03:00
Isaac ConnorandClaude Opus 5 6111ebea20 fix: don't let kill() short-circuit stop() refs #5029
kill() cleared this.started before calling this.stop(), but stop() returns
early when !started. For the zms path that meant clearInterval() on
statusCmdTimer and streamCmdTimer never ran, activePlayer was never reset and
mediaStream/audioTrack/videoTrack were never released. Every kill() leaked a
pair of intervals, which adds up over a montage or watch page that cycles
monitors every few seconds.

Keep started set until stop() has done its work, and pass skipStreamCommand so
stop() doesn't follow CMD_QUIT with a CMD_STOP against a socket zms is already
tearing down. Clear connkey afterwards.

stop() already sets started=false and activePlayer='' at the end, so kill()
doesn't need to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 08:03:13 -04:00
Pliable PixelsandClaude Opus 5 51e9e754be fix: snapshot stream command params before queueing ajax fixes #5031
$.ajaxQueue defers the $.ajax() call until earlier queued requests
finish, and jQuery serialises the data object at that later point.
streamCmdReq() passed its data object by reference, so callers that
hand it this.streamCmdParms directly had their command overwritten by
the streamCmdQuery timer setting command=CMD_QUERY before the request
was actually sent.

show_analyse_frames() is the only such caller, which is why the Show
Analysis button in the live view appeared to do nothing: zms received
CMD_QUERY instead of CMD_ANALYZE_ON and never switched to
FRAME_ANALYSIS.

Copy the params inside streamCmdReq() so every caller is covered.
streamCommand() and alarmCommand() already copied by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 17:10:18 -04:00
IgorA100 d41630b354 Merge branch 'master' into patch-684532 2026-07-31 22:12:43 +03:00
Isaac Connor 58d9e73615 Merge pull request #5016 from IgorA100/patch-710934
Fix: Changed the handling of key presses for PanZoom
2026-07-31 09:20:53 -04:00
IgorA100 c143d146ec Prevent lingering MJPEG streams by switching to mode=single when stopping monitor streams 2026-07-30 18:38:53 +03:00
IgorA100 49378ad7b9 Added protection in the form of replaceId for this.handlerEventListener['playStream'] 2026-07-30 16:14:05 +03:00
IgorA100 680e8435aa Fix: ESLint (MonitorStream.js) 2026-07-30 16:06:28 +03:00
IgorA100 27dee38bdd Add starting state for reliable stream start/stop transitions
And Perform cleanup of this.mediaStream = this.audioTrack = this.videoTrack = null; later in the code, after clearing the stream
2026-07-30 16:02:41 +03:00
IgorA100 b7b5d5b93b Prevent duplicate zm:tracksReceived event listeners using replaceId (MonitorStream.js) 2026-07-30 13:33:51 +03:00
IgorA100 260c7a455c Update MonitorStream.js 2026-07-29 19:15:26 +03:00
IgorA100 95b6927ca4 Merge branch 'master' into patch-684532 2026-07-29 16:53:15 +03:00
IgorA100 9944105bf4 Merge branch 'master' into patch-865636 2026-07-29 16:49:35 +03:00
IgorA100 61e8044b6e Merge branch 'master' into patch-710934 2026-07-29 16:47:24 +03:00
IgorA100 40bb621159 Remove existing handlers for a namespace before rebinding a handler. 2026-07-29 16:46:26 +03:00
IgorA100 e1731d9f33 Added subscription and unsubscription to custom events when extracting tracks from the stream. 2026-07-29 16:34:06 +03:00
IgorA100 282d6a72c9 Merge branch 'master' into patch-530122 2026-07-29 16:01:51 +03:00
IgorA100 601771956a Look for the VIDEO object in panZoomTarget, not in document
This will speed up and simplify the search a bit.
2026-07-29 10:58:43 +03:00
IgorA100 a256d9440b Optimization of the manageCursor() method in PanZoom and the problem of stopping the stream on the Watch page instead of zooming in Firefox when Shift + clicking has been resolved. (panzoom.js) 2026-07-29 01:02:57 +03:00
IgorA100 53ffeb6ff6 Merge branch 'master' into patch-530122 2026-07-28 19:50:25 +03:00
IgorA100 14fc511c56 Changed the stop() function to more correctly stop streams and free up occupied resources (MonitorStream.js)
This primarily applies to rtsp2web and go2rtc.
2026-07-28 18:46:18 +03:00
IgorA100 9b8aab1a49 Merge branch 'master' into patch-684532 2026-07-28 15:43:30 +03:00
Isaac Connor 775daf90a2 Merge pull request #5014 from IgorA100/patch-179991
Stop processing the selected player if the video stream DOM element for the monitor is not found.
2026-07-28 08:09:02 -04:00
IgorA100 116c4eae8d Added checks when clearing videoStream (MonitorStream.js) 2026-07-28 13:38:19 +03:00