From 54e180fad0c3273c798bd6b2460287da32193409 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 5 Mar 2026 18:28:58 +0300 Subject: [PATCH 1/7] Added the ManageEventListener class, which allows for more flexible event listener assignment and removal. (skin.js) This is especially important when using the "bind()" method, as this method creates a new function, and removing the listener is only possible with a reference function. --- web/skins/classic/js/skin.js | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index c5eb6f383..899e3717f 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2862,4 +2862,44 @@ const waitUntil = (condition, timeout = 0) => { }); }; +// https://stackoverflow.com/a/69273090 +class ManageEventListener { + #listeners = {}; // # in a JS class signifies private + #idx = 1; + + // add event listener, returns integer ID of new listener + addEventListener(element, type, listener, useCapture = false) { + this.#privateAddEventListener(element, this.#idx, type, listener, useCapture); + return this.#idx++; + } + + // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself) + addEventListenerById(element, id, type, listener, useCapture = false) { + this.#privateAddEventListener(element, id, type, listener, useCapture); + return id; + } + + #privateAddEventListener(element, id, type, listener, useCapture) { + if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`); + element.addEventListener(type, listener, useCapture); + this.#listeners[id] = {element, type, listener, useCapture}; + } + + // remove event listener with given ID, returns ID of removed listener or null (if listener with given ID does not exist) + removeEventListener(id) { + const listen = this.#listeners[id]; + if (listen) { + listen.element.removeEventListener(listen.type, listen.listener, listen.useCapture); + delete this.#listeners[id]; + } + return !!listen ? id : null; + } + + // returns number of events listeners + length() { + return Object.keys(this.#listeners).length; + } +} +const manageEventListener = new ManageEventListener(); + $j( window ).on("load", initPageGeneral); From 2f9839b20b5cd2187522d41684a99cc19751c6c1 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 5 Mar 2026 19:01:25 +0300 Subject: [PATCH 2/7] To assign the 'beforeunload' listener and remove the listener, we now use the ManageEventListener class (MonitorStream.js) --- web/js/MonitorStream.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 91259a701..490e00d7e 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -1,6 +1,7 @@ "use strict"; var janus = null; const streaming = []; +const handlerEventListener = []; function MonitorStream(monitorData) { this.id = monitorData.id; @@ -441,7 +442,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); if (typeof observerMontage !== 'undefined') observerMontage.observe(stream); this.activePlayer = 'go2rtc'; @@ -482,7 +483,7 @@ function MonitorStream(monitorData) { attachVideo(this); this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'janus'; this.updateStreamInfo('Janus', 'loading'); return; @@ -554,7 +555,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); this.updateStreamInfo((typeof players !== "undefined" && players) ? players[this.activePlayer] : 'RTSP2Web ' + this.RTSP2WebType, 'loading'); return; } else { @@ -618,12 +619,14 @@ function MonitorStream(monitorData) { } } // end if paused or not this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'zms'; this.updateStreamInfo('ZMS MJPEG'); }; // this.start this.stop = function() { + manageEventListener.removeEventListener(handlerEventListener['killStream']); + /* Stop should stop the stream (killing zms) but NOT set src=''; This leaves the last jpeg up on screen instead of a broken image */ const stream = this.getElement(); if (!stream) { @@ -1976,10 +1979,10 @@ function startRTSP2WebPlay(videoEl, url, stream) { } function streamListener(stream) { - window.addEventListener('beforeunload', function(event) { + return manageEventListener.addEventListener(window, 'beforeunload', function() { console.log('streamListener'); stream.kill(); - }); + }, false); } function mseListenerSourceopen(context, videoEl, url) { From 840db3b993149c8f919deb0514a45cd80a95116d Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 5 Mar 2026 19:05:48 +0300 Subject: [PATCH 3/7] Removed extra space (skin.js) --- web/skins/classic/js/skin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 899e3717f..74284a706 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2864,7 +2864,7 @@ const waitUntil = (condition, timeout = 0) => { // https://stackoverflow.com/a/69273090 class ManageEventListener { - #listeners = {}; // # in a JS class signifies private + #listeners = {}; // # in a JS class signifies private #idx = 1; // add event listener, returns integer ID of new listener @@ -2872,7 +2872,7 @@ class ManageEventListener { this.#privateAddEventListener(element, this.#idx, type, listener, useCapture); return this.#idx++; } - + // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself) addEventListenerById(element, id, type, listener, useCapture = false) { this.#privateAddEventListener(element, id, type, listener, useCapture); From 15cb090d9591c0a3c42e318cfb98518438438ea6 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 6 Mar 2026 10:46:09 +0300 Subject: [PATCH 4/7] Instead of handlerEventListener, we use this.handlerEventListener (MonitorStream.js). We won't use the "this.killStreamListenerId" variable recommended by Copilot, but will instead use the this.handlerEventListener array, as we may have other listeners in the future. Using an array will eliminate the need to declare additional constants. --- web/js/MonitorStream.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 490e00d7e..2a2562b28 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -1,7 +1,6 @@ "use strict"; var janus = null; const streaming = []; -const handlerEventListener = []; function MonitorStream(monitorData) { this.id = monitorData.id; @@ -31,6 +30,7 @@ function MonitorStream(monitorData) { this.wsMSE = null; this.streamStartTime = 0; // Initial point of flow start time. Used for flow lag time analysis. this.waitingStart; + this.handlerEventListener = []; this.mseListenerSourceopenBind = null; this.streamListenerBind = null; this.mseSourceBufferListenerUpdateendBind = null; @@ -442,7 +442,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); if (typeof observerMontage !== 'undefined') observerMontage.observe(stream); this.activePlayer = 'go2rtc'; @@ -483,7 +483,7 @@ function MonitorStream(monitorData) { attachVideo(this); this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'janus'; this.updateStreamInfo('Janus', 'loading'); return; @@ -555,7 +555,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); this.updateStreamInfo((typeof players !== "undefined" && players) ? players[this.activePlayer] : 'RTSP2Web ' + this.RTSP2WebType, 'loading'); return; } else { @@ -619,13 +619,13 @@ function MonitorStream(monitorData) { } } // end if paused or not this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'zms'; this.updateStreamInfo('ZMS MJPEG'); }; // this.start this.stop = function() { - manageEventListener.removeEventListener(handlerEventListener['killStream']); + manageEventListener.removeEventListener(this.handlerEventListener['killStream']); /* Stop should stop the stream (killing zms) but NOT set src=''; This leaves the last jpeg up on screen instead of a broken image */ const stream = this.getElement(); From dbd52087d3bd64a5f1a49ea04cad0fface125923 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 7 Mar 2026 17:35:02 -0500 Subject: [PATCH 5/7] Update web/js/MonitorStream.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/js/MonitorStream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 2a2562b28..f6126ea72 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -30,7 +30,7 @@ function MonitorStream(monitorData) { this.wsMSE = null; this.streamStartTime = 0; // Initial point of flow start time. Used for flow lag time analysis. this.waitingStart; - this.handlerEventListener = []; + this.handlerEventListener = {}; this.mseListenerSourceopenBind = null; this.streamListenerBind = null; this.mseSourceBufferListenerUpdateendBind = null; From e77993e35f013fbd9dc59f5828266064554c1be4 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sun, 8 Mar 2026 12:38:45 +0300 Subject: [PATCH 6/7] Instead of "useCapture" we use the extended version "options" MonitorStream.js --- web/js/MonitorStream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index f6126ea72..e2f9af2c1 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -1982,7 +1982,7 @@ function streamListener(stream) { return manageEventListener.addEventListener(window, 'beforeunload', function() { console.log('streamListener'); stream.kill(); - }, false); + }, {capture: false}); } function mseListenerSourceopen(context, videoEl, url) { From 8ecaff07517a6ee9a12000638e7a3a17211e3e3a Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sun, 8 Mar 2026 12:44:12 +0300 Subject: [PATCH 7/7] Instead of "useCapture" we use the extended version "options" (skin.js) We also explicitly attach the manageEventListener to the window. --- web/skins/classic/js/skin.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 74284a706..3a632b3dc 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2868,28 +2868,28 @@ class ManageEventListener { #idx = 1; // add event listener, returns integer ID of new listener - addEventListener(element, type, listener, useCapture = false) { - this.#privateAddEventListener(element, this.#idx, type, listener, useCapture); + addEventListener(element, type, listener, options = {}) { + this.#privateAddEventListener(element, this.#idx, type, listener, options); return this.#idx++; } // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself) - addEventListenerById(element, id, type, listener, useCapture = false) { - this.#privateAddEventListener(element, id, type, listener, useCapture); + addEventListenerById(element, id, type, listener, options = {}) { + this.#privateAddEventListener(element, id, type, listener, options); return id; } - #privateAddEventListener(element, id, type, listener, useCapture) { + #privateAddEventListener(element, id, type, listener, options) { if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`); - element.addEventListener(type, listener, useCapture); - this.#listeners[id] = {element, type, listener, useCapture}; + element.addEventListener(type, listener, options); + this.#listeners[id] = {element, type, listener, options}; } // remove event listener with given ID, returns ID of removed listener or null (if listener with given ID does not exist) removeEventListener(id) { const listen = this.#listeners[id]; if (listen) { - listen.element.removeEventListener(listen.type, listen.listener, listen.useCapture); + listen.element.removeEventListener(listen.type, listen.listener, listen.options); delete this.#listeners[id]; } return !!listen ? id : null; @@ -2901,5 +2901,6 @@ class ManageEventListener { } } const manageEventListener = new ManageEventListener(); +window.manageEventListener = manageEventListener; $j( window ).on("load", initPageGeneral);