// Render the slide at the requested index within the active set. async function renderSlideAtIndex(sourceSlides, targetIndex, options) { const availableSlides = Array.isArray(sourceSlides) ? sourceSlides : []; if (!availableSlides.length) { renderEmpty(slides.length ? 'No slides are scheduled for this time.' : 'No slides assigned to this screen yet.'); lastRenderedViewKey = getCurrentRenderKey(availableSlides); return false; } let normalizedIndex = Number(targetIndex || 0); if (!Number.isFinite(normalizedIndex) || normalizedIndex < 0 || normalizedIndex >= availableSlides.length) { } var slideCount = availableSlides.length; var currentIndex = normalizedIndex; var slide = null; var attempts = 0; var renderRequestId = ++slideRenderRequestId; while (attempts < slideCount) { slide = availableSlides[currentIndex]; if (!slide) { break; } if (currentPlaylistSkipUnavailableRtmp && typeof probeRtmpSlideAvailability === 'function') { var isAvailable = await probeRtmpSlideAvailability(slide); if (!isAvailable) { if (renderRequestId !== slideRenderRequestId) { return false; } currentIndex = (currentIndex + 1) % slideCount; attempts += 1; slide = null; continue; } } if (renderRequestId !== slideRenderRequestId) { break; } break; } if (!slide) { if (currentPlaylistSkipUnavailableRtmp) { renderEmpty(slides.length ? 'No RTMP slides are currently available.' : 'No slides assigned to this screen yet.'); lastRenderedViewKey = getCurrentRenderKey(availableSlides); scheduleRefreshRetry(); return false; } renderEmpty(slides.length ? 'No slides are scheduled for this time.' : 'No slides assigned to this screen yet.'); return false; } index = currentIndex; var markup = buildSlideMarkup(slide); var shouldFade = !(options && options.skipFade) && currentPlaylistFadeBetweenSlides; var mediaDelayMs = slide && slide.use_video_duration ? 0 : undefined; renderSlideMarkup(markup, shouldFade, mediaDelayMs); if (typeof scheduleSlideMarkupPreload === 'function') { scheduleSlideMarkupPreload(availableSlides, currentIndex); } sendCommandState(slide); if (!isPaused) { scheduleSlideAdvance(getSlideAdvanceDelay(slide)); } lastRenderedViewKey = getCurrentRenderKey(availableSlides); return true; } // Calculate the configured time from slide entry to the next transition. function getSlideAdvanceDelay(slide) { var durationMs = Math.max(1, Number(slide && slide.duration_seconds || 10)) * 1000; if (slide && slide.use_video_duration) { return currentPlaylistFadeBetweenSlides ? getSlideHoldDelay(Math.max(1, durationMs - slideFadeLengthMs)) : getSlideHoldDelay(durationMs); } if (currentPlaylistFadeBetweenSlides) { return getSlideHoldDelay(Math.max(1, durationMs - slideFadeOffsetMs)); } return getSlideHoldDelay(durationMs); } // Promote a deferred playlist update at the next safe point. function applyPendingPlaylistUpdate() { if (!pendingPlaylistUpdate) { return false; } var nextIndex = Number(index || 0); slides = pendingPlaylistUpdate.slides; currentPlaylistSignature = pendingPlaylistUpdate.signature; currentPlaylistFadeBetweenSlides = pendingPlaylistUpdate.fadeBetweenSlides; currentPlaylistSkipUnavailableRtmp = Boolean(pendingPlaylistUpdate.skipUnavailableRtmp); pendingPlaylistUpdate = null; clearActiveSlidesCache(); slideMarkupCache = Object.create(null); templateLayoutCache = Object.create(null); templateRenderPlanCache = Object.create(null); renderCacheViewportKey = window.innerWidth + 'x' + window.innerHeight; const activeSlides = getCurrentActiveSlides(); if (!Number.isFinite(nextIndex) || nextIndex < 0) { nextIndex = 0; } index = activeSlides.length ? Math.min(nextIndex, activeSlides.length - 1) : 0; logDebug('Applied updated playlist on slide transition.'); return true; } // Render the current active slide or the empty state. function showCurrent(options) { clearSlideTimer(); const activeSlides = getCurrentActiveSlides(); syncWebpagePreloads(activeSlides, index); if (typeof syncRtmpWarmups === 'function') { syncRtmpWarmups(activeSlides, index); } if (typeof scheduleSlideMarkupPreload === 'function') { scheduleSlideMarkupPreload(activeSlides, index); } if (!activeSlides.length) { renderEmpty(slides.length ? 'No slides are scheduled for this time.' : 'No slides assigned to this screen yet.'); lastRenderedViewKey = getCurrentRenderKey(activeSlides); return; } void renderSlideAtIndex(activeSlides, index, options); lastRenderedViewKey = getCurrentRenderKey(activeSlides); } function isSlideInList(slide, slideList) { if (!slide || !Array.isArray(slideList)) { return false; } return slideList.some(function (item) { return item && Number(item.id) === Number(slide.id); }); } // Skip the current slide when an RTMP feed is unavailable and the playlist asks for it. function handleRtmpPlaybackFailure(message, options) { if (!currentPlaylistSkipUnavailableRtmp) { return false; } var silent = Boolean(options && options.silent); const activeSlides = getCurrentActiveSlides(); if (!activeSlides.length) { return false; } clearSlideTimer(); if (!silent) { logDebug('Skipping RTMP slide because the stream is unavailable.', String(message || ''), 'error'); } const nextIndex = (Number(index || 0) + 1) % activeSlides.length; window.setTimeout(function () { if (!getCurrentActiveSlides().length) { return; } void renderSlideAtIndex(getCurrentActiveSlides(), nextIndex); }, 0); return true; } // Fetch the latest playlist and queue any updates. function refresh(applyImmediately) { var request = new XMLHttpRequest(); var url = window.location.origin + '/api/screens/' + encodeURIComponent(slug) + '/playlist?ts=' + Date.now(); request.open('GET', url, true); request.timeout = 2500; if (window.__pulsePageAuthToken) { request.setRequestHeader('x-pulse-page-auth', window.__pulsePageAuthToken); } if (typeof getCommandClientId === 'function') { request.setRequestHeader('x-pulse-client-id', getCommandClientId()); } if (currentPlaylistEtag) { request.setRequestHeader('If-None-Match', currentPlaylistEtag); } request.onreadystatechange = function () { if (request.readyState !== 4) { return; } if (request.status === 304) { if (!initialData || !Array.isArray(initialData.apiSources) || !Array.isArray(initialData.weatherLocations)) { currentPlaylistEtag = ''; refresh(applyImmediately); return; } logDebug('Playlist refresh completed with no changes.'); markRefreshHealthy(); setOfflineBannerVisible(false); if (lastRenderedSlide && getCurrentActiveSlides().length < 2) { scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide)); } return; } if (request.status < 200 || request.status >= 300) { if (request.status === 401 || request.status === 403) { window.location.replace('/'); return; } setOfflineBannerVisible(true); scheduleRefreshRetry(); logDebug( 'Screen not found or playlist unavailable.', ['URL: ' + url, 'Status: ' + request.status + ' ' + request.statusText, 'Response: ' + String(request.responseText || '').slice(0, 1000)].join(' | '), 'error' ); return; } try { const responseEtag = String(request.getResponseHeader('ETag') || '').trim(); const data = JSON.parse(request.responseText || '{}'); const nextSignature = getPlaylistRevision(data); logDebug('Playlist refresh completed.', 'Revision: ' + nextSignature); const nextSlides = Array.isArray(data.slides) ? data.slides.map(normalizeSlide) : []; const nextActiveSlides = getActiveSlidesFrom(nextSlides); const nextFadeBetweenSlides = Boolean(data && data.playlist && data.playlist.fade_between_slides); const nextSkipUnavailableRtmp = Boolean(data && data.playlist && data.playlist.skip_unavailable_rtmp); const hadSourceData = !(typeof window !== 'undefined' && window.initialData === null); var refreshedInitialData = Object.assign({}, typeof initialData !== 'undefined' && initialData ? initialData : (window.initialData || {}), { screen: data.screen || null, playlist: data.playlist || null, slides: nextSlides, rssFeeds: Array.isArray(data.rssFeeds) ? data.rssFeeds : [], apiSources: Array.isArray(data.apiSources) ? data.apiSources : [], timetableGroups: Array.isArray(data.timetableGroups) ? data.timetableGroups : [], weatherLocations: Array.isArray(data.weatherLocations) ? data.weatherLocations : [], revision: nextSignature }); if (typeof initialData !== 'undefined') { initialData = refreshedInitialData; } window.initialData = refreshedInitialData; if (!hadSourceData) { slideMarkupCache = Object.create(null); templateLayoutCache = Object.create(null); templateRenderPlanCache = Object.create(null); } savePlaylistSnapshot({ slides: nextSlides, signature: nextSignature, fadeBetweenSlides: nextFadeBetweenSlides, skipUnavailableRtmp: nextSkipUnavailableRtmp, etag: responseEtag }); markRefreshHealthy(); setOfflineBannerVisible(false); const currentActiveSlides = getCurrentActiveSlides(); if (responseEtag) { currentPlaylistEtag = responseEtag; } if (!currentPlaylistSignature) { slides = nextSlides; currentPlaylistSignature = nextSignature; currentPlaylistFadeBetweenSlides = nextFadeBetweenSlides; currentPlaylistSkipUnavailableRtmp = nextSkipUnavailableRtmp; index = 0; showCurrent(); sendCommandState(lastRenderedSlide); return; } if (nextSignature === currentPlaylistSignature || (pendingPlaylistUpdate && nextSignature === pendingPlaylistUpdate.signature)) { if (!hadSourceData) { showCurrent({ skipFade: true }); return; } if (currentActiveSlides.length < 2 && lastRenderedSlide) { scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide)); } return; } syncWebpagePreloads(nextActiveSlides, index); if (typeof syncRtmpWarmups === 'function') { syncRtmpWarmups(nextActiveSlides, index); } if (typeof scheduleSlideMarkupPreload === 'function') { scheduleSlideMarkupPreload(nextActiveSlides, index); } if (nextActiveSlides.length < 2) { pendingPlaylistUpdate = { slides: nextSlides, signature: nextSignature, fadeBetweenSlides: nextFadeBetweenSlides, skipUnavailableRtmp: nextSkipUnavailableRtmp }; if (applyImmediately) { applyPendingPlaylistUpdate(); showCurrent(); return; } if (!isSlideInList(lastRenderedSlide, nextSlides)) { applyPendingPlaylistUpdate(); showCurrent(); return; } logDebug('Playlist update detected; applying on next slide transition.'); return; } pendingPlaylistUpdate = { slides: nextSlides, signature: nextSignature, fadeBetweenSlides: nextFadeBetweenSlides, skipUnavailableRtmp: nextSkipUnavailableRtmp }; if (applyImmediately) { applyPendingPlaylistUpdate(); showCurrent(); return; } logDebug('Playlist update detected; applying on next slide transition.'); } catch (_error) { logDebug( 'Unable to load screen playlist.', ['URL: ' + url, 'Response: ' + String(request.responseText || '').slice(0, 1000)].join(' | '), 'error' ); setOfflineBannerVisible(true); scheduleRefreshRetry(); } }; request.onerror = function () { logDebug( 'Unable to load screen playlist.', ['URL: ' + url, 'Network error during request.'].join(' | '), 'error' ); setOfflineBannerVisible(true); scheduleRefreshRetry(); if (lastRenderedSlide && getCurrentActiveSlides().length < 2) { scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide)); } }; request.ontimeout = function () { logDebug( 'Playlist refresh timed out.', ['URL: ' + url, 'Timeout after ' + request.timeout + 'ms'].join(' | '), 'error' ); setOfflineBannerVisible(true); scheduleRefreshRetry(); if (lastRenderedSlide && getCurrentActiveSlides().length < 2) { scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide)); } }; request.send(); }