From 44849e0a6f58273af02cca92e85188a4d40dc058 Mon Sep 17 00:00:00 2001 From: Simpler1 Date: Thu, 12 Mar 2026 12:10:43 -0400 Subject: [PATCH 1/2] Fix orphaned zms processes accumulating on Montage page tab hide/show When the Montage page tab is hidden for more than 15 seconds, the onvisibilitychange handler calls monitors[i].stop() on all active streams. When the tab becomes visible again, startVisibleMonitors() detects monitor.started === false and calls start(), spawning new zms processes with new connKey values. The problem is that stop() sends CMD_STOP to the ZoneMinder API, which does not terminate the zms process. The old zms process continues running with no client consuming its output, no timeout to kill it, and no mechanism to ever clean it up. Each hide/show cycle leaves 3 more orphaned zms processes (one per monitor). Over time these accumulate until the server exhausts resources, at which point new zms requests begin returning 504 Gateway Timeout errors and the Montage page stops streaming entirely. This is particularly impactful for users who run the Montage page continuously (e.g. for alarm sound notifications), where the browser's tab visibility API will hide/show the tab frequently due to normal computer use. --- web/skins/classic/views/js/montage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/montage.js b/web/skins/classic/views/js/montage.js index e5c10edae4..9db56aa323 100644 --- a/web/skins/classic/views/js/montage.js +++ b/web/skins/classic/views/js/montage.js @@ -1046,7 +1046,7 @@ document.onvisibilitychange = () => { //closing should kill, hiding should stop/pause for (let i = 0, length = monitors.length; i < length; i++) { // Stop instead of pause because we don't want buffering in zms - monitors[i].stop(); + monitors[i].kill(); } }, 15*1000); } else { From 6a6daa75b9fcbf51bb99afe944d73523c145412d Mon Sep 17 00:00:00 2001 From: Simpler1 Date: Thu, 12 Mar 2026 13:29:59 -0400 Subject: [PATCH 2/2] Update watch.js and zone.js with same change from stop() to kill() --- web/skins/classic/views/js/watch.js | 5 ++++- web/skins/classic/views/js/zone.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index bbc32e1c76..9006b28dcd 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -1402,7 +1402,10 @@ document.onvisibilitychange = () => { prevStateStarted = 'played'; //Stop only if playing (not paused). // We might want to continue status updates so that alarm sounds etc still happen - monitorStream.stop(); + // Use kill() instead of stop() to send CMD_QUIT and terminate the zms + // process. stop() only sends CMD_STOP which leaves zms running, causing + // orphaned processes to accumulate each time the tab is hidden/shown. + monitorStream.kill(); } } else { prevStateStarted = 'stopped'; diff --git a/web/skins/classic/views/js/zone.js b/web/skins/classic/views/js/zone.js index 22b7b2b0e2..57baa903de 100644 --- a/web/skins/classic/views/js/zone.js +++ b/web/skins/classic/views/js/zone.js @@ -793,7 +793,10 @@ document.onvisibilitychange = () => { TimerHideShow = setTimeout(function() { //Stop monitors when closing or hiding page for (let i = 0, length = monitorData.length; i < length; i++) { - monitors[i].stop(); + // Use kill() instead of stop() to send CMD_QUIT and terminate the zms + // process. stop() only sends CMD_STOP which leaves zms running, causing + // orphaned processes to accumulate each time the tab is hidden/shown. + monitors[i].kill(); } }, 15*1000); } else {