Release v2.10.6
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m15s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s

This commit is contained in:
lzstealth committed 2026-08-29 14:59:06 +01:00
1 parent ca9f38f3b9
commit 8c0c22156e
30 files changed
+922 -531

No files matched your search

@@ -0,0 +1,253 @@
// Region animation configuration and lifecycle helpers.
function normalizePlayerAnimationConfig(value) {
var raw = value;
if (typeof raw === 'string') {
var text = String(raw || '').trim();
if (!text) {
raw = null;
} else {
try {
raw = JSON.parse(text);
} catch (_error) {
raw = null;
}
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
raw = {};
}
return {
intro: normalizePlayerAnimationStep(raw.intro, 'none'),
outro: normalizePlayerAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'),
loop: normalizePlayerAnimationStep(raw.loop, 'none')
};
}
function normalizePlayerAnimationStep(value, fallbackPreset) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return {
preset: String(value.preset || fallbackPreset || 'none').trim(),
duration_ms: Number.isFinite(Number(value.duration_ms)) && Number(value.duration_ms) > 0 ? Math.round(Number(value.duration_ms)) : null,
delay_ms: Number.isFinite(Number(value.delay_ms)) && Number(value.delay_ms) >= 0 ? Math.round(Number(value.delay_ms)) : null,
iterations: Number.isFinite(Number(value.iterations)) && Number(value.iterations) > 0 ? Math.round(Number(value.iterations)) : null
};
}
return {
preset: String(typeof value === 'string' ? value : fallbackPreset || 'none').trim(),
duration_ms: null,
delay_ms: null,
iterations: null
};
}
function getAnimationStepTimingMs(step) {
if (!step) {
return 0;
}
var preset = String(step.preset || 'none').trim();
if (!preset || preset === 'none') {
return 0;
}
var durationMs = Number(step.duration_ms);
if (!Number.isFinite(durationMs) || durationMs <= 0) {
durationMs = 1000;
}
var delayMs = Number(step.delay_ms);
if (!Number.isFinite(delayMs) || delayMs < 0) {
delayMs = 0;
}
var iterations = Number(step.iterations);
if (!Number.isFinite(iterations) || iterations < 1) {
iterations = 1;
}
return delayMs + (durationMs * iterations);
}
function getRegionAnimationPhaseTimings(root, phase) {
if (!root) {
return [];
}
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
normalizedPhase = 'intro';
}
var timings = [];
Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) {
if (!element) {
return;
}
var config;
try {
config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}');
} catch (_error) {
config = null;
}
if (!config) {
return;
}
var timingMs = getAnimationStepTimingMs(normalizedPhase === 'outro' ? config.outro : config.intro);
if (timingMs > 0) {
timings.push({
element: element,
timingMs: timingMs
});
}
});
return timings;
}
function getRegionAnimationPhaseTimingMs(root, phase) {
return getRegionAnimationPhaseTimings(root, phase).reduce(function (maxTimingMs, entry) {
return Math.max(maxTimingMs, Number(entry && entry.timingMs || 0));
}, 0);
}
function clearRegionAnimationClasses(root) {
if (!root) {
return;
}
var elements = [];
if (typeof root.matches === 'function' && root.matches('[data-animation-json]')) {
elements.push(root);
}
Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) {
elements.push(element);
});
elements.forEach(function (element) {
if (!element) {
return;
}
element.classList.remove('animate__animated', 'animate__infinite');
element.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3');
Array.prototype.slice.call(element.classList || []).forEach(function (className) {
if (String(className || '').indexOf('animate__') === 0) {
element.classList.remove(className);
}
});
element.style.removeProperty('--animate-duration');
element.style.removeProperty('--animate-delay');
element.style.removeProperty('--animate-repeat');
});
}
function isAttentionSeekerAnimation(preset) {
return ['bounce', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat'].indexOf(String(preset || '').trim()) !== -1;
}
function applyAnimationStep(element, step, phase) {
if (!element || !step) {
return;
}
var preset = String(step.preset || 'none').trim();
if (!preset || preset === 'none') {
return;
}
element.classList.add('animate__animated', 'animate__' + preset);
element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms');
if (Number(step.delay_ms || 0) > 0) {
element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms');
} else {
element.style.removeProperty('--animate-delay');
}
if (phase === 'loop') {
var repeatCount = Number(step.iterations);
if (!Number.isFinite(repeatCount) || repeatCount < 1) {
repeatCount = 1;
}
if (repeatCount > 1) {
element.classList.add('animate__repeat-1');
}
element.style.setProperty('--animate-repeat', String(repeatCount));
return;
}
if (isAttentionSeekerAnimation(preset) && Number(step.iterations || 0) > 1) {
element.style.setProperty('--animate-repeat', String(Math.max(1, Number(step.iterations || 1))));
}
}
function playRegionAnimation(element, phase) {
if (!element) {
return;
}
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
normalizedPhase = 'intro';
}
var config;
try {
config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}');
} catch (_error) {
config = null;
}
if (!config) {
return;
}
element.dataset.animationPhase = normalizedPhase;
clearRegionAnimationClasses(element);
if (normalizedPhase === 'outro') {
applyAnimationStep(element, config.outro, 'outro');
return;
}
if (config.intro && String(config.intro.preset || '').trim() && String(config.intro.preset || '').trim() !== 'none') {
applyAnimationStep(element, config.intro, 'intro');
if (config.loop && String(config.loop.preset || '').trim() && String(config.loop.preset || '').trim() !== 'none') {
element.addEventListener('animationend', function handleAnimationEnd(event) {
if (event.target !== element) {
return;
}
if (String(element.dataset.animationPhase || '').trim() !== 'intro') {
return;
}
element.removeEventListener('animationend', handleAnimationEnd);
clearRegionAnimationClasses(element);
applyAnimationStep(element, config.loop, 'loop');
});
}
return;
}
applyAnimationStep(element, config.loop, 'loop');
}
function playRegionAnimations(root, phase) {
if (!root) {
return;
}
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
normalizedPhase = 'intro';
}
var elements = Array.prototype.slice.call(root.querySelectorAll('[data-animation-json]'));
elements.forEach(function (element) {
playRegionAnimation(element, normalizedPhase);
});
}
+9 -187
View File
@@ -6,7 +6,6 @@ function getCurrentViewport() {
};
}
var slideOutroTimers = [];
var commandHeartbeatTimer = null;
// Command websocket and player-state helpers.
@@ -62,103 +61,6 @@ function scheduleViewportRenderUpdate() {
}, 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 [];
@@ -195,65 +97,11 @@ function initializeRegionInstances(root) {
}
// Swap slide markup with optional fade animation.
function renderSlideMarkup(markup, shouldFade) {
function renderSlideMarkup(markup, shouldFade, mediaDelayMs) {
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 });
});
if (typeof destroySlideMedia === 'function') {
destroySlideMedia(app);
}
function schedulePostRenderSetup(root, delayMs) {
@@ -265,13 +113,10 @@ function renderSlideMarkup(markup, shouldFade) {
return;
}
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(root);
}
initializeRegionInstances(root);
initializeRenderedVideoPlayback(root, delayMs);
if (typeof initializeSlideMedia === 'function') {
initializeSlideMedia(root, delayMs);
}
if (typeof playRegionAnimations === 'function') {
playRegionAnimations(root, 'intro');
@@ -298,28 +143,6 @@ function renderSlideMarkup(markup, shouldFade) {
});
}
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';
@@ -332,7 +155,7 @@ function renderSlideMarkup(markup, shouldFade) {
nextShell.classList.remove('slide-shell-entering');
nextShell.classList.add('is-visible');
app.appendChild(nextShell);
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
schedulePostRenderSetup(nextShell, mediaDelayMs === undefined ? slideFadeOffsetMs : mediaDelayMs);
return nextShell;
}
@@ -343,7 +166,6 @@ function renderSlideMarkup(markup, shouldFade) {
previousShell.classList.add('is-exiting');
previousShell.style.zIndex = '1';
previousShell.style.opacity = '1';
pauseRenderedVideoPlayback(previousShell, slideFadeDurationMs / 2);
app.appendChild(nextShell);
window.requestAnimationFrame(function () {
@@ -351,7 +173,7 @@ function renderSlideMarkup(markup, shouldFade) {
previousShell.style.opacity = '0';
nextShell.classList.remove('slide-shell-entering');
nextShell.classList.add('is-visible');
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
schedulePostRenderSetup(nextShell, mediaDelayMs === undefined ? slideFadeOffsetMs : mediaDelayMs);
});
slideTransitionTimer = window.setTimeout(function () {
@@ -365,7 +187,7 @@ function renderSlideMarkup(markup, shouldFade) {
nextShell.classList.add('is-visible');
}
slideTransitionTimer = null;
}, slideFadeDurationMs);
}, slideFadeLengthMs);
return nextShell;
}
+94
View File
@@ -0,0 +1,94 @@
// Slide media startup and teardown helpers.
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 = false;
video.loop = !(video.dataset && video.dataset.loop === '0');
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 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) {
return null;
}
}, pauseDelayMs);
});
}
function initializeSlideMedia(root, delayMs) {
if (!root || root.isConnected === false) {
return;
}
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(root);
}
initializeRenderedVideoPlayback(root, delayMs);
}
function destroySlideMedia(root) {
if (typeof destroyRtmpRegions === 'function') {
destroyRtmpRegions(root);
}
}
+37 -6
View File
@@ -53,18 +53,34 @@ async function renderSlideAtIndex(sourceSlides, targetIndex, options) {
index = currentIndex;
var markup = buildSlideMarkup(slide);
renderSlideMarkup(markup, !(options && options.skipFade) && currentPlaylistFadeBetweenSlides);
var shouldFade = !(options && options.skipFade) && currentPlaylistFadeBetweenSlides;
var mediaDelayMs = slide && slide.use_video_duration ? 0 : undefined;
renderSlideMarkup(markup, shouldFade, mediaDelayMs);
if (typeof scheduleSlideMarkupPreload === 'function') {
scheduleSlideMarkupPreload(availableSlides, currentIndex);
}
sendCommandState(slide);
if (!isPaused) {
scheduleSlideAdvance(getSlideHoldDelay(Math.max(1, Number(slide.duration_seconds || 10)) * 1000));
scheduleSlideAdvance(getSlideAdvanceDelay(slide));
}
lastRenderedViewKey = getCurrentRenderKey(availableSlides);
return true;
}
// Calculate the configured time from slide entry to the next transition.
function getSlideAdvanceDelay(slide) {
var durationMs = Math.max(1, Number(slide && slide.duration_seconds || 10)) * 1000;
if (slide && slide.use_video_duration) {
return currentPlaylistFadeBetweenSlides
? getSlideHoldDelay(Math.max(1, durationMs - slideFadeLengthMs))
: getSlideHoldDelay(durationMs);
}
if (currentPlaylistFadeBetweenSlides) {
return getSlideHoldDelay(Math.max(1, durationMs - slideFadeOffsetMs));
}
return getSlideHoldDelay(durationMs);
}
// Promote a deferred playlist update at the next safe point.
function applyPendingPlaylistUpdate() {
if (!pendingPlaylistUpdate) {
@@ -167,11 +183,16 @@ function refresh(applyImmediately) {
return;
}
if (request.status === 304) {
if (!initialData || !Array.isArray(initialData.apiSources) || !Array.isArray(initialData.weatherLocations)) {
currentPlaylistEtag = '';
refresh(applyImmediately);
return;
}
logDebug('Playlist refresh completed with no changes.');
markRefreshHealthy();
setOfflineBannerVisible(false);
if (lastRenderedSlide && getCurrentActiveSlides().length < 2) {
scheduleSlideAdvance(getSlideHoldDelay(Math.max(1, Number(lastRenderedSlide.duration_seconds || 10)) * 1000));
scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide));
}
return;
}
@@ -198,6 +219,7 @@ function refresh(applyImmediately) {
const nextActiveSlides = getActiveSlidesFrom(nextSlides);
const nextFadeBetweenSlides = Boolean(data && data.playlist && data.playlist.fade_between_slides);
const nextSkipUnavailableRtmp = Boolean(data && data.playlist && data.playlist.skip_unavailable_rtmp);
const hadSourceData = !(typeof window !== 'undefined' && window.initialData === null);
var refreshedInitialData = Object.assign({},
typeof initialData !== 'undefined' && initialData ? initialData : (window.initialData || {}), {
screen: data.screen || null,
@@ -213,6 +235,11 @@ function refresh(applyImmediately) {
initialData = refreshedInitialData;
}
window.initialData = refreshedInitialData;
if (!hadSourceData) {
slideMarkupCache = Object.create(null);
templateLayoutCache = Object.create(null);
templateRenderPlanCache = Object.create(null);
}
savePlaylistSnapshot({
slides: nextSlides,
signature: nextSignature,
@@ -237,8 +264,12 @@ function refresh(applyImmediately) {
return;
}
if (nextSignature === currentPlaylistSignature || (pendingPlaylistUpdate && nextSignature === pendingPlaylistUpdate.signature)) {
if (!hadSourceData) {
showCurrent({ skipFade: true });
return;
}
if (currentActiveSlides.length < 2 && lastRenderedSlide) {
scheduleSlideAdvance(getSlideHoldDelay(Math.max(1, Number(lastRenderedSlide.duration_seconds || 10)) * 1000));
scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide));
}
return;
}
@@ -300,7 +331,7 @@ function refresh(applyImmediately) {
setOfflineBannerVisible(true);
scheduleRefreshRetry();
if (lastRenderedSlide && getCurrentActiveSlides().length < 2) {
scheduleSlideAdvance(getSlideHoldDelay(Math.max(1, Number(lastRenderedSlide.duration_seconds || 10)) * 1000));
scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide));
}
};
request.ontimeout = function () {
@@ -312,7 +343,7 @@ function refresh(applyImmediately) {
setOfflineBannerVisible(true);
scheduleRefreshRetry();
if (lastRenderedSlide && getCurrentActiveSlides().length < 2) {
scheduleSlideAdvance(getSlideHoldDelay(Math.max(1, Number(lastRenderedSlide.duration_seconds || 10)) * 1000));
scheduleSlideAdvance(getSlideAdvanceDelay(lastRenderedSlide));
}
};
request.send();
+9 -259
View File
@@ -12,12 +12,10 @@ function normalizeStyleAttributeValue(value) {
.replace(/&amp;#39;/g, "'");
}
// Clamp font size to the supported range.
function sanitizeFontSize(value) {
return Math.max(8, Number(value || 0) || 24);
}
// Validate a text color and fall back when needed.
function sanitizeTextColor(value, fallback) {
var raw = String(value || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) {
@@ -26,7 +24,6 @@ function sanitizeTextColor(value, fallback) {
return fallback || '#000000';
}
// Read the template's canvas dimensions with safe defaults.
function getTemplateCanvasSize(template) {
return {
width: Math.max(1, Number(template.canvas_size_width || 1920)),
@@ -34,7 +31,6 @@ function getTemplateCanvasSize(template) {
};
}
// Read the server-supplied playlist revision, or fall back to the ETag.
function getPlaylistRevision(data) {
if (data && data.revision) {
return String(data.revision);
@@ -48,7 +44,6 @@ function getPlaylistRevision(data) {
return String(Date.now());
}
// Scale a canvas to fit within the viewport.
function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) {
var width = Math.max(1, Number(canvasWidth || 0) || 1920);
var height = Math.max(1, Number(canvasHeight || 0) || 1080);
@@ -61,53 +56,10 @@ function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) {
};
}
function normalizePlayerAnimationStep(value, fallbackPreset) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return {
preset: String(value.preset || fallbackPreset || 'none').trim(),
duration_ms: Number.isFinite(Number(value.duration_ms)) && Number(value.duration_ms) > 0 ? Math.round(Number(value.duration_ms)) : null,
delay_ms: Number.isFinite(Number(value.delay_ms)) && Number(value.delay_ms) >= 0 ? Math.round(Number(value.delay_ms)) : null,
iterations: Number.isFinite(Number(value.iterations)) && Number(value.iterations) > 0 ? Math.round(Number(value.iterations)) : null
};
}
return {
preset: String(typeof value === 'string' ? value : fallbackPreset || 'none').trim(),
duration_ms: null,
delay_ms: null,
iterations: null
};
}
function normalizePlayerAnimationConfig(value) {
var raw = value;
if (typeof raw === 'string') {
var text = String(raw || '').trim();
if (!text) {
raw = null;
} else {
try {
raw = JSON.parse(text);
} catch (_error) {
raw = null;
}
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
raw = {};
}
return {
intro: normalizePlayerAnimationStep(raw.intro, 'none'),
outro: normalizePlayerAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'),
loop: normalizePlayerAnimationStep(raw.loop, 'none')
};
}
function hasPlayerAnimation(config) {
return Boolean(config && ['intro', 'outro', 'loop'].some(function (stepName) {
return String((config[stepName] && config[stepName].preset) || '').trim() && String((config[stepName] && config[stepName].preset) || '').trim() !== 'none';
var preset = String((config[stepName] && config[stepName].preset) || '').trim();
return preset && preset !== 'none';
}));
}
@@ -122,215 +74,6 @@ function decorateRegionMarkup(markup, region) {
});
}
function clearRegionAnimationClasses(root) {
if (!root) {
return;
}
var elements = [];
if (typeof root.matches === 'function' && root.matches('[data-animation-json]')) {
elements.push(root);
}
Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) {
elements.push(element);
});
elements.forEach(function (element) {
if (!element) {
return;
}
element.classList.remove('animate__animated', 'animate__infinite');
element.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3');
Array.prototype.slice.call(element.classList || []).forEach(function (className) {
if (String(className || '').indexOf('animate__') === 0) {
element.classList.remove(className);
}
});
element.style.removeProperty('--animate-duration');
element.style.removeProperty('--animate-delay');
element.style.removeProperty('--animate-repeat');
});
}
function isAttentionSeekerAnimation(preset) {
return ['bounce', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat'].indexOf(String(preset || '').trim()) !== -1;
}
function applyAnimationStep(element, step, phase) {
if (!element || !step) {
return;
}
var preset = String(step.preset || 'none').trim();
if (!preset || preset === 'none') {
return;
}
element.classList.add('animate__animated', 'animate__' + preset);
element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms');
if (Number(step.delay_ms || 0) > 0) {
element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms');
} else {
element.style.removeProperty('--animate-delay');
}
if (phase === 'loop') {
var repeatCount = Number(step.iterations);
if (!Number.isFinite(repeatCount) || repeatCount < 1) {
repeatCount = 1;
}
if (repeatCount > 1) {
element.classList.add('animate__repeat-1');
}
element.style.setProperty('--animate-repeat', String(repeatCount));
return;
}
if (isAttentionSeekerAnimation(preset) && Number(step.iterations || 0) > 1) {
element.style.setProperty('--animate-repeat', String(Math.max(1, Number(step.iterations || 1))));
}
}
function getAnimationStepTimingMs(step) {
if (!step) {
return 0;
}
var preset = String(step.preset || 'none').trim();
if (!preset || preset === 'none') {
return 0;
}
var durationMs = Number(step.duration_ms);
if (!Number.isFinite(durationMs) || durationMs <= 0) {
durationMs = 1000;
}
var delayMs = Number(step.delay_ms);
if (!Number.isFinite(delayMs) || delayMs < 0) {
delayMs = 0;
}
var iterations = Number(step.iterations);
if (!Number.isFinite(iterations) || iterations < 1) {
iterations = 1;
}
return delayMs + (durationMs * iterations);
}
function getRegionAnimationPhaseTimingMs(root, phase) {
var timings = getRegionAnimationPhaseTimings(root, phase);
return timings.reduce(function (maxTimingMs, entry) {
return Math.max(maxTimingMs, Number(entry && entry.timingMs || 0));
}, 0);
}
function getRegionAnimationPhaseTimings(root, phase) {
if (!root) {
return [];
}
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
normalizedPhase = 'intro';
}
var timings = [];
Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) {
if (!element) {
return;
}
var config;
try {
config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}');
} catch (_error) {
config = null;
}
if (!config) {
return;
}
var timingMs = getAnimationStepTimingMs(normalizedPhase === 'outro' ? config.outro : config.intro);
if (timingMs > 0) {
timings.push({
element: element,
timingMs: timingMs
});
}
});
return timings;
}
function playRegionAnimation(element, phase) {
if (!element) {
return;
}
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
normalizedPhase = 'intro';
}
var config;
try {
config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}');
} catch (_error) {
config = null;
}
if (!config) {
return;
}
element.dataset.animationPhase = normalizedPhase;
clearRegionAnimationClasses(element);
if (normalizedPhase === 'outro') {
applyAnimationStep(element, config.outro, 'outro');
return;
}
if (config.intro && String(config.intro.preset || '').trim() && String(config.intro.preset || '').trim() !== 'none') {
applyAnimationStep(element, config.intro, 'intro');
if (config.loop && String(config.loop.preset || '').trim() && String(config.loop.preset || '').trim() !== 'none') {
element.addEventListener('animationend', function handleAnimationEnd(event) {
if (event.target !== element) {
return;
}
if (String(element.dataset.animationPhase || '').trim() !== 'intro') {
return;
}
element.removeEventListener('animationend', handleAnimationEnd);
clearRegionAnimationClasses(element);
applyAnimationStep(element, config.loop, 'loop');
});
}
return;
}
applyAnimationStep(element, config.loop, 'loop');
}
function playRegionAnimations(root, phase) {
if (!root) {
return;
}
var normalizedPhase = String(phase || 'intro').trim().toLowerCase();
if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') {
normalizedPhase = 'intro';
}
var elements = Array.prototype.slice.call(root.querySelectorAll('[data-animation-json]'));
elements.forEach(function (element) {
playRegionAnimation(element, normalizedPhase);
});
}
function setPlayerCanvasDimensions(canvasWidth, canvasHeight) {
if (!document || !document.documentElement) {
return;
@@ -600,6 +343,13 @@ function normalizeSlide(slide) {
Object.keys(content).forEach(function (regionKey) {
normalized.content[regionKey] = normalizeContentValue(content[regionKey]);
});
if (normalized.use_video_duration && normalized.template && Array.isArray(normalized.template.regions)) {
normalized.template.regions.forEach(function (region) {
if (region && region.region_type === 'video' && normalized.content[region.region_key]) {
normalized.content[region.region_key].loop = false;
}
});
}
return normalized;
}
@@ -0,0 +1,104 @@
// Slide transition timing, cancellation, and outgoing animation coordination.
var slideOutroTimers = [];
// 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 configured slide duration without shifting it for fade timing.
function getSlideHoldDelay(delayMs) {
return Math.max(1, Number(delayMs || 0));
}
// Calculate the configured time from slide entry to the next transition.
function getSlideAdvanceDelay(slide) {
return getSlideHoldDelay(Math.max(1, Number(slide && slide.duration_seconds || 10)) * 1000);
}
// Cancel any pending fade-transition cleanup.
function clearSlideTransitionTimer() {
if (slideTransitionTimer) {
window.clearTimeout(slideTransitionTimer);
slideTransitionTimer = null;
}
}