Release 1.5.15
Publish Docker Image / build-and-push (push) Successful in 1m16s

This commit is contained in:
2026-07-26 21:06:03 +01:00
parent 9c5cabbd13
commit 42279d95aa
6 changed files with 70 additions and 14 deletions
+11
View File
@@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file.
- No unreleased changes recorded yet.
## 1.5.15 - 2026-07-26
### Changed
- Player slide transitions now start and pause video playback around the fade window so video regions stay aligned with the slide boundary.
- Dashboard action cards now use a three-column grid on wider layouts.
### Fixed
- Slide video duration probes now preserve sub-second precision instead of rounding to whole seconds.
## 1.5.14 - 2026-07-26
### Fixed
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pulse-signage",
"version": "1.5.14",
"version": "1.5.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pulse-signage",
"version": "1.5.14",
"version": "1.5.15",
"dependencies": {
"@sparticuz/chromium": "^137.0.0",
"bootstrap-icons": "1.11.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage",
"version": "1.5.14",
"version": "1.5.15",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"repository": {
+54 -9
View File
@@ -89,10 +89,13 @@ function scheduleSlideAdvance(delayMs) {
}, holdDelayMs);
}
// Add the fade time to a slide's hold duration unless the slide duration already accounts for it.
function getSlideHoldDelay(delayMs, skipFadePadding) {
// Account for fade only when it is enabled so the transition is centered on the slide boundary.
function getSlideHoldDelay(delayMs) {
var holdDelayMs = Math.max(1, Number(delayMs || 0));
return holdDelayMs + (currentPlaylistFadeBetweenSlides && !skipFadePadding ? slideFadeDurationMs : 0);
if (!currentPlaylistFadeBetweenSlides) {
return holdDelayMs;
}
return Math.max(1, holdDelayMs - (slideFadeDurationMs / 2));
}
// Cancel any pending fade-transition cleanup.
@@ -110,17 +113,20 @@ function renderSlideMarkup(markup, shouldFade) {
destroyRtmpRegions(app);
}
function initializeRenderedVideoPlayback(root) {
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 = true;
@@ -135,13 +141,29 @@ function renderSlideMarkup(markup, shouldFade) {
}
}
if (video.readyState >= 2) {
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', startPlayback, { once: true });
video.addEventListener('loadedmetadata', startPlayback, { once: true });
video.addEventListener('canplay', schedulePlaybackStart, { once: true });
video.addEventListener('loadedmetadata', function () {
if (video.readyState >= 2) {
schedulePlaybackStart();
}
}, { once: true });
});
}
@@ -168,6 +190,28 @@ 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';
nextShell.style.opacity = '0';
@@ -180,7 +224,7 @@ function renderSlideMarkup(markup, shouldFade) {
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(nextShell);
}
initializeRenderedVideoPlayback(nextShell);
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
return nextShell;
}
@@ -188,6 +232,7 @@ function renderSlideMarkup(markup, shouldFade) {
previousShell.classList.add('slide-shell');
}
previousShell.style.opacity = '1';
pauseRenderedVideoPlayback(previousShell, slideFadeDurationMs / 2);
app.appendChild(nextShell);
void nextShell.offsetHeight;
@@ -200,7 +245,7 @@ function renderSlideMarkup(markup, shouldFade) {
syncRtmpRegions(nextShell);
}
initializeRenderedVideoPlayback(nextShell);
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
slideTransitionTimer = window.setTimeout(function () {
if (previousShell && previousShell.parentNode) {
+1 -1
View File
@@ -290,7 +290,7 @@
.dashboard-action-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
+1 -1
View File
@@ -1277,7 +1277,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
var duration = Number(video.duration);
cleanup();
if (Number.isFinite(duration) && duration > 0) {
resolve(Math.max(1, Math.round(duration)));
resolve(Math.max(0.001, Math.round(duration * 1000) / 1000));
} else {
reject(new Error('The video duration could not be read.'));
}