361 lines
9.6 KiB
JavaScript
361 lines
9.6 KiB
JavaScript
// Capture the current viewport dimensions.
|
|
function getCurrentViewport() {
|
|
return {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// Schedule the next slide transition.
|
|
function scheduleSlideAdvance(delayMs) {
|
|
clearSlideTimer();
|
|
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
|
slideExpiresAt = Date.now() + holdDelayMs;
|
|
timer = window.setTimeout(function () {
|
|
timer = null;
|
|
slideExpiresAt = null;
|
|
pausedRemainingMs = null;
|
|
const activeSlides = getCurrentActiveSlides();
|
|
if (activeSlides.length < 2) {
|
|
refresh();
|
|
return;
|
|
}
|
|
if (index >= activeSlides.length) {
|
|
index = 0;
|
|
}
|
|
index = (index + 1) % activeSlides.length;
|
|
showCurrent();
|
|
}, holdDelayMs);
|
|
}
|
|
|
|
// Add the fade time to a slide's hold duration so the configured duration remains visible.
|
|
function getSlideHoldDelay(delayMs) {
|
|
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
|
return holdDelayMs + (currentPlaylistFadeBetweenSlides ? slideFadeDurationMs : 0);
|
|
}
|
|
|
|
// Cancel any pending fade-transition cleanup.
|
|
function clearSlideTransitionTimer() {
|
|
if (slideTransitionTimer) {
|
|
window.clearTimeout(slideTransitionTimer);
|
|
slideTransitionTimer = null;
|
|
}
|
|
}
|
|
|
|
// Swap slide markup with optional fade animation.
|
|
function renderSlideMarkup(markup, shouldFade) {
|
|
clearSlideTransitionTimer();
|
|
if (typeof destroyRtmpRegions === 'function') {
|
|
destroyRtmpRegions(app);
|
|
}
|
|
if (!shouldFade) {
|
|
app.innerHTML = markup;
|
|
if (typeof syncRtmpRegions === 'function') {
|
|
syncRtmpRegions(app);
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
if (typeof syncRtmpRegions === 'function') {
|
|
syncRtmpRegions(nextShell);
|
|
}
|
|
return nextShell;
|
|
}
|
|
|
|
if (!previousShell.classList.contains('slide-shell')) {
|
|
previousShell.classList.add('slide-shell');
|
|
}
|
|
previousShell.style.opacity = '1';
|
|
|
|
app.appendChild(nextShell);
|
|
void nextShell.offsetHeight;
|
|
window.requestAnimationFrame(function () {
|
|
nextShell.style.opacity = '1';
|
|
previousShell.style.opacity = '0';
|
|
});
|
|
|
|
if (typeof syncRtmpRegions === 'function') {
|
|
syncRtmpRegions(nextShell);
|
|
}
|
|
|
|
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':
|
|
setPaused(!isPaused);
|
|
return;
|
|
case 'blackout':
|
|
var desiredBlackout = normalizeBoolean(payload.blackout);
|
|
if (desiredBlackout !== null) {
|
|
setBlackout(desiredBlackout);
|
|
} else {
|
|
setBlackout(!isBlackout);
|
|
}
|
|
return;
|
|
case 'previous':
|
|
case 'left':
|
|
navigateSlides(-1);
|
|
return;
|
|
case 'next':
|
|
case 'right':
|
|
navigateSlides(1);
|
|
return;
|
|
case 'reload':
|
|
window.location.reload();
|
|
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 protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
var socketUrl = new URL(commandSocketPath, window.location.origin);
|
|
if (window.__pulsePageAuthToken) {
|
|
socketUrl.searchParams.set('auth', window.__pulsePageAuthToken);
|
|
}
|
|
var socket = new WebSocket(socketUrl.toString());
|
|
commandSocket = socket;
|
|
|
|
socket.onopen = function () {
|
|
if (typeof syncOnboardingClientNameFromServer === 'function') {
|
|
syncOnboardingClientNameFromServer(socket).then(function () {
|
|
sendCommandHello(socket);
|
|
});
|
|
return;
|
|
}
|
|
sendCommandHello(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
|
|
}
|
|
};
|
|
}
|