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