From 575e5484b31f6fc86add476df818edebb08db3fb Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Mon, 5 Oct 2026 18:36:33 -0400 Subject: [PATCH] fix: skip an unconfigured go2rtc under the Auto player With its player left on Auto, a monitor with go2rtc enabled had selectNextPlayer() pick go2rtc_webrtc first, and with ZM_GO2RTC_PATH empty select_go2rtc() then alerted "ZM_GO2RTC_PATH is empty", on the montage for every such monitor, although nobody had chosen go2rtc. The fallback chain already skipped go2rtc for a monitor without it enabled; skip it when ZM_GO2RTC_PATH is empty too. A monitor or browser that chose go2rtc reaches select_go2rtc() directly and still gets the alert. A node test drives selectPlayer() through the restart chain for Auto without a path, Auto with one, and go2rtc chosen explicitly. Co-Authored-By: Claude Opus 5.5 --- tests/js/monitorstream-go2rtc-auto.test.js | 121 +++++++++++++++++++++ web/js/MonitorStream.js | 5 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/js/monitorstream-go2rtc-auto.test.js diff --git a/tests/js/monitorstream-go2rtc-auto.test.js b/tests/js/monitorstream-go2rtc-auto.test.js new file mode 100644 index 000000000..96804dc9d --- /dev/null +++ b/tests/js/monitorstream-go2rtc-auto.test.js @@ -0,0 +1,121 @@ +'use strict'; + +// A monitor with go2rtc enabled and its player left on Auto tried go2rtc first, +// and with ZM_GO2RTC_PATH empty select_go2rtc() alerted "ZM_GO2RTC_PATH is empty" +// on every page that showed it, montage included, although nobody had asked for +// go2rtc. Under Auto an unconfigured go2rtc is now skipped for the next player; +// the alert stays for a monitor or browser that chose go2rtc explicitly. + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const src = fs.readFileSync( + path.join(__dirname, '../../web/js/MonitorStream.js'), 'utf8'); + +let passed = 0; +let failed = 0; +function test(name, fn) { + try { + fn(); + console.log(' ok ' + name); + passed++; + } catch (e) { + console.error(' FAIL ' + name); + console.error(' ' + (e.stack || e.message)); + failed++; + } +} + +function makeChainable() { + const proxy = new Proxy(function() {}, { + get: (target, prop) => (prop === 'then' ? undefined : proxy), + apply: () => proxy, + }); + return proxy; +} + +function loadMonitorStream(go2rtcPath) { + const globals = { + ZM_GO2RTC_PATH: go2rtcPath, + parseInt, Number, Object, Math, String, Array, JSON, + alerts: [], + alert(msg) { + this.alerts.push(msg); + }, + $j: makeChainable(), + $: makeChainable(), + console: {log() {}, warn() {}, error() {}, debug() {}}, + currentView: 'watch', + getCookie: () => 'true', + setCookie: () => {}, + document: { + getElementById: () => null, + querySelector: () => null, + createElement: () => ({}), + }, + setTimeout: () => 0, + clearTimeout: () => 0, + setInterval: () => 0, + clearInterval: () => 0, + }; + const sandbox = new Proxy(globals, { + has: () => true, // make every bare identifier resolve against this object + get: (target, prop) => (prop === Symbol.unscopables ? undefined : target[prop]), + }); + globals.window = sandbox; + vm.createContext(sandbox); + vm.runInContext(src, sandbox, {filename: 'MonitorStream.js'}); + assert.strictEqual(typeof globals.MonitorStream, 'function', + 'MonitorStream.js did not define MonitorStream'); + return globals; +} + + +// Build a go2rtc-enabled monitor and record which player selectPlayer() starts. +function select(go2rtcPath, defaultPlayer) { + const sandbox = loadMonitorStream(go2rtcPath); + const monitor = new sandbox.MonitorStream({ + id: 2, name: 'test', connKey: null, url: '', url_to_zms: '', + width: 640, height: 480, Go2RTCEnabled: true, DefaultPlayer: defaultPlayer, + }); + const started = []; + monitor.isActive = true; + monitor.select_go2rtc = () => started.push('go2rtc'); + monitor.select_janus = () => started.push('janus'); + monitor.select_rtsp2web = () => started.push('rtsp2web'); + monitor.select_zms = () => started.push('zms'); + // selectNextPlayer() switches by setting this.player and restarting, and + // restart() comes back through selectPlayer() with the new player. + let depth = 0; + monitor.restart = () => { + started.push('restart:' + monitor.player); + if (++depth < 10) monitor.selectPlayer('default'); + }; + monitor.selectPlayer('default'); + return {started, alerts: sandbox.alerts}; +} + +console.log('MonitorStream player selection with go2rtc unconfigured'); + +test('Auto with no ZM_GO2RTC_PATH does not try go2rtc or alert', () => { + const {started, alerts} = select('', ''); + assert.ok(!started.includes('go2rtc'), 'started go2rtc: ' + started.join(',')); + assert.deepStrictEqual(alerts, []); + assert.ok(started.includes('zms'), 'did not fall back to zms: ' + started.join(',')); +}); + +test('go2rtc chosen for the monitor still tries go2rtc', () => { + const {started} = select('', 'go2rtc'); + assert.deepStrictEqual(started, ['go2rtc']); +}); + +test('Auto with ZM_GO2RTC_PATH set uses go2rtc', () => { + const {started, alerts} = select('http://localhost:1984', ''); + assert.ok(started.includes('go2rtc'), 'did not start go2rtc: ' + started.join(',')); + assert.deepStrictEqual(alerts, []); +}); + +console.log('\n' + passed + ' passed, ' + failed + ' failed'); +process.exit(failed ? 1 : 0); diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index e82430621..3ce2d59f1 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -2319,7 +2319,10 @@ function MonitorStream(monitorData) { while (idx !== -1 && idx + 1 < keys.length) { const nextKey = keys[++idx]; const nextName = this.playerPriority[nextKey]['name']; - if (nextName.indexOf('go2rtc') !== -1 && !this.Go2RTCEnabled) continue; + // Skip an unconfigured go2rtc here rather than letting select_go2rtc() alert: + // this is the Auto path, so nobody chose go2rtc. A monitor or browser that + // did choose it reaches select_go2rtc() directly and still gets the alert. + if (nextName.indexOf('go2rtc') !== -1 && (!this.Go2RTCEnabled || !ZM_GO2RTC_PATH)) continue; if (nextName.indexOf('rtsp2web') !== -1 && !this.RTSP2WebEnabled) continue; if (nextName.indexOf('janus') !== -1 && !this.janusEnabled) continue; if (this.selectedPlayer === 'go2rtc' && nextName.indexOf('go2rtc') === -1 && nextName.indexOf('zms') === -1 ) continue;