Release v2.10.6
This commit is contained in:
@@ -44,6 +44,22 @@
|
||||
})
|
||||
.catch(function () { return null; });
|
||||
}
|
||||
function keepPairingSessionLoaded(deviceId, clientId) {
|
||||
var pairingSessionPoll = null;
|
||||
function poll() {
|
||||
loadPairingSession(deviceId, clientId).then(function (payload) {
|
||||
if (payload && payload.pairingCode) {
|
||||
loadQr(deviceId, clientId);
|
||||
if (pairingSessionPoll) {
|
||||
window.clearInterval(pairingSessionPoll);
|
||||
pairingSessionPoll = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
poll();
|
||||
pairingSessionPoll = window.setInterval(poll, 1000);
|
||||
}
|
||||
function getClientId() {
|
||||
var stored = getSessionStorageItem("pulse-signage-player-client-id");
|
||||
if (stored) { return stored; }
|
||||
@@ -74,9 +90,7 @@
|
||||
if (qr && !qr.getAttribute("src")) {
|
||||
qr.src = qrPlaceholderSrc;
|
||||
}
|
||||
loadPairingSession(deviceId, clientId).then(function () {
|
||||
loadQr(deviceId, clientId);
|
||||
});
|
||||
keepPairingSessionLoaded(deviceId, clientId);
|
||||
window.setInterval(function () { redirectIfOnboarded(deviceId); }, 2000);
|
||||
});
|
||||
}());
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
let isBlackout = false;
|
||||
let pausedRemainingMs = null;
|
||||
let slideExpiresAt = null;
|
||||
const slideFadeDurationMs = 560;
|
||||
const slideFadeLengthMs = 560;
|
||||
const slideFadeOffsetMs = slideFadeLengthMs / 2;
|
||||
const commandSocketPath = '/ws/screens/' + encodeURIComponent(slug);
|
||||
const commandClientStorageKey = 'pulse-signage-player-client-id';
|
||||
const playlistSnapshotStorageKey = 'pulse-signage-player-playlist-snapshot:' + slug;
|
||||
@@ -64,10 +65,12 @@
|
||||
|
||||
function logDebug(message, details, level) {
|
||||
var logger = level === 'error' ? console.error : console.info;
|
||||
var timestamp = new Date().toISOString();
|
||||
var timestampedMessage = '[' + timestamp + '] ' + String(message || '');
|
||||
if (details) {
|
||||
logger(message, details);
|
||||
logger(timestampedMessage, details);
|
||||
} else {
|
||||
logger(message);
|
||||
logger(timestampedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -12,12 +12,10 @@ function normalizeStyleAttributeValue(value) {
|
||||
.replace(/&#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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Service worker cache strategy for player pages, assets, media, and playlists.
|
||||
|
||||
const CACHE_VERSION = 'v38';
|
||||
const CACHE_VERSION = new URL(self.location.href).searchParams.get('v') || 'development';
|
||||
const PAGE_CACHE = `pulse-signage-player-pages-${CACHE_VERSION}`;
|
||||
const ASSET_CACHE = `pulse-signage-player-assets-${CACHE_VERSION}`;
|
||||
const MEDIA_CACHE = `pulse-signage-player-media-${CACHE_VERSION}`;
|
||||
|
||||
@@ -112,6 +112,8 @@ function renderVideoRegion(region, regionContent) {
|
||||
var cachedSrc = regionKey ? String(videoRegionLastGoodSrcCache[regionKey] || '').trim() : '';
|
||||
var cachedSrcVersioned = appendCacheBust(cachedSrc, regionContent && regionContent.cache_bust);
|
||||
var disableAudio = regionContent && regionContent.disable_audio === undefined ? true : Boolean(regionContent && regionContent.disable_audio);
|
||||
var shouldLoop = !(regionContent && regionContent.loop === false);
|
||||
var loopMarkup = shouldLoop ? ' loop' : '';
|
||||
|
||||
if (!requestedSrc) {
|
||||
return '';
|
||||
@@ -122,7 +124,7 @@ function renderVideoRegion(region, regionContent) {
|
||||
videoRegionLastGoodSrcCache[regionKey] = requestedSrc;
|
||||
}
|
||||
setVideoSourceAvailability(requestedSrc, true);
|
||||
return '<div class="template-region video" style="' + region.baseStyle + '"><video src="' + escapeHtml(requestedSrcVersioned) + '" title="' + escapeHtml(region.label) + '" data-disable-audio="' + (disableAudio ? '1' : '0') + '" autoplay' + (disableAudio ? ' muted' : '') + ' loop playsinline preload="auto" disablepictureinpicture oncanplay="this.play().catch(function () {})"></video></div>';
|
||||
return '<div class="template-region video" style="' + region.baseStyle + '"><video src="' + escapeHtml(requestedSrcVersioned) + '" title="' + escapeHtml(region.label) + '" data-disable-audio="' + (disableAudio ? '1' : '0') + '" data-loop="' + (shouldLoop ? '1' : '0') + '"' + (disableAudio ? ' muted' : '') + loopMarkup + ' playsinline preload="auto" disablepictureinpicture></video></div>';
|
||||
}
|
||||
|
||||
var requestedState = getVideoSourceAvailability(requestedSrc);
|
||||
@@ -132,7 +134,7 @@ function renderVideoRegion(region, regionContent) {
|
||||
if (regionKey) {
|
||||
videoRegionLastGoodSrcCache[regionKey] = requestedSrc;
|
||||
}
|
||||
return '<div class="template-region video" style="' + region.baseStyle + '"><video src="' + escapeHtml(requestedSrcVersioned) + '" title="' + escapeHtml(region.label) + '" data-disable-audio="' + (disableAudio ? '1' : '0') + '" autoplay' + (disableAudio ? ' muted' : '') + ' loop playsinline preload="auto" disablepictureinpicture oncanplay="this.play().catch(function () {})"></video></div>';
|
||||
return '<div class="template-region video" style="' + region.baseStyle + '"><video src="' + escapeHtml(requestedSrcVersioned) + '" title="' + escapeHtml(region.label) + '" data-disable-audio="' + (disableAudio ? '1' : '0') + '" data-loop="' + (shouldLoop ? '1' : '0') + '"' + (disableAudio ? ' muted' : '') + loopMarkup + ' playsinline preload="auto" disablepictureinpicture></video></div>';
|
||||
}
|
||||
|
||||
scheduleVideoSourceProbe(regionKey, requestedSrc, false);
|
||||
@@ -141,7 +143,7 @@ function renderVideoRegion(region, regionContent) {
|
||||
if (cachedSrc !== requestedSrc) {
|
||||
logVideoRegionStatus('Keeping the previous playable video until the new mirrored file finishes transferring.', 'region=' + regionKey + ' old=' + cachedSrc + ' new=' + requestedSrc);
|
||||
}
|
||||
return '<div class="template-region video" style="' + region.baseStyle + '"><video src="' + escapeHtml(cachedSrcVersioned) + '" title="' + escapeHtml(region.label) + '" data-disable-audio="' + (disableAudio ? '1' : '0') + '" autoplay' + (disableAudio ? ' muted' : '') + ' loop playsinline preload="auto" disablepictureinpicture oncanplay="this.play().catch(function () {})"></video></div>';
|
||||
return '<div class="template-region video" style="' + region.baseStyle + '"><video src="' + escapeHtml(cachedSrcVersioned) + '" title="' + escapeHtml(region.label) + '" data-disable-audio="' + (disableAudio ? '1' : '0') + '" data-loop="' + (shouldLoop ? '1' : '0') + '"' + (disableAudio ? ' muted' : '') + loopMarkup + ' playsinline preload="auto" disablepictureinpicture></video></div>';
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
@@ -347,6 +347,9 @@ const playerPageTemplatePath = path.join(__dirname, 'player-page.template.html')
|
||||
const playerClientNameScriptPath = path.join(__dirname, 'player-client-name.script.html');
|
||||
const playerPageOfflineScriptPath = path.join(__dirname, 'public', 'js', 'player-page-offline.js');
|
||||
const playerPagePlaylistScriptPath = path.join(__dirname, 'public', 'js', 'player-page-playlist.js');
|
||||
const playerPageAnimationScriptPath = path.join(__dirname, 'public', 'js', 'player-page-animation.js');
|
||||
const playerPageMediaScriptPath = path.join(__dirname, 'public', 'js', 'player-page-media.js');
|
||||
const playerPageTransitionScriptPath = path.join(__dirname, 'public', 'js', 'player-page-transition.js');
|
||||
const playerPageCommandsScriptPath = path.join(__dirname, 'public', 'js', 'player-page-commands.js');
|
||||
const playerPageRenderingScriptPath = path.join(__dirname, 'public', 'js', 'player-page-rendering.js');
|
||||
const playerPagePlaybackScriptPath = path.join(__dirname, 'public', 'js', 'player-page-playback.js');
|
||||
@@ -540,6 +543,9 @@ function getPlayerRuntimeScripts() {
|
||||
['client-name', getScriptBody(getPlayerClientNameScript()())],
|
||||
['offline', getScriptBody(getPlayerPageOfflineScript()())],
|
||||
['playlist', getScriptBody(getPlayerPagePlaylistScript()())],
|
||||
['animation', fs.readFileSync(playerPageAnimationScriptPath, 'utf8').trim()],
|
||||
['media', fs.readFileSync(playerPageMediaScriptPath, 'utf8').trim()],
|
||||
['transition', fs.readFileSync(playerPageTransitionScriptPath, 'utf8').trim()],
|
||||
['commands', getScriptBody(getPlayerPageCommandsScript()())],
|
||||
['rendering', getScriptBody(getPlayerPageRenderingScript()())],
|
||||
['playback', getScriptBody(getPlayerPagePlaybackScript()())],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const Handlebars = require('handlebars');
|
||||
const path = require('path');
|
||||
const packageMetadata = require('../../package.json');
|
||||
const { mediaKind, safeJsonForScript, getPlayerPageTemplate, getPlayerPageAnnouncementsScript, getPlayerPageScript, getPlayerOnboardingLandingScript, getPlayerOnboardingFormScript, getPlayerRuntimeScripts } = require('./render-helpers');
|
||||
const { createPageAuthBundle, createPageFetchAuthScript } = require('#src/request-auth');
|
||||
const { getFontStylesheetHref } = require('#src/web/lib/media/font-library');
|
||||
@@ -22,11 +23,12 @@ function renderPage(template, options) {
|
||||
}
|
||||
|
||||
function getPlayerServiceWorkerRegistrationScript() {
|
||||
const releaseVersion = encodeURIComponent(String(packageMetadata.version || 'development'));
|
||||
return [
|
||||
'<script>',
|
||||
' if ("serviceWorker" in navigator) {',
|
||||
' window.addEventListener("load", function () {',
|
||||
' navigator.serviceWorker.register("/sw.js?v=38").catch(function () {',
|
||||
' navigator.serviceWorker.register("/sw.js?v=' + releaseVersion + '").catch(function () {',
|
||||
' return null;',
|
||||
' });',
|
||||
' });',
|
||||
@@ -120,7 +122,7 @@ function renderPlayerPage(slug, initialData) {
|
||||
REGION_SCRIPTS: ''
|
||||
});
|
||||
const runtimeScriptTags = getPlayerRuntimeScripts().map(function (entry) {
|
||||
return '<script src="/assets/player-script/' + encodeURIComponent(entry[0]) + '.js"></script>';
|
||||
return '<script src="/assets/player-script/' + encodeURIComponent(entry[0]) + '.js?v=' + encodeURIComponent(String(packageMetadata.version || 'development')) + '"></script>';
|
||||
}).join('');
|
||||
|
||||
return renderPage(template, {
|
||||
|
||||
+16
-2
@@ -83,7 +83,7 @@ function registerPlayerRoutes(app, options) {
|
||||
if (requestHeaders['x-pulse-page-auth']) {
|
||||
headers['x-pulse-page-auth'] = String(requestHeaders['x-pulse-page-auth']).trim();
|
||||
}
|
||||
if (requestHeaders['if-none-match']) {
|
||||
if (requestHeaders['if-none-match'] && !requestOptions.skipIfNoneMatch) {
|
||||
headers['if-none-match'] = String(requestHeaders['if-none-match']).trim();
|
||||
}
|
||||
if (requestHeaders['x-pulse-client-id']) {
|
||||
@@ -495,7 +495,21 @@ function registerPlayerRoutes(app, options) {
|
||||
res.set('Cache-Control', cacheControl);
|
||||
}
|
||||
if (response.status === 304) {
|
||||
return res.end();
|
||||
const cachedSnapshot = await readPlaylistSnapshot(req.params.slug);
|
||||
if (cachedSnapshot) {
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const refreshedResponse = await fetchBridge(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/playlist', {
|
||||
method: 'GET',
|
||||
skipIfNoneMatch: true
|
||||
});
|
||||
const refreshedData = await readJsonResponse(refreshedResponse);
|
||||
if (!refreshedResponse || refreshedResponse.status >= 400 || !refreshedData) {
|
||||
return res.status(502).json({ error: 'Thin client playlist cache unavailable.' });
|
||||
}
|
||||
await writePlaylistSnapshot(req.params.slug, refreshedData);
|
||||
return res.json(refreshedData);
|
||||
}
|
||||
res.type(response.headers.get('content-type') || 'application/json');
|
||||
const responseText = await response.text();
|
||||
|
||||
Reference in New Issue
Block a user