// 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 } }; }