351 lines
13 KiB
JavaScript
351 lines
13 KiB
JavaScript
// 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();
|
|
}
|