Release v2.10.3
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

This commit is contained in:
lzstealth committed 2026-08-29 01:56:45 +01:00
1 parent 30814f3f46
commit 3f4f57020a
60 files changed
+1075 -523

No files matched your search

+46 -15
View File
@@ -7,6 +7,7 @@ function getCurrentViewport() {
}
var slideOutroTimers = [];
var commandHeartbeatTimer = null;
// Command websocket and player-state helpers.
// Send the current playback state to the command websocket.
@@ -166,19 +167,11 @@ function getPlayerRegionModules() {
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') {
@@ -276,9 +269,7 @@ function renderSlideMarkup(markup, shouldFade) {
syncRtmpRegions(root);
}
if (!isThumbnailPreview()) {
initializeRegionInstances(root);
}
initializeRegionInstances(root);
initializeRenderedVideoPlayback(root, delayMs);
@@ -330,13 +321,16 @@ function renderSlideMarkup(markup, shouldFade) {
}
var nextShell = document.createElement('div');
nextShell.className = 'slide-shell';
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;
@@ -345,6 +339,9 @@ function renderSlideMarkup(markup, shouldFade) {
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);
@@ -352,6 +349,8 @@ function renderSlideMarkup(markup, shouldFade) {
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);
});
@@ -360,7 +359,10 @@ function renderSlideMarkup(markup, shouldFade) {
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);
@@ -537,14 +539,28 @@ function handleCommandMessage(rawMessage) {
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();
refresh(true);
return;
case 'setclientname':
if (payload.clientName) {
@@ -602,7 +618,6 @@ function handleCommandMessage(rawMessage) {
function handleClientIdConflict() {
var replacementClientId = regenerateCommandClientId();
var onboardingUrl = new URL('/', window.location.origin);
onboardingUrl.searchParams.set('clientId', replacementClientId);
window.location.replace(onboardingUrl.toString());
}
@@ -629,6 +644,12 @@ function connectCommandSocket() {
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);
@@ -643,6 +664,13 @@ function connectCommandSocket() {
};
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();
@@ -651,7 +679,10 @@ function connectCommandSocket() {
scheduleCommandReconnect();
};
socket.onerror = function () {
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) {
@@ -1,8 +1,5 @@
// Show or hide the offline status banner.
function setOfflineBannerVisible(visible, message) {
if (window.__pulseThumbnailPreview) {
return;
}
var normalizedVisible = Boolean(visible);
var bannerMessage = String(message || 'Offline mode: using cached playlist.').trim();
if (normalizedVisible) {
@@ -41,9 +38,6 @@ function setOfflineBannerVisible(visible, message) {
// Update the offline banner based on connectivity or playlist availability.
function syncOfflineBanner() {
if (window.__pulseThumbnailPreview) {
return;
}
if (!window.navigator.onLine) {
setOfflineBannerVisible(true, 'Offline mode: using cached playlist.');
return;
@@ -63,9 +57,6 @@ function clearRefreshRetry() {
// Retry playlist refresh with a short backoff while the player is offline.
function scheduleRefreshRetry() {
if (window.__pulseThumbnailPreview) {
return;
}
if (refreshRetryTimer) {
return;
}
+17 -5
View File
@@ -1,5 +1,5 @@
// Render the slide at the requested index within the active set.
async function renderSlideAtIndex(sourceSlides, targetIndex) {
async function renderSlideAtIndex(sourceSlides, targetIndex, options) {
const availableSlides = Array.isArray(sourceSlides) ? sourceSlides : [];
if (!availableSlides.length) {
renderEmpty(slides.length ? 'No slides are scheduled for this time.' : 'No slides assigned to this screen yet.');
@@ -53,7 +53,7 @@ async function renderSlideAtIndex(sourceSlides, targetIndex) {
index = currentIndex;
var markup = buildSlideMarkup(slide);
renderSlideMarkup(markup, currentPlaylistFadeBetweenSlides);
renderSlideMarkup(markup, !(options && options.skipFade) && currentPlaylistFadeBetweenSlides);
if (typeof scheduleSlideMarkupPreload === 'function') {
scheduleSlideMarkupPreload(availableSlides, currentIndex);
}
@@ -91,7 +91,7 @@ function applyPendingPlaylistUpdate() {
}
// Render the current active slide or the empty state.
function showCurrent() {
function showCurrent(options) {
clearSlideTimer();
const activeSlides = getCurrentActiveSlides();
syncWebpagePreloads(activeSlides, index);
@@ -106,7 +106,7 @@ function showCurrent() {
lastRenderedViewKey = getCurrentRenderKey(activeSlides);
return;
}
void renderSlideAtIndex(activeSlides, index);
void renderSlideAtIndex(activeSlides, index, options);
lastRenderedViewKey = getCurrentRenderKey(activeSlides);
}
@@ -148,7 +148,7 @@ function handleRtmpPlaybackFailure(message, options) {
}
// Fetch the latest playlist and queue any updates.
function refresh() {
function refresh(applyImmediately) {
var request = new XMLHttpRequest();
var url = window.location.origin + '/api/screens/' + encodeURIComponent(slug) + '/playlist?ts=' + Date.now();
request.open('GET', url, true);
@@ -167,6 +167,7 @@ function refresh() {
return;
}
if (request.status === 304) {
logDebug('Playlist refresh completed with no changes.');
markRefreshHealthy();
setOfflineBannerVisible(false);
if (lastRenderedSlide && getCurrentActiveSlides().length < 2) {
@@ -192,6 +193,7 @@ function refresh() {
const responseEtag = String(request.getResponseHeader('ETag') || '').trim();
const data = JSON.parse(request.responseText || '{}');
const nextSignature = getPlaylistRevision(data);
logDebug('Playlist refresh completed.', 'Revision: ' + nextSignature);
const nextSlides = Array.isArray(data.slides) ? data.slides.map(normalizeSlide) : [];
const nextActiveSlides = getActiveSlidesFrom(nextSlides);
const nextFadeBetweenSlides = Boolean(data && data.playlist && data.playlist.fade_between_slides);
@@ -254,6 +256,11 @@ function refresh() {
fadeBetweenSlides: nextFadeBetweenSlides,
skipUnavailableRtmp: nextSkipUnavailableRtmp
};
if (applyImmediately) {
applyPendingPlaylistUpdate();
showCurrent();
return;
}
if (!isSlideInList(lastRenderedSlide, nextSlides)) {
applyPendingPlaylistUpdate();
showCurrent();
@@ -268,6 +275,11 @@ function refresh() {
fadeBetweenSlides: nextFadeBetweenSlides,
skipUnavailableRtmp: nextSkipUnavailableRtmp
};
if (applyImmediately) {
applyPendingPlaylistUpdate();
showCurrent();
return;
}
logDebug('Playlist update detected; applying on next slide transition.');
} catch (_error) {
logDebug(
+1 -4
View File
@@ -129,10 +129,6 @@ function getSlideMarkupPreloadSlides(sourceSlides, targetIndex) {
}
function scheduleSlideMarkupPreload(sourceSlides, targetIndex) {
if (window.__pulseThumbnailPreview) {
return;
}
const preloadSlides = getSlideMarkupPreloadSlides(sourceSlides, targetIndex);
if (!preloadSlides.length || typeof primeSlideMarkup !== 'function') {
return;
@@ -236,6 +232,7 @@ function loadPlaylistSnapshot() {
slides: parsed.slides.map(normalizeSlide),
signature: String(parsed.signature || ''),
fadeBetweenSlides: Boolean(parsed.fadeBetweenSlides),
skipUnavailableRtmp: Boolean(parsed.skipUnavailableRtmp),
etag: String(parsed.etag || '')
};
} catch (_error) {
@@ -227,7 +227,7 @@ function getRegionAnimationPhaseTimingMs(root, phase) {
}
function getRegionAnimationPhaseTimings(root, phase) {
if (!root || isThumbnailPreview()) {
if (!root) {
return [];
}
@@ -266,7 +266,7 @@ function getRegionAnimationPhaseTimings(root, phase) {
}
function playRegionAnimation(element, phase) {
if (!element || isThumbnailPreview()) {
if (!element) {
return;
}
@@ -316,7 +316,7 @@ function playRegionAnimation(element, phase) {
}
function playRegionAnimations(root, phase) {
if (!root || isThumbnailPreview()) {
if (!root) {
return;
}
@@ -797,7 +797,7 @@ function buildBackdropStyle(backgroundColor, backgroundImagePath, backgroundGrad
var gradient = '';
try {
var gradientData = typeof backgroundGradient === 'string' ? JSON.parse(backgroundGradient) : backgroundGradient;
if (gradientData && gradientData.type === 'linear' && Array.isArray(gradientData.colors) && gradientData.colors.length >= 2) {
if (gradientData && gradientData.type === 'linear' && ((Array.isArray(gradientData.stops) && gradientData.stops.length >= 2) || (Array.isArray(gradientData.colors) && gradientData.colors.length >= 2))) {
var stops = Array.isArray(gradientData.stops) ? gradientData.stops : (gradientData.colors || []).map(function (color, index, colors) { return { color: color, position: colors.length > 1 ? Math.round(index * 100 / (colors.length - 1)) : 0 }; });
stops = stops.filter(function (stop) { return stop && /^#[0-9a-fA-F]{3,8}$/.test(String(stop.color || '').trim()); }).slice(0, 12);
if (stops.length >= 2) {
@@ -975,7 +975,7 @@ function notifyVideoRegionSourceReady() {
}
slideMarkupCache = Object.create(null);
if (typeof showCurrent === 'function' && slides && slides.length) {
showCurrent();
showCurrent({ skipFade: true });
}
}
@@ -0,0 +1,34 @@
// Registry used by the player runtime to discover independently loaded region modules.
(function () {
var root = window;
var registry = root.pulsePlayerRegionTypes && typeof root.pulsePlayerRegionTypes === 'object' ? root.pulsePlayerRegionTypes : {};
function normalizeType(type) {
return String(type || '').trim().toLowerCase();
}
function register(type, definition) {
registry[normalizeType(type)] = definition || {};
return registry[normalizeType(type)];
}
function get(type) {
return registry[normalizeType(type)] || null;
}
function list() {
return Object.keys(registry).map(function (type) {
return {
type: type,
definition: registry[type] || {}
};
});
}
root.pulsePlayerRegionTypes = {
register: register,
get: get,
list: list
};
}());