diff --git a/server/player/index.html b/server/player/index.html index 6ed46a4..a2d3872 100644 --- a/server/player/index.html +++ b/server/player/index.html @@ -423,6 +423,15 @@ // playback muted). let userHasInteracted = false; let advanceTimer = null; + // Buffered widget swap (#directory-board black-cycle): build the next widget iframe + // behind the current content and reveal it only on 'load', so a widget reload never + // blanks the screen. WIDGET_SWAP_TIMEOUT_MS reveals anyway if 'load' never fires (a + // network hang must not leave a frozen/blank board). A solo/held widget re-fetches its + // data every WIDGET_SOLO_REFRESH_MS — decoupled from duration_sec, because a static + // board re-querying the DB + re-rendering every few seconds, fleet-wide, is pure waste. + const WIDGET_SWAP_TIMEOUT_MS = 8000; + const WIDGET_SOLO_REFRESH_MS = 60000; + let pendingWidgetSwap = null; // { iframe, timer } while a new widget iframe loads // Per-zone rotation timers (multi-zone). Each zone advances independently on // its own interval, decoupled from the fullscreen advanceTimer/nextItem. let zoneTimers = {}; @@ -2035,8 +2044,16 @@ zoneTimers = {}; } - function teardownCurrentMedia() { - if (advanceTimer) { clearTimeout(advanceTimer); advanceTimer = null; } + function teardownCurrentMedia(keep) { + // On a buffered widget reveal (keep set) the widget's advance/refresh timer was just + // armed by renderContent and must survive; only a real teardown cancels it. + if (!keep && advanceTimer) { clearTimeout(advanceTimer); advanceTimer = null; } + // Cancel any in-flight buffered widget swap so its deferred reveal can't fire after + // we've torn down (which would drop the incoming content). reveal() nulls this before + // calling teardownCurrentMedia(iframe), so preserving `keep` is safe. + if (pendingWidgetSwap && pendingWidgetSwap.iframe !== keep) { + clearTimeout(pendingWidgetSwap.timer); pendingWidgetSwap = null; + } clearZoneTimers(); const container = document.getElementById('playerContainer'); if (container) { @@ -2048,7 +2065,13 @@ v.load(); } catch (e) { /* element may already be detached */ } }); - container.innerHTML = ''; + if (keep) { + // Buffered widget swap: drop the outgoing content but keep the freshly-loaded + // iframe we're swapping in. + Array.from(container.children).forEach(ch => { if (ch !== keep) { try { ch.remove(); } catch (e) {} } }); + } else { + container.innerHTML = ''; + } } // #146 fix: also release currentVideoEl even if it was DETACHED from the container — // a detached-but-playing