From b57e7eec7f04304efc7dc54de52be9bdcffa59d1 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Wed, 1 Jul 2026 22:03:44 -0500 Subject: [PATCH] =?UTF-8?q?fix(#146):=20web=20player=20=E2=80=94=20reconne?= =?UTF-8?q?ct=20drops=20video=20to=20"Waiting=20for=20content"=20(idle=20r?= =?UTF-8?q?eset=20over=20live=20playback)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CONFIRMED from a live console capture (restore cache -> video plays -> reconnect -> "Playlist unchanged" -> screen falls to "Waiting for content..."). Audio survived because only the "showing content" VIEW was covered, not the audio path. ROOT CAUSE: the server re-emits device:paired on EVERY re-register of an already-paired device (ws/deviceSocket.js:510) — i.e. on every reconnect, while content is already playing. The player's device:paired handler called showStatus('Waiting for content...') UNCONDITIONALLY (the "falls through to idle" sibling), putting the idle overlay OVER the live video. The following device:playlist-update -> "Playlist unchanged" branch returned early and never cleared it, so the idle screen stuck on top of playing content. FIX (idle screen only when genuinely idle; unchanged is a strict no-op that keeps playback): - lib/player-media-health.js: new shouldShowIdle(state) — idle ONLY when nothing is playing AND there's genuinely no content. Already-playing (or content-present-about-to-render) is never idle. - device:paired handler: gate showStatus on shouldShowIdle({isPlaying, hasContent}) instead of showing it unconditionally. On a reconnect while playing -> no-op. - "Playlist unchanged" branch: when healthy playback is confirmed, hideStatus() to clear any stale idle overlay a reconnect's device:paired may have put up — so the confirmation can never leave "Waiting for content..." over live content. Still leaves the actual media element exactly as-is (no teardown, no flicker). - sw.js cache v10 -> v11. SIBLING SCAN: device:paired was the only unconditional idle reset. The connect() idle prompts (connecting / connecting_muted) were already guarded by !isPlaying; empty-playlist and no-renderable idles are genuine. Tests: player-media-health.test.js +2 (shouldShowIdle: playing never idle; idle only when empty+not-playing). Inline player JS syntax-checked; module served + guard referenced on a booted server. Suite 318/318. Co-Authored-By: Claude Opus 4.8 (1M context) --- server/lib/player-media-health.js | 14 +++++++++++++- server/player/index.html | 15 ++++++++++++++- server/player/sw.js | 2 +- server/test/player-media-health.test.js | 15 ++++++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/server/lib/player-media-health.js b/server/lib/player-media-health.js index f460780..4322fd8 100644 --- a/server/lib/player-media-health.js +++ b/server/lib/player-media-health.js @@ -44,5 +44,17 @@ return !s.surfaceAttached; } - return { needsReattach: needsReattach }; + // Whether the idle "Waiting for content..." screen should be shown, given player state. + // THE RECONNECT BUG: the server re-emits device:paired on every re-register of an already- + // paired device, and the player showed the idle overlay UNCONDITIONALLY — covering live + // content (audio kept playing underneath), and the following "Playlist unchanged" left it + // up. Rule: only fall to idle when nothing is playing AND there is genuinely no content to + // play. Already playing, or content present and about to render, is NEVER idle. + function shouldShowIdle(state) { + var s = state || {}; + if (s.isPlaying) return false; // something is playing -> never cover it with idle + return !s.hasContent; // idle only when there's genuinely no content + } + + return { needsReattach: needsReattach, shouldShowIdle: shouldShowIdle }; }); diff --git a/server/player/index.html b/server/player/index.html index d95cd08..dc788c3 100644 --- a/server/player/index.html +++ b/server/player/index.html @@ -807,7 +807,15 @@ saveConfig(config); console.log('Paired as:', data.name); document.getElementById('setupScreen').style.display = 'none'; - showStatus('Waiting for content...'); + // #146 fix: the server re-emits device:paired on EVERY re-register of an already- + // paired device (deviceSocket.js), i.e. on every reconnect — while content is already + // playing. Showing the idle "Waiting for content..." overlay unconditionally covered + // the live video (audio kept playing underneath) and the subsequent "Playlist + // unchanged" left it up. Only fall to idle when nothing is actually playing. + const showIdle = window.PlayerMediaHealth + ? PlayerMediaHealth.shouldShowIdle({ isPlaying: isPlaying, hasContent: playlist.length > 0 }) + : !isPlaying; + if (showIdle) showStatus('Waiting for content...'); }); socket.on('device:unpaired', () => { @@ -1333,6 +1341,11 @@ if (window.PlayerMediaHealth && PlayerMediaHealth.needsReattach(state)) { console.log('[refresh] media surface lost on no-change refresh — re-attaching current item'); playCurrentItem(); + } else if (isPlaying) { + // #146 fix: unchanged + already playing must LEAVE playback exactly as-is. Clear + // any stale idle overlay (e.g. the one a reconnect's device:paired put up) so the + // "unchanged" confirmation never leaves "Waiting for content..." over live content. + hideStatus(); } } catch (e) { /* never let the health check break a refresh */ } return; diff --git a/server/player/sw.js b/server/player/sw.js index 08a6af9..bb78c16 100644 --- a/server/player/sw.js +++ b/server/player/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'rd-player-v10'; +const CACHE_NAME = 'rd-player-v11'; // Install: skip waiting to activate immediately self.addEventListener('install', (event) => { diff --git a/server/test/player-media-health.test.js b/server/test/player-media-health.test.js index a1fb825..afdd0ba 100644 --- a/server/test/player-media-health.test.js +++ b/server/test/player-media-health.test.js @@ -6,7 +6,7 @@ const { test } = require('node:test'); const assert = require('node:assert/strict'); -const { needsReattach } = require('../lib/player-media-health'); +const { needsReattach, shouldShowIdle } = require('../lib/player-media-health'); const video = (o) => ({ isPlaying: true, hasCurrentItem: true, itemKind: 'video', videoEl: o, surfaceAttached: true }); @@ -40,3 +40,16 @@ test('non-video surface (image/youtube/widget): re-attach only when the surface assert.equal(needsReattach({ ...base, itemKind: 'youtube', surfaceAttached: false }), true); assert.equal(needsReattach({ ...base, itemKind: 'widget', surfaceAttached: true }), false); }); + +// shouldShowIdle — the reconnect reset guard (device:paired / unchanged reconciliation). +test('THE RECONNECT BUG: already playing -> NEVER show the idle "Waiting" screen', () => { + // reconnect re-emits device:paired while content is playing: must stay on the content + assert.equal(shouldShowIdle({ isPlaying: true, hasContent: true }), false); + assert.equal(shouldShowIdle({ isPlaying: true, hasContent: false }), false); +}); + +test('idle screen shows ONLY when genuinely no content and nothing playing', () => { + assert.equal(shouldShowIdle({ isPlaying: false, hasContent: false }), true); // first pair, empty + assert.equal(shouldShowIdle({ isPlaying: false, hasContent: true }), false); // content present, about to render + assert.equal(shouldShowIdle(undefined), true); +});