From 1258a3d944f31a7e90cb9213e30c4ccb9e98dfd7 Mon Sep 17 00:00:00 2001 From: Technomancer702 Date: Tue, 13 Oct 2026 18:03:49 -0700 Subject: [PATCH] Keep Live TV playing while browsing other pages --- README.md | 2 +- public/js/pages/LivePage.js | 2 - test/enigma2-playback.test.js | 78 +++++++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d6871ed..af5942b 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ All transcoding and stream processing settings are found in **Settings → Trans Add the receiver's bouquet export URL (for example, `http://receiver/web/services.m3u?bRef=...`) as an M3U source. Program tags select the matching video/audio service during probing, remux and transcoding. Tagged raw streams are remuxed even when Force Remux is off. -For a single-tuner receiver, enable **Zap before streaming (Enigma2/OpenWebif)** on that source. The player releases its previous stream, tunes the configured receiver, and waits for the source's zap delay before starting playback. The default delay is 1500 ms; values from 0 to 10000 ms are supported. Stop, changing channels, and leaving Live TV cancel pending playback startup. The receiver address comes from the administrator-managed source URL, and only imported channels can be tuned. +For a single-tuner receiver, enable **Zap before streaming (Enigma2/OpenWebif)** on that source. The player releases its previous stream, tunes the configured receiver, and waits for the source's zap delay before starting playback. The default delay is 1500 ms; values from 0 to 10000 ms are supported. Stop and changing channels cancel pending playback startup. Live TV continues playing when you minimize the player and browse the guide or other pages. The receiver address comes from the administrator-managed source URL, and only imported channels can be tuned. ## Troubleshooting diff --git a/public/js/pages/LivePage.js b/public/js/pages/LivePage.js index 3f5fd58..908840c 100644 --- a/public/js/pages/LivePage.js +++ b/public/js/pages/LivePage.js @@ -103,8 +103,6 @@ class LivePage { hide() { document.removeEventListener('keydown', this.handleKeydown); - // Stop the stream when leaving Live TV so the transcode session is released - this.app.player?.stop(); } } diff --git a/test/enigma2-playback.test.js b/test/enigma2-playback.test.js index 244d02d..0f7bf53 100644 --- a/test/enigma2-playback.test.js +++ b/test/enigma2-playback.test.js @@ -60,25 +60,67 @@ for (const name of ['VideoPlayer', 'WatchPage']) { } } -test('Stop and leaving Live TV cancel the actual zap delay without starting playback', async () => { - for (const navigate of [false, true]) { - let requested; - const request = new Promise(resolve => { requested = resolve; }); - const f = playerFixture('VideoPlayer', { forceVideoTranscode: true }, async (url, options) => { - requested(options.signal); - return { ok: true, json: async () => ({ success: true }) }; - }); - const playing = f.player.play({ streamId: 'service', sourceId: 1, zapBeforeStreaming: true, zapDelay: 10000 }, 'http://receiver/multi.ts'); - const signal = await request; - if (navigate) { - vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js/pages/LivePage.js'), 'utf8'), f.context); - new f.context.window.LivePage({ player: f.player }).hide(); - } else f.player.stop(); - await playing; - assert.equal(signal.aborted, true); - assert.equal(f.sessions.length, 0); - assert.equal(f.player.video.src, ''); +test('Stop cancels the actual zap delay without starting playback', async () => { + let requested; + const request = new Promise(resolve => { requested = resolve; }); + const f = playerFixture('VideoPlayer', { forceVideoTranscode: true }, async (url, options) => { + requested(options.signal); + return { ok: true, json: async () => ({ success: true }) }; + }); + const playing = f.player.play({ streamId: 'service', sourceId: 1, zapBeforeStreaming: true, zapDelay: 10000 }, 'http://receiver/multi.ts'); + const signal = await request; + f.player.stop(); + await playing; + assert.equal(signal.aborted, true); + assert.equal(f.sessions.length, 0); + assert.equal(f.player.video.src, ''); +}); + +function navigationFixture(f) { + Object.assign(f.context.document, { addEventListener() {}, querySelectorAll: () => [] }); + f.context.history = { pushState() {}, replaceState() {} }; + vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js/app.js'), 'utf8') + ';window.TestApp = App;', f.context); + vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js/pages/LivePage.js'), 'utf8'), f.context); + const app = Object.create(f.context.window.TestApp.prototype); + app.player = f.player; + app.channelList = { channels: [{}] }; + app.pages = { live: new f.context.window.LivePage(app) }; + app.currentPage = 'live'; + return app; +} + +test('browsing the guide and catalogs preserves Live TV playback and its session', async () => { + const f = playerFixture('VideoPlayer', { forceVideoTranscode: true }); + await f.start(); + let released = 0; + f.player.playbackSessions = { stop() { released++; } }; + f.player.currentSessionId = 'live-session'; + const source = f.player.video.src, generation = f.player.playbackGeneration; + const app = navigationFixture(f); + for (const page of ['guide', 'movies', 'series']) { + app.navigateTo('live'); + app.navigateTo(page); + assert.equal(f.player.video.src, source); + assert.equal(f.player.currentSessionId, 'live-session'); + assert.equal(f.player.playbackGeneration, generation); } + assert.equal(released, 0); +}); + +test('browsing during a zap delay lets the selected live channel start normally', async () => { + let requested; + const ready = new Promise(resolve => { requested = resolve; }); + const f = playerFixture('VideoPlayer', { forceVideoTranscode: true }, async (url, options) => { + requested(options.signal); + return { ok: true, json: async () => ({ success: true }) }; + }); + const playing = f.player.play({ streamId: 'service', sourceId: 1, zapBeforeStreaming: true, zapDelay: 0 }, 'http://receiver/multi.ts'); + const signal = await ready; + navigationFixture(f).navigateTo('guide'); + assert.equal(signal.aborted, false); + await playing; + assert.equal(f.sessions.length, 1); + assert.equal(f.player.video.src, '/api/session/stream.m3u8'); }); test('Stop cancels an in-flight zap request and releases the old session before retuning', async () => {