557 lines
15 KiB
JavaScript
557 lines
15 KiB
JavaScript
// Capture the current viewport dimensions.
|
|
function getCurrentViewport() {
|
|
return {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
};
|
|
}
|
|
|
|
var slideOutroTimers = [];
|
|
|
|
// Command websocket and player-state helpers.
|
|
// Send the current playback state to the command websocket.
|
|
function sendCommandState(currentSlide) {
|
|
if (!commandSocket || commandSocket.readyState !== WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
commandSocket.send(JSON.stringify({
|
|
type: 'state',
|
|
clientId: getCommandClientId(),
|
|
clientName: getOnboardingClientName() || null,
|
|
deviceId: getOnboardingDeviceId() || null,
|
|
userAgent: window.navigator.userAgent || '',
|
|
page: window.location.href,
|
|
viewport: getCurrentViewport(),
|
|
paused: isPaused,
|
|
blackout: isBlackout,
|
|
currentSlide: currentSlide ? {
|
|
id: currentSlide.id || null,
|
|
title: currentSlide.title || '',
|
|
kind: currentSlide.kind || '',
|
|
playlistSignature: currentPlaylistSignature || ''
|
|
} : null
|
|
}));
|
|
}
|
|
|
|
// Debounce command-state updates during rapid changes.
|
|
function scheduleCommandStateUpdate() {
|
|
if (commandStateTimer) {
|
|
window.clearTimeout(commandStateTimer);
|
|
}
|
|
commandStateTimer = window.setTimeout(function () {
|
|
commandStateTimer = null;
|
|
sendCommandState(lastRenderedSlide);
|
|
}, 300);
|
|
}
|
|
|
|
// Debounce rerenders after viewport changes.
|
|
function scheduleViewportRenderUpdate() {
|
|
if (viewportRenderTimer) {
|
|
window.clearTimeout(viewportRenderTimer);
|
|
}
|
|
viewportRenderTimer = window.setTimeout(function () {
|
|
viewportRenderTimer = null;
|
|
if (slides.length) {
|
|
var activeSlides = getCurrentActiveSlides();
|
|
if (getCurrentRenderKey(activeSlides) === lastRenderedViewKey) {
|
|
return;
|
|
}
|
|
showCurrent();
|
|
}
|
|
}, 150);
|
|
}
|
|
|
|
// Cancel the current slide-advance timer.
|
|
function clearSlideTimer() {
|
|
if (timer) {
|
|
window.clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
clearSlideOutroTimer();
|
|
}
|
|
|
|
// Cancel any pending outro triggers for the current slide.
|
|
function clearSlideOutroTimer() {
|
|
if (!Array.isArray(slideOutroTimers) || !slideOutroTimers.length) {
|
|
slideOutroTimers = [];
|
|
return;
|
|
}
|
|
|
|
slideOutroTimers.forEach(function (timerId) {
|
|
window.clearTimeout(timerId);
|
|
});
|
|
slideOutroTimers = [];
|
|
}
|
|
|
|
// Return the rendered slide root that is currently on screen.
|
|
function getCurrentSlideRoot() {
|
|
var shells = Array.prototype.slice.call(app ? app.querySelectorAll('.slide-shell') : []);
|
|
if (shells.length) {
|
|
return shells[shells.length - 1];
|
|
}
|
|
return app && app.firstElementChild ? app.firstElementChild : app;
|
|
}
|
|
|
|
// Schedule the outgoing slide animation for each region so it finishes before removal.
|
|
function scheduleSlideOutro(holdDelayMs) {
|
|
clearSlideOutroTimer();
|
|
|
|
var currentRoot = getCurrentSlideRoot();
|
|
if (!currentRoot || typeof getRegionAnimationPhaseTimings !== 'function' || typeof playRegionAnimation !== 'function') {
|
|
return;
|
|
}
|
|
|
|
var regionTimings = getRegionAnimationPhaseTimings(currentRoot, 'outro');
|
|
if (!regionTimings.length) {
|
|
return;
|
|
}
|
|
|
|
var slideDurationMs = Math.max(1, Math.round(Number(holdDelayMs || 0)));
|
|
slideOutroTimers = regionTimings.map(function (entry) {
|
|
var timingMs = Math.max(0, Math.round(Number(entry && entry.timingMs || 0)));
|
|
var triggerDelayMs = Math.max(0, slideDurationMs - timingMs);
|
|
return window.setTimeout(function () {
|
|
if (!entry || !entry.element || !entry.element.isConnected) {
|
|
return;
|
|
}
|
|
playRegionAnimation(entry.element, 'outro');
|
|
}, triggerDelayMs);
|
|
});
|
|
}
|
|
|
|
// Schedule the next slide transition.
|
|
function scheduleSlideAdvance(delayMs) {
|
|
clearSlideTimer();
|
|
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
|
slideExpiresAt = Date.now() + holdDelayMs;
|
|
scheduleSlideOutro(holdDelayMs);
|
|
timer = window.setTimeout(function () {
|
|
timer = null;
|
|
slideExpiresAt = null;
|
|
pausedRemainingMs = null;
|
|
clearSlideOutroTimer();
|
|
applyPendingPlaylistUpdate();
|
|
const activeSlides = getCurrentActiveSlides();
|
|
if (activeSlides.length < 2) {
|
|
showCurrent();
|
|
return;
|
|
}
|
|
if (index >= activeSlides.length) {
|
|
index = 0;
|
|
}
|
|
index = (index + 1) % activeSlides.length;
|
|
showCurrent();
|
|
}, holdDelayMs);
|
|
}
|
|
|
|
// Return the slide duration without shifting it for fade timing.
|
|
function getSlideHoldDelay(delayMs) {
|
|
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
|
return holdDelayMs;
|
|
}
|
|
|
|
// Cancel any pending fade-transition cleanup.
|
|
function clearSlideTransitionTimer() {
|
|
if (slideTransitionTimer) {
|
|
window.clearTimeout(slideTransitionTimer);
|
|
slideTransitionTimer = null;
|
|
}
|
|
}
|
|
|
|
function getPlayerRegionModules() {
|
|
if (!window.pulsePlayerRegionTypes || typeof window.pulsePlayerRegionTypes.list !== 'function') {
|
|
return [];
|
|
}
|
|
|
|
return window.pulsePlayerRegionTypes.list();
|
|
}
|
|
|
|
function isThumbnailPreview() {
|
|
return Boolean(window.__pulseThumbnailPreview);
|
|
}
|
|
|
|
function runRegionLifecycle(root, lifecycleName) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
if (isThumbnailPreview() && lifecycleName === 'initRegion') {
|
|
return;
|
|
}
|
|
|
|
getPlayerRegionModules().forEach(function (entry) {
|
|
var module = entry && entry.definition ? entry.definition : null;
|
|
if (!module || typeof module[lifecycleName] !== 'function') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
module[lifecycleName](root);
|
|
} catch (_error) {
|
|
// Keep slide rendering resilient if a region lifecycle hook fails.
|
|
}
|
|
});
|
|
}
|
|
|
|
function destroyRegionInstances(root) {
|
|
runRegionLifecycle(root, 'destroyRegion');
|
|
}
|
|
|
|
function initializeRegionInstances(root) {
|
|
runRegionLifecycle(root, 'initRegion');
|
|
}
|
|
|
|
// Swap slide markup with optional fade animation.
|
|
function renderSlideMarkup(markup, shouldFade) {
|
|
clearSlideTransitionTimer();
|
|
destroyRegionInstances(app);
|
|
if (typeof destroyRtmpRegions === 'function') {
|
|
destroyRtmpRegions(app);
|
|
}
|
|
|
|
function initializeRenderedVideoPlayback(root, delayMs) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
var startDelayMs = Math.max(0, Number(delayMs || 0));
|
|
var videos = root.querySelectorAll('.template-region.video video');
|
|
Array.prototype.forEach.call(videos, function (video) {
|
|
if (!video) {
|
|
return;
|
|
}
|
|
|
|
var playbackScheduled = false;
|
|
|
|
video.autoplay = true;
|
|
video.loop = true;
|
|
video.muted = !(video.dataset && video.dataset.disableAudio === '0');
|
|
video.playsInline = true;
|
|
|
|
function startPlayback() {
|
|
var playPromise = video.play && video.play();
|
|
if (playPromise && typeof playPromise.catch === 'function') {
|
|
playPromise.catch(function () {
|
|
return null;
|
|
});
|
|
}
|
|
}
|
|
|
|
function schedulePlaybackStart() {
|
|
if (playbackScheduled) {
|
|
return;
|
|
}
|
|
playbackScheduled = true;
|
|
if (startDelayMs > 0) {
|
|
window.setTimeout(startPlayback, startDelayMs);
|
|
return;
|
|
}
|
|
startPlayback();
|
|
}
|
|
|
|
if (video.readyState >= 2) {
|
|
schedulePlaybackStart();
|
|
return;
|
|
}
|
|
|
|
video.addEventListener('canplay', schedulePlaybackStart, { once: true });
|
|
video.addEventListener('loadedmetadata', function () {
|
|
if (video.readyState >= 2) {
|
|
schedulePlaybackStart();
|
|
}
|
|
}, { once: true });
|
|
});
|
|
}
|
|
|
|
function schedulePostRenderSetup(root, delayMs) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
if (root.isConnected === false) {
|
|
return;
|
|
}
|
|
|
|
if (typeof syncRtmpRegions === 'function') {
|
|
syncRtmpRegions(root);
|
|
}
|
|
|
|
if (!isThumbnailPreview()) {
|
|
initializeRegionInstances(root);
|
|
}
|
|
|
|
initializeRenderedVideoPlayback(root, delayMs);
|
|
|
|
if (typeof playRegionAnimations === 'function') {
|
|
playRegionAnimations(root, 'intro');
|
|
}
|
|
}
|
|
|
|
if (!shouldFade) {
|
|
app.innerHTML = markup;
|
|
schedulePostRenderSetup(app, 0);
|
|
return app.firstElementChild;
|
|
}
|
|
|
|
var topLevelChildren = Array.prototype.slice.call(app.children || []);
|
|
var existingShells = topLevelChildren.filter(function (child) {
|
|
return child && child.classList && child.classList.contains('slide-shell');
|
|
});
|
|
var previousShell = existingShells.length ? existingShells[existingShells.length - 1] : app.firstElementChild;
|
|
|
|
if (existingShells.length > 1) {
|
|
existingShells.slice(0, -1).forEach(function (shell) {
|
|
if (shell && shell.parentNode) {
|
|
shell.parentNode.removeChild(shell);
|
|
}
|
|
});
|
|
}
|
|
|
|
function pauseRenderedVideoPlayback(root, delayMs) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
var pauseDelayMs = Math.max(0, Number(delayMs || 0));
|
|
var videos = root.querySelectorAll('.template-region.video video, .slide-media video');
|
|
Array.prototype.forEach.call(videos, function (video) {
|
|
if (!video || typeof video.pause !== 'function') {
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(function () {
|
|
try {
|
|
video.pause();
|
|
} catch (_error) {
|
|
// Ignore pause errors from detached or unsupported media elements.
|
|
}
|
|
}, pauseDelayMs);
|
|
});
|
|
}
|
|
|
|
var nextShell = document.createElement('div');
|
|
nextShell.className = 'slide-shell';
|
|
nextShell.style.opacity = '0';
|
|
nextShell.innerHTML = markup;
|
|
|
|
if (!previousShell || (previousShell.classList && previousShell.classList.contains('empty'))) {
|
|
app.innerHTML = '';
|
|
nextShell.style.opacity = '1';
|
|
app.appendChild(nextShell);
|
|
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
|
|
return nextShell;
|
|
}
|
|
|
|
if (!previousShell.classList.contains('slide-shell')) {
|
|
previousShell.classList.add('slide-shell');
|
|
}
|
|
previousShell.style.opacity = '1';
|
|
pauseRenderedVideoPlayback(previousShell, slideFadeDurationMs / 2);
|
|
|
|
app.appendChild(nextShell);
|
|
window.requestAnimationFrame(function () {
|
|
nextShell.style.opacity = '1';
|
|
previousShell.style.opacity = '0';
|
|
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
|
|
});
|
|
|
|
slideTransitionTimer = window.setTimeout(function () {
|
|
if (previousShell && previousShell.parentNode) {
|
|
previousShell.parentNode.removeChild(previousShell);
|
|
}
|
|
if (nextShell) {
|
|
nextShell.style.opacity = '1';
|
|
}
|
|
slideTransitionTimer = null;
|
|
}, slideFadeDurationMs);
|
|
|
|
return nextShell;
|
|
}
|
|
|
|
// Mirror blackout state onto the document body.
|
|
function syncBlackoutState() {
|
|
document.body.classList.toggle('screen-blackout', isBlackout);
|
|
}
|
|
|
|
// Apply pause state and preserve remaining slide time.
|
|
function setPaused(nextPaused) {
|
|
var normalized = Boolean(nextPaused);
|
|
if (isPaused === normalized) {
|
|
return;
|
|
}
|
|
if (normalized) {
|
|
pausedRemainingMs = slideExpiresAt ? Math.max(0, slideExpiresAt - Date.now()) : null;
|
|
isPaused = true;
|
|
clearSlideTimer();
|
|
sendCommandState(lastRenderedSlide);
|
|
return;
|
|
}
|
|
|
|
isPaused = false;
|
|
sendCommandState(lastRenderedSlide);
|
|
if (!slides.length || !lastRenderedSlide) {
|
|
return;
|
|
}
|
|
if (pausedRemainingMs !== null) {
|
|
scheduleSlideAdvance(pausedRemainingMs);
|
|
pausedRemainingMs = null;
|
|
}
|
|
}
|
|
|
|
// Apply blackout state and notify the server.
|
|
function setBlackout(nextBlackout) {
|
|
var normalized = Boolean(nextBlackout);
|
|
if (isBlackout === normalized) {
|
|
return;
|
|
}
|
|
isBlackout = normalized;
|
|
syncBlackoutState();
|
|
sendCommandState(lastRenderedSlide);
|
|
}
|
|
|
|
// Coerce command payload values into booleans or null.
|
|
function normalizeBoolean(value) {
|
|
if (value === true || value === false) {
|
|
return value;
|
|
}
|
|
if (value === null || value === undefined) {
|
|
return null;
|
|
}
|
|
var normalized = String(value).trim().toLowerCase();
|
|
if (['1', 'true', 'yes', 'on'].indexOf(normalized) !== -1) {
|
|
return true;
|
|
}
|
|
if (['0', 'false', 'no', 'off', ''].indexOf(normalized) !== -1) {
|
|
return false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Move to the previous or next active slide.
|
|
function navigateSlides(offset) {
|
|
const manualSlides = getCurrentActiveSlides();
|
|
if (!manualSlides.length) {
|
|
return;
|
|
}
|
|
let currentIndex = manualSlides.findIndex(function (slide) {
|
|
return slide && lastRenderedSlide && slide.id === lastRenderedSlide.id;
|
|
});
|
|
if (currentIndex < 0) {
|
|
currentIndex = Math.min(Math.max(index, 0), manualSlides.length - 1);
|
|
}
|
|
const nextIndex = (currentIndex + offset + manualSlides.length) % manualSlides.length;
|
|
clearSlideTimer();
|
|
applyPendingPlaylistUpdate();
|
|
renderSlideAtIndex(manualSlides, nextIndex);
|
|
}
|
|
|
|
// Route incoming websocket command messages.
|
|
function handleCommandMessage(rawMessage) {
|
|
var payload;
|
|
try {
|
|
payload = JSON.parse(String(rawMessage || ''));
|
|
} catch (_error) {
|
|
return;
|
|
}
|
|
|
|
if (!payload || payload.type !== 'command') {
|
|
return;
|
|
}
|
|
|
|
switch (payload.command) {
|
|
case 'refresh':
|
|
refresh();
|
|
return;
|
|
case 'setclientname':
|
|
if (payload.clientName) {
|
|
applyOnboardingClientName(payload.clientName, commandSocket);
|
|
}
|
|
return;
|
|
case 'redirect':
|
|
if (payload.url) {
|
|
window.location.replace(String(payload.url));
|
|
}
|
|
return;
|
|
case 'pause':
|
|
var desiredPause = normalizeBoolean(payload.paused);
|
|
if (desiredPause !== null) {
|
|
setPaused(desiredPause);
|
|
} else {
|
|
setPaused(!isPaused);
|
|
}
|
|
return;
|
|
case 'blackout':
|
|
var desiredBlackout = normalizeBoolean(payload.blackout);
|
|
if (desiredBlackout !== null) {
|
|
setBlackout(desiredBlackout);
|
|
} else {
|
|
setBlackout(!isBlackout);
|
|
}
|
|
return;
|
|
case 'previous':
|
|
navigateSlides(-1);
|
|
return;
|
|
case 'next':
|
|
navigateSlides(1);
|
|
return;
|
|
case 'reload':
|
|
window.location.reload();
|
|
return;
|
|
case 'announcement-refresh':
|
|
if (typeof refreshAnnouncementOverlay === 'function') {
|
|
refreshAnnouncementOverlay();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Retry the command websocket after a disconnect.
|
|
function scheduleCommandReconnect() {
|
|
if (commandReconnectTimer) {
|
|
return;
|
|
}
|
|
commandReconnectTimer = window.setTimeout(function () {
|
|
commandReconnectTimer = null;
|
|
connectCommandSocket();
|
|
}, 5000);
|
|
}
|
|
|
|
// Open and wire the command websocket connection.
|
|
function connectCommandSocket() {
|
|
if (!window.WebSocket) {
|
|
return;
|
|
}
|
|
if (commandSocket && (commandSocket.readyState === WebSocket.OPEN || commandSocket.readyState === WebSocket.CONNECTING)) {
|
|
return;
|
|
}
|
|
var socket = new WebSocket(new URL(commandSocketPath, window.location.origin).toString());
|
|
commandSocket = socket;
|
|
|
|
socket.onopen = function () {
|
|
if (typeof syncOnboardingClientNameFromServer === 'function') {
|
|
syncOnboardingClientNameFromServer(socket).then(function () {
|
|
sendCommandState(socket);
|
|
});
|
|
return;
|
|
}
|
|
sendCommandState(socket);
|
|
};
|
|
|
|
socket.onmessage = function (event) {
|
|
handleCommandMessage(event.data);
|
|
};
|
|
|
|
socket.onclose = function () {
|
|
commandSocket = null;
|
|
scheduleCommandReconnect();
|
|
};
|
|
|
|
socket.onerror = function () {
|
|
try {
|
|
socket.close();
|
|
} catch (_error) {
|
|
// ignore socket close errors
|
|
}
|
|
};
|
|
}
|