Release 1.5.15
This commit is contained in:
@@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- No unreleased changes recorded yet.
|
- 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
|
## 1.5.14 - 2026-07-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.5.14",
|
"version": "1.5.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.5.14",
|
"version": "1.5.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sparticuz/chromium": "^137.0.0",
|
"@sparticuz/chromium": "^137.0.0",
|
||||||
"bootstrap-icons": "1.11.3",
|
"bootstrap-icons": "1.11.3",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.5.14",
|
"version": "1.5.15",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -89,10 +89,13 @@ function scheduleSlideAdvance(delayMs) {
|
|||||||
}, holdDelayMs);
|
}, holdDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the fade time to a slide's hold duration unless the slide duration already accounts for it.
|
// Account for fade only when it is enabled so the transition is centered on the slide boundary.
|
||||||
function getSlideHoldDelay(delayMs, skipFadePadding) {
|
function getSlideHoldDelay(delayMs) {
|
||||||
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
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.
|
// Cancel any pending fade-transition cleanup.
|
||||||
@@ -110,17 +113,20 @@ function renderSlideMarkup(markup, shouldFade) {
|
|||||||
destroyRtmpRegions(app);
|
destroyRtmpRegions(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeRenderedVideoPlayback(root) {
|
function initializeRenderedVideoPlayback(root, delayMs) {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startDelayMs = Math.max(0, Number(delayMs || 0));
|
||||||
var videos = root.querySelectorAll('.template-region.video video');
|
var videos = root.querySelectorAll('.template-region.video video');
|
||||||
Array.prototype.forEach.call(videos, function (video) {
|
Array.prototype.forEach.call(videos, function (video) {
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var playbackScheduled = false;
|
||||||
|
|
||||||
video.autoplay = true;
|
video.autoplay = true;
|
||||||
video.loop = true;
|
video.loop = true;
|
||||||
video.muted = 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();
|
startPlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.readyState >= 2) {
|
||||||
|
schedulePlaybackStart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.addEventListener('canplay', startPlayback, { once: true });
|
video.addEventListener('canplay', schedulePlaybackStart, { once: true });
|
||||||
video.addEventListener('loadedmetadata', startPlayback, { 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');
|
var nextShell = document.createElement('div');
|
||||||
nextShell.className = 'slide-shell';
|
nextShell.className = 'slide-shell';
|
||||||
nextShell.style.opacity = '0';
|
nextShell.style.opacity = '0';
|
||||||
@@ -180,7 +224,7 @@ function renderSlideMarkup(markup, shouldFade) {
|
|||||||
if (typeof syncRtmpRegions === 'function') {
|
if (typeof syncRtmpRegions === 'function') {
|
||||||
syncRtmpRegions(nextShell);
|
syncRtmpRegions(nextShell);
|
||||||
}
|
}
|
||||||
initializeRenderedVideoPlayback(nextShell);
|
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
|
||||||
return nextShell;
|
return nextShell;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +232,7 @@ function renderSlideMarkup(markup, shouldFade) {
|
|||||||
previousShell.classList.add('slide-shell');
|
previousShell.classList.add('slide-shell');
|
||||||
}
|
}
|
||||||
previousShell.style.opacity = '1';
|
previousShell.style.opacity = '1';
|
||||||
|
pauseRenderedVideoPlayback(previousShell, slideFadeDurationMs / 2);
|
||||||
|
|
||||||
app.appendChild(nextShell);
|
app.appendChild(nextShell);
|
||||||
void nextShell.offsetHeight;
|
void nextShell.offsetHeight;
|
||||||
@@ -200,7 +245,7 @@ function renderSlideMarkup(markup, shouldFade) {
|
|||||||
syncRtmpRegions(nextShell);
|
syncRtmpRegions(nextShell);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeRenderedVideoPlayback(nextShell);
|
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
|
||||||
|
|
||||||
slideTransitionTimer = window.setTimeout(function () {
|
slideTransitionTimer = window.setTimeout(function () {
|
||||||
if (previousShell && previousShell.parentNode) {
|
if (previousShell && previousShell.parentNode) {
|
||||||
|
|||||||
@@ -290,7 +290,7 @@
|
|||||||
|
|
||||||
.dashboard-action-grid {
|
.dashboard-action-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1277,7 +1277,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
|||||||
var duration = Number(video.duration);
|
var duration = Number(video.duration);
|
||||||
cleanup();
|
cleanup();
|
||||||
if (Number.isFinite(duration) && duration > 0) {
|
if (Number.isFinite(duration) && duration > 0) {
|
||||||
resolve(Math.max(1, Math.round(duration)));
|
resolve(Math.max(0.001, Math.round(duration * 1000) / 1000));
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('The video duration could not be read.'));
|
reject(new Error('The video duration could not be read.'));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user