mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-15 23:03:14 -06:00
fix(#146): web player — reconnect drops video to "Waiting for content" (idle reset over live playback)
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) <noreply@anthropic.com>
This commit is contained in:
parent
26c72d62bf
commit
b57e7eec7f
|
|
@ -44,5 +44,17 @@
|
||||||
return !s.surfaceAttached;
|
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 };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -807,7 +807,15 @@
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
console.log('Paired as:', data.name);
|
console.log('Paired as:', data.name);
|
||||||
document.getElementById('setupScreen').style.display = 'none';
|
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', () => {
|
socket.on('device:unpaired', () => {
|
||||||
|
|
@ -1333,6 +1341,11 @@
|
||||||
if (window.PlayerMediaHealth && PlayerMediaHealth.needsReattach(state)) {
|
if (window.PlayerMediaHealth && PlayerMediaHealth.needsReattach(state)) {
|
||||||
console.log('[refresh] media surface lost on no-change refresh — re-attaching current item');
|
console.log('[refresh] media surface lost on no-change refresh — re-attaching current item');
|
||||||
playCurrentItem();
|
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 */ }
|
} catch (e) { /* never let the health check break a refresh */ }
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const CACHE_NAME = 'rd-player-v10';
|
const CACHE_NAME = 'rd-player-v11';
|
||||||
|
|
||||||
// Install: skip waiting to activate immediately
|
// Install: skip waiting to activate immediately
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
const { test } = require('node:test');
|
const { test } = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
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 });
|
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: 'youtube', surfaceAttached: false }), true);
|
||||||
assert.equal(needsReattach({ ...base, itemKind: 'widget', surfaceAttached: true }), false);
|
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);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue