diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 71aee499ec1..659fd0f592c 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -125,6 +125,7 @@ function MonitorStream(monitorData) { }; this.playbackSessionId = null; + this.isActive = true; //The monitor is active, which is relevant when quickly switching between monitors on the Watch page this.ajaxQueue = null; this.type = monitorData.type; this.capturing = monitorData.capturing; @@ -155,6 +156,7 @@ function MonitorStream(monitorData) { console.log('Image stream has been stopped! stopping streamCmd'); this.streamCmdTimer = clearInterval(this.streamCmdTimer); this.writeTextInfoBlock("Error", {showImg: false}); + if (!this.isActive) return; // zms returns 403 on a stale auth hash (default TTL 2h). For a live multipart // (mode=jpeg) the browser reconnects on its own using the same baked-in @@ -208,6 +210,7 @@ function MonitorStream(monitorData) { clearTimeout(this.authRefreshTimer); this.authRefreshTimer = null; } + if (!this.started) return; if (!this.streamCmdTimer) { console.log('Image stream has loaded! starting streamCmd for monitor ID='+this.id+' connKey='+this.connKey+' in '+statusRefreshTimeout + 'ms'); this.streamCmdQuery(); // This is to get an instant status update @@ -536,6 +539,7 @@ function MonitorStream(monitorData) { }; this.handlerEventListenerStream = function(stream = null) { + const playbackSessionId = this.playbackSessionId; if (!stream) stream = this.getAVStream(); if (!stream) { console.debug(`Stream for monitor ID=${this.id} not found. Assigning listeners is not possible.`); @@ -543,14 +547,58 @@ function MonitorStream(monitorData) { } this.handlerEventListener['playStream'] = manageEventListener.addEventListener(stream, 'play', (e) => { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; this.writeTextInfoBlock(""); this.createVolumeSlider(); + + this.handlerEventListener['zm:tracksReceived'] = manageEventListener.addEventListener(this.getElement(), 'zm:tracksReceived', + (e) => { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; + if (e.detail.monitorId !== this.id) return; + + if (this.started) { + if (e.detail.status == 'success') { + if (!this.videoTrack ) { + this.updateStreamInfo('', 'Video track missing'); + this.writeTextInfoBlock("Video track missing", {showImg: false}); + } + + // We'll determine whether we need a video track or just an audio track. + // When playing H.265, the video may not be decoded, but the audio track will play. + const selectorWhatDisplay = document.getElementById('whatDisplay'); + const defaultWhatDisplay = this.whatDisplay; + let videoTrackRequired = true; + + if (!selectorWhatDisplay || (-1 !== selectorWhatDisplay.value.toLowerCase().indexOf('default'))) { // Default monitor settings + if (defaultWhatDisplay && (-1 === defaultWhatDisplay.toLowerCase().indexOf('video'))) videoTrackRequired = false; + } else { + if (-1 === selectorWhatDisplay.value.toLowerCase().indexOf('video')) videoTrackRequired = false; + } + + if (!this.videoTrack && videoTrackRequired && (!this.selectedPlayer || this.selectedPlayer === "go2rtc")) { + // Switch to a different player only when mode=Auto + this.streamErrorRegistration(); + this.restart(this.currentChannelStream); + } + if (this.audioTrack) connectAudioMotion(this.id); + } else { + console.warn(`Error receiving audio/video tracks for monitor ID=${this.id}. [${e.detail.reason}]`, this.activePlayer, e); + this.streamErrorRegistration(); + this.restart(this.currentChannelStream); + } + } else { + console.warn(`RACE for monitor ID=${this.id}, we received audio video tracks, but the stream has already stopped.`, this.activePlayer, e); + } + }, + {replaceId: this.handlerEventListener['zm:tracksReceived']} + ); getTracksFromStream(this); }, {replaceId: this.handlerEventListener['playStream']} ); this.handlerEventListener['pauseStream'] = manageEventListener.addEventListener(stream, 'pause', (e) => { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; this.writeTextInfoBlock("Paused", {showImg: false}); manageEventListener.removeEventListener(this.handlerEventListener['volumechange']); if (typeof pauseAudioMotion === 'function') { @@ -561,6 +609,7 @@ function MonitorStream(monitorData) { this.handlerEventListener['errorStream'] = manageEventListener.addEventListener(stream, 'error', (e) => { clearTimeout(this.mseWaitingErrorReset); + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; const mediaErrorMsg = e?.target?.error?.message || e?.srcElement?.error?.message || 'Unknown media error'; console.warn(`Stream playback error for monitor ID=${this.id}.`, `ERROR: ${mediaErrorMsg}`, e); this.writeTextInfoBlock("Error"); @@ -571,6 +620,8 @@ function MonitorStream(monitorData) { }; this.start = function(streamChannel = 'default') { + if (!this.isActive) return; + if (this.started || this.starting) { console.debug( `Start() ignored for monitor ID=${this.id}`, @@ -598,13 +649,7 @@ function MonitorStream(monitorData) { //$j('#volumeControls'+this.id).hide(); $j('#volumeControls'+this.id).addClass('disabled'); $j('#delay'+this.id).addClass('hidden'); - this.handlerEventListener['zm:tracksReceived'] = manageEventListener.addEventListener(document, 'zm:tracksReceived', - (e) => { - if (e.detail.monitorId !== this.id) return; - if (this.audioTrack) connectAudioMotion(this.id); - }, - {replaceId: this.handlerEventListener['zm:tracksReceived']} - ); + this.selectPlayer(streamChannel); }; // this.start @@ -760,6 +805,7 @@ function MonitorStream(monitorData) { } //this.started = false; + if (this.audioMotion && this.audioMotion.stop) this.audioMotion.stop(); if (-1 !== this.activePlayer.indexOf('zms')) { this.writeTextInfoBlock("Stopped", {showImg: false}); } else { @@ -770,7 +816,6 @@ function MonitorStream(monitorData) { this.streamCmdTimer = clearInterval(this.streamCmdTimer); this.mediaStream = this.audioTrack = this.videoTrack = null; - if (this.audioMotion && this.audioMotion.stop) this.audioMotion.stop(); if (-1 !== this.activePlayer.indexOf('zms')) { // Icon: My current thought is to just tell zms to stop. Don't go to single. if ((this.started || wasStarting) && !options.skipStreamCommand) this.streamCommand(CMD_STOP); @@ -800,7 +845,7 @@ function MonitorStream(monitorData) { this.stopMse().finally(() => { console.debug(`RTSP2Web type MSE fully stopped for ID=${this.id}`); stream.removeAttribute('src'); - stream.load?.(); + //stream.load?.(); }); } } else if (-1 !== this.activePlayer.indexOf('janus')) { @@ -918,7 +963,7 @@ function MonitorStream(monitorData) { } // this.stop tells zms to stop streaming, but the process remains. We need to turn the stream into an image. - const quit = this.started && (-1 !== this.activePlayer.indexOf('zms')) && this.connKey; + const quit = (this.started || this.starting) && (-1 !== this.activePlayer.indexOf('zms')) && this.connKey; if (quit) { // Make zms exit, sometimes zms doesn't receive SIGPIPE, so try to send QUIT this.streamCommand(CMD_QUIT); @@ -938,25 +983,32 @@ function MonitorStream(monitorData) { this.stop(); const countErrors = this.getCountStreamErrors(this.player); if (countErrors < this.limitCountErrors) { + const playbackSessionId = this.playbackSessionId; setTimeout(function(self) {// During the downtime, the monitor may have already started to work. + if (!isCurrentPlaybackSession(self, playbackSessionId)) return; + if (!self.isActive) return; if (!self.started && !self.starting) self.start(channelStream); }, delay, this); } else { - if (typeof streamCmdStop === 'function') { - // Let's set the correct state for the player control buttons (for example, on the Watch page) - streamCmdStop(); - } + if (this.selectedPlayer) { + if (typeof updatePlayerControls === 'function') { + // Let's set the correct state for the player control buttons (for example, on the Watch page) + updatePlayerControls("stop"); + } - if (-1 !== this.player.indexOf('zms')) { - this.writeTextInfoBlock("Error", {showImg: false}); + if (-1 !== this.player.indexOf('zms')) { + this.writeTextInfoBlock("Error", {showImg: false}); + } else { + this.writeTextInfoBlock("Error"); + } + this.updateStreamInfo('', 'Error'); + this.resetCountStreamErrors(this.player); + const msg = `Out of ${this.limitCountErrors} consecutive attempts to start a stream for monitor ID=${this.id} using player "${this.player}", none were successful. The stream has been stopped.`; + console.warn(msg); + this.showText(msg); } else { - this.writeTextInfoBlock("Error"); + this.selectNextPlayer(); } - this.updateStreamInfo('', 'Error'); - this.resetCountStreamErrors(this.player); - const msg = `Out of ${this.limitCountErrors} consecutive attempts to start a stream for monitor ID=${this.id} using player "${this.player}", none were successful. The stream has been stopped.`; - console.warn(msg); - this.showText(msg); } }; @@ -1852,7 +1904,6 @@ function MonitorStream(monitorData) { this.select_go2rtc = function(streamChannel) { if (ZM_GO2RTC_PATH) { - this.playbackSessionId = generateUUID(); const url = new URL(ZM_GO2RTC_PATH); const stream = this.element = replaceDOMElement(this.getElement(), 'video-stream'); @@ -1870,6 +1921,10 @@ function MonitorStream(monitorData) { webrtcUrl.protocol = (url.protocol=='https:') ? 'wss:' : 'ws'; webrtcUrl.pathname += "/ws"; webrtcUrl.search = 'src=' + streamName; + if (!this.isActive) { + this.kill(); + return; + } stream.src = webrtcUrl.href; this.webrtc = stream; // track separately do to api differences between video tag and video-stream @@ -1927,6 +1982,10 @@ function MonitorStream(monitorData) { //.urlParts.length > 1 ? urlParts[1] : urlParts[0]; // drop the username and password for viewing this.currentChannelStream = streamChannel; const numericChannel = this.getNumericChannel(streamChannel); + if (!this.isActive) { + this.kill(); + return; + } if (-1 !== this.player.indexOf('hls')) { const hlsUrl = rtsp2webModUrl; hlsUrl.pathname = "/stream/" + this.id + "/channel/" + numericChannel + "/hls/live/index.m3u8"; @@ -1938,6 +1997,9 @@ function MonitorStream(monitorData) { } */ if (Hls.isSupported()) { + // Save the playback session ID so asynchronous HLS callbacks + // can ignore events from obsolete playback sessions. + const playbackSessionId = this.playbackSessionId; this.hls = new Hls({ maxBufferLength: 10, maxMaxBufferLength: 30, @@ -1954,10 +2016,15 @@ function MonitorStream(monitorData) { */ this.hls.on(Hls.Events.MEDIA_ATTACHING, function(event, data) { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; console.debug(`HLS Event = MEDIA_ATTACHING for monitor ID=${this.id}`); }, this); this.hls.on(Hls.Events.BUFFER_CODECS, function(event, data) { // Triggers if there is an audio track. + if (!isCurrentPlaybackSession(this, playbackSessionId)) { + //hlsDestroy(this); + return; + } console.log(`For monitor with ID=${this.id}, the "${data.audio.codec}" audio codec is used.`); if (data.audio.codec.indexOf('mp4a.40.') > -1) { // AAC: mp4a.40.2 - HLS can't play it, so the "Loading" status will always be displayed. @@ -1969,9 +2036,14 @@ function MonitorStream(monitorData) { } }, this); this.hls.on(Hls.Events.MEDIA_ATTACHED, function(event, data) { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; console.log(`Video and hls.js are now bound together for monitor ID=${this.id}`); }, this); this.hls.on(Hls.Events.ERROR, function(event, data) { + if (!isCurrentPlaybackSession(this, playbackSessionId)) { + //hlsDestroy(this); + return; + } console.warn("HLS Event = ERROR", "\n", "event:", event, "\n", "errorType:", data.type, "\n", "errorDetails:", data.details, "\n", "errorFatal:", data.fatal); if (!data || !data.fatal) return; this.updateStreamInfo('', 'Error'); //HLS @@ -2038,6 +2110,10 @@ function MonitorStream(monitorData) { } else { server = "http://" + window.location.hostname + "/janus"; } + if (!this.isActive) { + this.kill(); + return; + } if (janus == null) { Janus.init({debug: "all", callback: function() { @@ -2055,19 +2131,32 @@ function MonitorStream(monitorData) { this.select_zms = function() { // zms stream this.playbackSessionId = generateUUID(); + const playbackSessionId = this.playbackSessionId; const stream = this.element = replaceDOMElement(this.getElement(), 'img'); stream.srcObject = null; if (!stream) return; this.destroyVolumeSlider(); + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; this.streamCmdTimer = clearInterval(this.streamCmdTimer); // Step 1 make sure we are streaming instead of a static image if (stream.getAttribute('loading') == 'lazy') { stream.setAttribute('loading', 'eager'); } - stream.onerror = this.img_onerror.bind(this); - stream.onload = this.img_onload.bind(this); + const onError = this.img_onerror.bind(this); + const onLoad = this.img_onload.bind(this); + + stream.onerror = (e) => { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; + onError(e); + }; + stream.onload = (e) => { + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; + this.resetCountStreamErrors(this.activePlayer); + onLoad(e); + }; + // Check if the auth hash in the current img src is still valid. // On long-running pages the hash from page load may have expired. // zmAuth.hash is '' when authentication is off or under the plain/none relay @@ -2075,6 +2164,11 @@ function MonitorStream(monitorData) { // this has always done. const srcAuthCurrent = stream.src && zmAuth.hash && authHashFromRelay(stream.src) === zmAuth.hash; + if (!this.isActive) { + this.kill(); + return; + } + if (srcAuthCurrent && this.activePlayer == 'zms') { // Auth is current and zms was already the active player — just resume this.streamCmdTimer = setInterval(this.streamCmdQuery.bind(this), statusRefreshTimeout); @@ -2107,10 +2201,19 @@ function MonitorStream(monitorData) { } if (stream.src != src) { //console.log("Setting src.src", stream.src, src); + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; stream.src = ''; stream.src = src; + // This isn't a duplicate of the code above. It's intentional. However, upon closer testing, the similar line above may prove unnecessary. + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; } } // end if paused or not + if (!isCurrentPlaybackSession(this, playbackSessionId)) return; + if (!this.isActive) { + this.kill(); + return; + } + this.started = true; this.handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'zms'; @@ -2145,6 +2248,11 @@ function MonitorStream(monitorData) { }; this.selectNextPlayer = function(currentPlayer = null) { + if (!this.isActive) { + this.kill(); + return; + } + if (this.defaultPlayer == this.player) { // This means we need to start the bypass from the beginning, since we started playback from the default player, which may be in the middle of the list. currentPlayer = this.playerPriority[1]['name']; @@ -2172,6 +2280,13 @@ function MonitorStream(monitorData) { return; } } + + // We're already on the last player, but ZMS could theoretically still have errors. This is necessary to avoid loops. + if (this.player === 'zms' || currentPlayer.indexOf('zms') !== -1) { + console.error("All players failed. Stop restart loop", currentPlayer); + return; + } + this.player = 'zms'; this.restart(this.currentChannelStream); foundNextPlayer = true; @@ -2184,11 +2299,11 @@ function MonitorStream(monitorData) { } }; - this.streamErrorRegistration = function() { + this.streamErrorRegistration = function(fatal = false) { const currentPlayer = this.player; for (const key in this.playerPriority) { if (-1 !== currentPlayer.indexOf(this.playerPriority[key]['name'])) { - this.playerPriority[key]['countErrors'] = parseInt(this.playerPriority[key]['countErrors'], 10) + 1; + this.playerPriority[key]['countErrors'] = parseInt(this.playerPriority[key]['countErrors'], 10) + (fatal ? this.limitCountErrors : 1); break; } } @@ -2507,17 +2622,21 @@ function streamListener(stream) { } function mseListenerSourceopen(context, videoEl, url) { + const playbackSessionId = context.playbackSessionId; context.wsMSE = new WebSocket(url); context.wsMSE.binaryType = 'arraybuffer'; context.wsMSE.onopen = function(event) { + if (!isCurrentPlaybackSession(context, playbackSessionId)) return; console.log(`Connect to WebSocket MSE for a video object ID=${context.id}`); }; context.wsMSE.onclose = (event) => { + if (!isCurrentPlaybackSession(context, playbackSessionId)) return; context.clearWebSocket(); console.log(`${dateTimeToISOLocal(new Date())} WebSocket MSE CLOSED for a video object ID=${context.id}.`); }; context.wsMSE.onerror = function(event) { + if (!isCurrentPlaybackSession(context, playbackSessionId)) return; // Firefox will display error 1006 when closing the socket. There's likely a problem with RTSP2Web. console.warn(`${dateTimeToISOLocal(new Date())} WebSocket MSE ERROR for a video object ID=${context.id} [stream status: ${(context.started) ? "started" : "stopped"}]:`, event); if (context.started) { @@ -2527,6 +2646,7 @@ function mseListenerSourceopen(context, videoEl, url) { }; context.wsMSE.onmessage = function(event) { if (!context.mse || (context.mse && context.mse.readyState !== "open")) return; + if (!isCurrentPlaybackSession(context, playbackSessionId)) return; const data = new Uint8Array(event.data); if (data[0] === 9) { let mimeCodec; @@ -2547,9 +2667,9 @@ function mseListenerSourceopen(context, videoEl, url) { if (context.selectedPlayer) { context.stop(); } else { - // Select next player only for "Auto" mode - context.streamErrorRegistration(); - context.selectNextPlayer(context.player); + // Restart for select next player only for "Auto" mode + context.streamErrorRegistration(true); + context.restart(context.currentChannelStream); } return; } @@ -2565,6 +2685,7 @@ function mseListenerSourceopen(context, videoEl, url) { } function startMsePlay(context, videoEl, url) { + const playbackSessionId = context.playbackSessionId; console.log(`startMsePlay for monitor with ID=${context.id}`); var startPermitted = true; if (!context.MSEBufferCleared) { @@ -2581,6 +2702,7 @@ function startMsePlay(context, videoEl, url) { clearTimeout(context.waitingStart); } else { context.waitingStart = setTimeout(function(_context) { + if (!isCurrentPlaybackSession(_context, playbackSessionId)) return; if (context.started) startMsePlay(context, videoEl, url); }, 100, context); return; @@ -2588,9 +2710,11 @@ function startMsePlay(context, videoEl, url) { context.mse = new MediaSource(); videoEl.onplay = (event) => { + if (!isCurrentPlaybackSession(context, playbackSessionId)) return; context.mseWaitingErrorReset = setTimeout(function(self) { // If the video is in H.265, the browser may start playing (even if it doesn't support H.265) and an error may immediately appear. // You need to wait a bit before resetting the error. This will allow for more accurate error counting. + if (!isCurrentPlaybackSession(context, playbackSessionId)) return; self.updateStreamInfo('', ''); //MSE self.resetCountStreamErrors(context.activePlayer); }, 500, context); diff --git a/web/js/video-stream.js b/web/js/video-stream.js index 7232512ced4..89b9dbe9f11 100644 --- a/web/js/video-stream.js +++ b/web/js/video-stream.js @@ -37,6 +37,13 @@ class VideoStream extends VideoRTC { this.currentMode = 'ERROR'; } + set src(value) { + const monitorStream = getMonitorStream(stringToNumber(this.id)); + monitorStream.playbackSessionId = generateUUID(); + this.playbackSessionId = monitorStream.playbackSessionId; + super.src = value; + } + /** * Custom GUI */ @@ -167,6 +174,7 @@ class VideoStream extends VideoRTC { errorHandling(currentMode, message = null) { const monitorStream = getMonitorStream(stringToNumber(this.id)); if (monitorStream) { + if (!isCurrentPlaybackSession(monitorStream, this.playbackSessionId)) return; monitorStream.player = "go2rtc_" + currentMode; monitorStream.streamErrorRegistration(); monitorStream.restart(monitorStream.currentChannelStream); diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 2242ef53178..d2d0d93892e 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -3438,33 +3438,6 @@ async function getTracksFromStream(videoFeedStream) { } - // We'll determine whether we need a video track or just an audio track. - // When playing H.265, the video may not be decoded, but the audio track will play. - const selectorWhatDisplay = document.getElementById('whatDisplay'); - const monitorStream = getMonitorStream(mid); - const defaultWhatDisplay = (typeof eventData !== 'undefined') ? eventData.whatDisplay : (monitorStream) ? monitorStream.whatDisplay : null; - - let videoTrackRequired = true; - if (!selectorWhatDisplay || (-1 !== selectorWhatDisplay.value.toLowerCase().indexOf('default'))) { // Default monitor settings - if (defaultWhatDisplay && (-1 === defaultWhatDisplay.toLowerCase().indexOf('video'))) videoTrackRequired = false; - } else { - if (-1 === selectorWhatDisplay.value.toLowerCase().indexOf('video')) videoTrackRequired = false; - } - if (!videoFeedStream.videoTrack ) { - monitorStream.updateStreamInfo('', 'Video track missing'); - monitorStream.writeTextInfoBlock("Video track missing", {showImg: false}); - } - if (!videoFeedStream.videoTrack && videoTrackRequired && (!videoFeedStream.selectedPlayer || videoFeedStream.selectedPlayer === "go2rtc")) { - // Switch to a different player only when mode=Auto - videoFeedStream.streamErrorRegistration(); - videoFeedStream.restart(videoFeedStream.currentChannelStream); - dispatchTracksReceived(videoFeedStream, { - status: 'aborted', - reason: 'playback-videoTrack-missing' - }); - return; - } - if (videoFeedStream.started === false) { console.debug(`RACE [${playbackSessionId}] activePlayer: "${videoFeedStream.activePlayer || "not defined"}" skip tracksReceived because stream for monitor ID=${mid} stopped`); return; @@ -3482,19 +3455,24 @@ const dispatchTracksReceived = function(videoFeedStream, { status, reason = null } = {}) { - document.dispatchEvent(new CustomEvent('zm:tracksReceived', { - detail: { - monitorId: (typeof eventData !== 'undefined') ? eventData.MonitorId : videoFeedStream?.id, - status, - reason, - activePlayer: videoFeedStream?.activePlayer ?? null, - stream: { - mediaStream: videoFeedStream?.mediaStream ?? null, - audioTrack: videoFeedStream?.audioTrack ?? null, - videoTrack: videoFeedStream?.videoTrack ?? null + const objStream = (videoFeedStream?.dispatchEvent instanceof Function) ? videoFeedStream : videoFeedStream?.element; + if (objStream) { + objStream.dispatchEvent(new CustomEvent('zm:tracksReceived', { + detail: { + monitorId: (typeof eventData !== 'undefined') ? eventData.MonitorId : videoFeedStream?.id, + status, + reason, + activePlayer: videoFeedStream?.activePlayer ?? null, + stream: { + mediaStream: videoFeedStream?.mediaStream ?? null, + audioTrack: videoFeedStream?.audioTrack ?? null, + videoTrack: videoFeedStream?.videoTrack ?? null + } } - } - })); + })); + } else { + console.warn(`No stream found for ${videoFeedStream}. dispatchEvent for 'zm:tracksReceived' will not be added.`); + } }; /** diff --git a/web/skins/classic/views/js/event.js b/web/skins/classic/views/js/event.js index ec361dad010..8989ffed8d5 100644 --- a/web/skins/classic/views/js/event.js +++ b/web/skins/classic/views/js/event.js @@ -1357,9 +1357,14 @@ function onStatsResize(vidWidth) { } function initPage() { - document.addEventListener('zm:tracksReceived', (e) => { - if (e.detail.stream.audioTrack) connectAudioMotion(e.detail.monitorId); - }); + const stream = document.getElementById('videoFeedStream' + eventData.MonitorId); + if (stream) { + stream.addEventListener('zm:tracksReceived', (e) => { + if (e.detail.stream.audioTrack) connectAudioMotion(e.detail.monitorId); + }); + } else { + console.warn(`No stream found for monitor with ID=${eventData.MonitorId}. Listener for 'zm:tracksReceived' not added.`); + } getAvailableTags(); getSelectedTags(); diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index 0ab87dfdb07..95025cd4cdd 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -202,6 +202,7 @@ function streamCmdPlay(action) { } onPlay(); if (action) { + monitorStream.isActive = true; if (monitorStream.started) { //Stream was on pause monitorStream.play(); @@ -213,23 +214,40 @@ function streamCmdPlay(action) { } function streamCmdStop() { - monitorStream.onplay = false; //Without this line, "onPlay" is triggered immediately due to "if (this.onplay) this.onplay();" in MonitorStream.js - //setButtonState('pauseBtn', 'inactive'); - //setButtonState('playBtn', 'unavail'); - //setButtonState('stopBtn', 'active'); - if (currentMonitor.monitorStreamReplayBuffer) { - setButtonState('fastFwdBtn', 'unavail'); - setButtonState('slowFwdBtn', 'unavail'); - setButtonState('slowRevBtn', 'unavail'); - setButtonState('fastRevBtn', 'unavail'); - } + monitorStream.isActive = false; monitorStream.stop(); + updatePlayerControls("stop"); +} + +function updatePlayerControls(state) { + switch (state) { + case 'play': + break; + + case 'pause': + break; + + case 'stop': + monitorStream.onplay = false; //Without this line, "onPlay" is triggered immediately due to "if (this.onplay) this.onplay();" in MonitorStream.js + //setButtonState('pauseBtn', 'inactive'); + //setButtonState('playBtn', 'unavail'); + //setButtonState('stopBtn', 'active'); + if (currentMonitor.monitorStreamReplayBuffer) { + setButtonState('fastFwdBtn', 'unavail'); + setButtonState('slowFwdBtn', 'unavail'); + setButtonState('slowRevBtn', 'unavail'); + setButtonState('fastRevBtn', 'unavail'); + } + //setButtonState('stopBtn', 'unavail'); + //setButtonState('playBtn', 'active'); + setButtonStateWatch('playBtn', 'inactive'); + setButtonStateWatch('stopBtn', 'unavail'); + setButtonStateWatch('pauseBtn', 'hidden'); + break; - //setButtonState('stopBtn', 'unavail'); - //setButtonState('playBtn', 'active'); - setButtonStateWatch('playBtn', 'inactive'); - setButtonStateWatch('stopBtn', 'unavail'); - setButtonStateWatch('pauseBtn', 'hidden'); + default: + console.warn(`Unknown player control state: ${state}`); + } } function streamCmdFastFwd(action) { @@ -911,6 +929,7 @@ function streamReStart(oldId, newId) { zmPanZoom.action('disable', {id: oldId}); if (monitorStream) { + monitorStream.isActive = false; monitorStream.kill(); } else { console.log("No monitorStream?"); @@ -1401,6 +1420,7 @@ function monitorChangeStreamChannel() { if ((monitorStream.activePlayer) && (-1 !== monitorStream.activePlayer.indexOf('go2rtc') || -1 !== monitorStream.activePlayer.indexOf('rtsp2web'))) { streamCmdStop(); setTimeout(function() { + monitorStream.isActive = true; monitorStream.start(streamChannel); onPlay(); monitorsSetScale(monitorId); @@ -1521,6 +1541,7 @@ function stopPage() { prevStateStarted = 'played'; //Stop only if playing (not paused). // We might want to continue status updates so that alarm sounds etc still happen + monitorStream.isActive = false; monitorStream.stop(); } } else { @@ -1535,6 +1556,7 @@ function startPage() { if (monitorStream && prevStateStarted == 'played' && !idleTimeoutTriggered) { prevStateStarted = null; onPlay(); //Set the correct state of the player buttons. + monitorStream.isActive = true; monitorStream.start(monitorStream.currentChannelStream); monitorsSetScale(monitorId); //} else if (prevStateStarted != 'paused') {