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.
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "1.5.13",
|
||||
"version": "1.5.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pulse-signage",
|
||||
"version": "1.5.13",
|
||||
"version": "1.5.15",
|
||||
"dependencies": {
|
||||
"@sparticuz/chromium": "^137.0.0",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "1.5.13",
|
||||
"version": "1.5.15",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"repository": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
||||
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) {
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
var paused = Boolean(client.paused);
|
||||
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;
|
||||
if (pauseForm) {
|
||||
|
||||
@@ -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.'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user