Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42279d95aa | ||
|
|
9c5cabbd13 |
@@ -6,6 +6,23 @@ 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
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Dashboard client action icons now render valid Bootstrap Icon classes without duplicating the `bi-` prefix.
|
||||||
|
|
||||||
## 1.5.13 - 2026-07-26
|
## 1.5.13 - 2026-07-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.5.13",
|
"version": "1.5.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "1.5.13",
|
"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.13",
|
"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
||||||
var blackoutCommandValue = blackout ? 'false' : 'true';
|
var blackoutCommandValue = blackout ? 'false' : 'true';
|
||||||
|
|
||||||
return '<div class="actions justify-content-end"><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="previous" aria-label="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="next" aria-label="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi bi-' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi bi-' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
|
return '<div class="actions justify-content-end"><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="previous" aria-label="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="next" aria-label="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</button></form><form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateClientActionCell(cell, client) {
|
function updateClientActionCell(cell, client) {
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
var paused = Boolean(client.paused);
|
var paused = Boolean(client.paused);
|
||||||
setButtonVariant(pauseButton, ['btn-secondary', 'btn-outline-primary'], 'btn-info');
|
setButtonVariant(pauseButton, ['btn-secondary', 'btn-outline-primary'], 'btn-info');
|
||||||
pauseButton.innerHTML = '<i class="bi bi-' + (paused ? 'play-fill' : 'pause-fill') + ' me-1" aria-hidden="true"></i>' + (paused ? 'Resume' : 'Pause');
|
pauseButton.innerHTML = '<i class="bi ' + (paused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (paused ? 'Resume' : 'Pause');
|
||||||
|
|
||||||
var pauseForm = pauseButton.form;
|
var pauseForm = pauseButton.form;
|
||||||
if (pauseForm) {
|
if (pauseForm) {
|
||||||
|
|||||||
@@ -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