Files
pulse-signage/src/player/public/js/player-page-commands.js
T
lzstealth 3f4f57020a
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s
Release v2.10.3
2026-08-29 01:56:45 +01:00

693 lines
19 KiB
JavaScript

// Capture the current viewport dimensions.
function getCurrentViewport() {
return {
width: window.innerWidth,
height: window.innerHeight
};
}
var slideOutroTimers = [];
var commandHeartbeatTimer = null;
// 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 runRegionLifecycle(root, lifecycleName) {
if (!root) {
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);
}
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 slide-shell-entering';
nextShell.style.zIndex = '0';
nextShell.style.opacity = '0';
nextShell.innerHTML = markup;
if (!previousShell || (previousShell.classList && previousShell.classList.contains('empty'))) {
app.innerHTML = '';
nextShell.style.opacity = '1';
nextShell.classList.remove('slide-shell-entering');
nextShell.classList.add('is-visible');
app.appendChild(nextShell);
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
return nextShell;
}
if (!previousShell.classList.contains('slide-shell')) {
previousShell.classList.add('slide-shell');
}
previousShell.classList.remove('is-visible');
previousShell.classList.add('is-exiting');
previousShell.style.zIndex = '1';
previousShell.style.opacity = '1';
pauseRenderedVideoPlayback(previousShell, slideFadeDurationMs / 2);
app.appendChild(nextShell);
window.requestAnimationFrame(function () {
nextShell.style.opacity = '1';
previousShell.style.opacity = '0';
nextShell.classList.remove('slide-shell-entering');
nextShell.classList.add('is-visible');
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
});
slideTransitionTimer = window.setTimeout(function () {
if (previousShell && previousShell.parentNode) {
previousShell.parentNode.removeChild(previousShell);
}
if (nextShell) {
nextShell.style.zIndex = '1';
nextShell.style.opacity = '1';
nextShell.classList.remove('slide-shell-entering');
nextShell.classList.add('is-visible');
}
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;
}
function isEditableTarget(target) {
if (!target) {
return false;
}
if (target.isContentEditable) {
return true;
}
var tagName = String(target.tagName || '').toUpperCase();
return ['INPUT', 'TEXTAREA', 'SELECT', 'OPTION'].indexOf(tagName) !== -1;
}
var keyboardFeedbackTimer = null;
function showKeyboardFeedback(message) {
if (typeof document === 'undefined' || !document.body) {
return;
}
var feedback = document.querySelector('.player-keyboard-feedback');
if (!feedback) {
feedback = document.createElement('div');
feedback.className = 'player-keyboard-feedback';
feedback.setAttribute('aria-live', 'polite');
document.body.appendChild(feedback);
}
feedback.textContent = String(message || '');
feedback.classList.remove('is-visible');
void feedback.offsetWidth;
feedback.classList.add('is-visible');
if (keyboardFeedbackTimer) {
window.clearTimeout(keyboardFeedbackTimer);
}
keyboardFeedbackTimer = window.setTimeout(function () {
feedback.classList.remove('is-visible');
keyboardFeedbackTimer = null;
}, 900);
}
function handlePlayerKeydown(event) {
if (!event || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) {
return;
}
if (isEditableTarget(event.target)) {
return;
}
if (event.key === 'ArrowLeft') {
event.preventDefault();
navigateSlides(-1);
showKeyboardFeedback('Previous slide');
return;
}
if (event.key === 'ArrowRight') {
event.preventDefault();
navigateSlides(1);
showKeyboardFeedback('Next slide');
return;
}
if (String(event.key || '').toLowerCase() === 'p') {
event.preventDefault();
setPaused(!isPaused);
showKeyboardFeedback(isPaused ? 'Paused' : 'Playing');
return;
}
if (String(event.key || '').toLowerCase() === 'b') {
event.preventDefault();
setBlackout(!isBlackout);
showKeyboardFeedback(isBlackout ? 'Blackout on' : 'Blackout off');
}
}
window.addEventListener('keydown', handlePlayerKeydown);
// 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 === 'client-id-conflict') {
handleClientIdConflict();
return;
}
if (payload && payload.type === 'client-name-updated') {
if (payload.clientName) {
applyOnboardingClientName(payload.clientName, commandSocket);
}
return;
}
if (!payload || payload.type !== 'command') {
return;
}
if (payload.requestId && commandSocket && commandSocket.readyState === WebSocket.OPEN) {
commandSocket.send(JSON.stringify({
type: 'command-ack',
requestId: payload.requestId,
ok: true
}));
}
switch (payload.command) {
case 'refresh':
refresh(true);
return;
case 'setclientname':
if (payload.clientName) {
applyOnboardingClientName(payload.clientName, commandSocket);
}
return;
case 'redirect':
if (payload.url) {
var redirectUrl = String(payload.url);
var authorizeMove = payload.moveToken
? fetch('/api/screen-move-authorize', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ moveToken: String(payload.moveToken) })
})
: Promise.resolve();
authorizeMove.finally(function () {
window.location.replace(redirectUrl);
});
}
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;
}
}
function handleClientIdConflict() {
var replacementClientId = regenerateCommandClientId();
var onboardingUrl = new URL('/', window.location.origin);
window.location.replace(onboardingUrl.toString());
}
// 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 (commandHeartbeatTimer) {
window.clearInterval(commandHeartbeatTimer);
}
commandHeartbeatTimer = window.setInterval(function () {
sendCommandState(lastRenderedSlide);
}, 60 * 1000);
if (typeof syncOnboardingClientNameFromServer === 'function') {
syncOnboardingClientNameFromServer(socket).then(function () {
sendCommandState(socket);
});
return;
}
sendCommandState(socket);
};
socket.onmessage = function (event) {
handleCommandMessage(event.data);
};
socket.onclose = function (event) {
if (typeof logDebug === 'function') {
logDebug('Command websocket closed.', 'code=' + String(event && event.code || '') + ' reason=' + String(event && event.reason || ''), 'warn');
}
if (commandHeartbeatTimer) {
window.clearInterval(commandHeartbeatTimer);
commandHeartbeatTimer = null;
}
commandSocket = null;
if (event && event.code === 4009) {
handleClientIdConflict();
return;
}
scheduleCommandReconnect();
};
socket.onerror = function (error) {
if (typeof logDebug === 'function') {
logDebug('Command websocket error.', error && error.message ? String(error.message) : 'Websocket transport error.', 'error');
}
try {
socket.close();
} catch (_error) {
// ignore socket close errors
}
};
}