Refine player control-plane flow

This commit is contained in:
2026-08-07 13:10:16 +01:00
parent 74318eb34e
commit 433be06bc7
35 changed files with 1604 additions and 233 deletions
+40 -2
View File
@@ -20,7 +20,36 @@ function createThinClientConfig() {
};
}
function logBridge(message, details) {
if (details === undefined) {
console.info(`[player-bridge] ${message}`);
return;
}
console.info(`[player-bridge] ${message}`, details);
}
function normalizeRemoteAddress(value) {
const address = String(value || '').trim();
if (!address) {
return '';
}
return address.toLowerCase().startsWith('::ffff:') ? address.slice(7) : address;
}
function formatPlayerConnectionLabel(deviceId, remoteAddress) {
const normalizedDeviceId = String(deviceId || '').trim() || 'unknown-player';
const normalizedRemoteAddress = normalizeRemoteAddress(remoteAddress);
return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId;
}
function resolveWebBaseUrl(req) {
const configuredWebBaseUrl = String(process.env.WEB_BASE_URL || '').trim().replace(/\/$/, '');
if (configuredWebBaseUrl) {
return configuredWebBaseUrl;
}
const forwardedHost = String(req && req.headers && req.headers['x-forwarded-host'] || '').trim().split(',')[0];
const host = forwardedHost || String(req && req.headers && req.headers.host || '').trim();
if (!host) {
@@ -40,7 +69,12 @@ function resolveWebBaseUrl(req) {
if (url.port === '8090') {
url.port = '8080';
} else if (!url.port) {
url.port = '8080';
const forwardedPort = String(req && req.headers && req.headers['x-forwarded-port'] || '').trim().split(',')[0];
if (forwardedPort) {
url.port = forwardedPort === '8090' ? '8080' : forwardedPort;
} else if (protocol === 'http') {
url.port = '8080';
}
}
return url.toString().replace(/\/$/, '');
@@ -600,6 +634,7 @@ async function start() {
});
playerSockets.set(deviceId, socket);
logBridge(`Player ${formatPlayerConnectionLabel(deviceId, socket.bridgeRemoteAddress)} has connected`);
socket.send(JSON.stringify({ type: 'registered', ok: true, player: player }));
return;
}
@@ -646,11 +681,14 @@ async function start() {
}
if (!verifyRequestAuth(request)) {
const remoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
logBridge(remoteAddress ? `Player (ip ${remoteAddress}) denied with wrong shared secret` : 'Player denied with wrong shared secret');
socket.destroy();
return;
}
playersWs.handleUpgrade(request, socket, head, function (ws) {
ws.bridgeRemoteAddress = normalizeRemoteAddress(request && request.socket && request.socket.remoteAddress);
playersWs.emit('connection', ws, request);
});
});
@@ -868,7 +906,7 @@ async function start() {
});
}
module.exports = { start: start };
module.exports = { start: start, resolveWebBaseUrl: resolveWebBaseUrl };
if (require.main === module) {
start().catch(function (error) {
+12 -6
View File
@@ -87,6 +87,8 @@ async function start() {
}
}
let webMediaSyncCompleted = false;
async function handleThinClientCommand(socket, rawMessage) {
let payload = null;
try {
@@ -246,11 +248,14 @@ async function start() {
internalBaseUrl: PLAYER_INTERNAL_BASE_URL
}));
triggerWebMediaSync().then(function (success) {
webMediaSyncTriggered = Boolean(success);
}).catch(function () {
webMediaSyncTriggered = false;
});
if (!webMediaSyncCompleted) {
triggerWebMediaSync().then(function (success) {
webMediaSyncTriggered = Boolean(success);
webMediaSyncCompleted = Boolean(success) || webMediaSyncCompleted;
}).catch(function () {
webMediaSyncTriggered = false;
});
}
heartbeatTimer = setInterval(function () {
if (!socket || socket.readyState !== WebSocket.OPEN) {
@@ -263,9 +268,10 @@ async function start() {
internalBaseUrl: PLAYER_INTERNAL_BASE_URL
}));
if (!webMediaSyncTriggered) {
if (!webMediaSyncTriggered && !webMediaSyncCompleted) {
triggerWebMediaSync().then(function (success) {
webMediaSyncTriggered = Boolean(success);
webMediaSyncCompleted = Boolean(success) || webMediaSyncCompleted;
}).catch(function () {
webMediaSyncTriggered = false;
});
+27 -30
View File
@@ -263,20 +263,33 @@ function renderSlideMarkup(markup, shouldFade) {
});
}
function schedulePostRenderSetup(root, delayMs) {
if (!root) {
return;
}
if (root.isConnected === false) {
return;
}
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(root);
}
if (!isThumbnailPreview()) {
initializeRegionInstances(root);
}
initializeRenderedVideoPlayback(root, delayMs);
if (typeof playRegionAnimations === 'function') {
playRegionAnimations(root, 'intro');
}
}
if (!shouldFade) {
app.innerHTML = markup;
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(app);
}
if (!isThumbnailPreview()) {
initializeRegionInstances(app);
}
initializeRenderedVideoPlayback(app);
if (typeof playRegionAnimations === 'function') {
window.requestAnimationFrame(function () {
playRegionAnimations(app, 'intro');
});
}
schedulePostRenderSetup(app, 0);
return app.firstElementChild;
}
@@ -325,11 +338,7 @@ function renderSlideMarkup(markup, shouldFade) {
app.innerHTML = '';
nextShell.style.opacity = '1';
app.appendChild(nextShell);
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(nextShell);
}
initializeRegionInstances(nextShell);
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
return nextShell;
}
@@ -340,24 +349,12 @@ function renderSlideMarkup(markup, shouldFade) {
pauseRenderedVideoPlayback(previousShell, slideFadeDurationMs / 2);
app.appendChild(nextShell);
void nextShell.offsetHeight;
window.requestAnimationFrame(function () {
nextShell.style.opacity = '1';
previousShell.style.opacity = '0';
if (typeof playRegionAnimations === 'function') {
playRegionAnimations(nextShell, 'intro');
}
schedulePostRenderSetup(nextShell, slideFadeDurationMs / 2);
});
if (typeof syncRtmpRegions === 'function') {
syncRtmpRegions(nextShell);
}
if (!isThumbnailPreview()) {
initializeRegionInstances(nextShell);
}
initializeRenderedVideoPlayback(nextShell, slideFadeDurationMs / 2);
slideTransitionTimer = window.setTimeout(function () {
if (previousShell && previousShell.parentNode) {
previousShell.parentNode.removeChild(previousShell);
@@ -54,6 +54,9 @@ async function renderSlideAtIndex(sourceSlides, targetIndex) {
index = currentIndex;
var markup = buildSlideMarkup(slide);
renderSlideMarkup(markup, currentPlaylistFadeBetweenSlides);
if (typeof scheduleSlideMarkupPreload === 'function') {
scheduleSlideMarkupPreload(availableSlides, currentIndex);
}
sendCommandState(slide);
if (!isPaused) {
scheduleSlideAdvance(getSlideHoldDelay(Math.max(1, Number(slide.duration_seconds || 10)) * 1000));
@@ -90,6 +93,9 @@ function showCurrent() {
if (typeof syncRtmpWarmups === 'function') {
syncRtmpWarmups(activeSlides, index);
}
if (typeof scheduleSlideMarkupPreload === 'function') {
scheduleSlideMarkupPreload(activeSlides, index);
}
if (!activeSlides.length) {
renderEmpty(slides.length ? 'No slides are scheduled for this time.' : 'No slides assigned to this screen yet.');
lastRenderedViewKey = getCurrentRenderKey(activeSlides);
@@ -211,6 +217,9 @@ function refresh() {
if (typeof syncRtmpWarmups === 'function') {
syncRtmpWarmups(nextActiveSlides, index);
}
if (typeof scheduleSlideMarkupPreload === 'function') {
scheduleSlideMarkupPreload(nextActiveSlides, index);
}
if (nextActiveSlides.length < 2) {
pendingPlaylistUpdate = {
slides: nextSlides,
+58 -6
View File
@@ -84,7 +84,7 @@ function getCurrentRenderKey(activeSlides) {
return [currentPlaylistSignature || '', viewportKey, 'slide', slide && slide.id ? slide.id : ''].join('|');
}
// Pick the current slide and the next slide for webpage preloading.
// Pick the next slide for webpage preloading.
function getWebpagePreloadSlides(sourceSlides, targetIndex) {
const availableSlides = Array.isArray(sourceSlides) ? sourceSlides : [];
if (!availableSlides.length) {
@@ -97,19 +97,71 @@ function getWebpagePreloadSlides(sourceSlides, targetIndex) {
}
const preloadSlides = [];
const currentSlide = availableSlides[normalizedIndex];
const nextSlide = availableSlides[normalizedIndex + 1];
if (currentSlide) {
preloadSlides.push(currentSlide);
}
if (nextSlide && nextSlide !== currentSlide) {
if (nextSlide) {
preloadSlides.push(nextSlide);
}
return preloadSlides;
}
// Pick the next slide to warm its markup before it becomes visible.
function getSlideMarkupPreloadSlides(sourceSlides, targetIndex) {
const availableSlides = Array.isArray(sourceSlides) ? sourceSlides : [];
if (!availableSlides.length) {
return [];
}
let normalizedIndex = Number(targetIndex || 0);
if (!Number.isFinite(normalizedIndex) || normalizedIndex < 0 || normalizedIndex >= availableSlides.length) {
normalizedIndex = 0;
}
const preloadSlides = [];
const nextSlide = availableSlides[normalizedIndex + 1];
if (nextSlide) {
preloadSlides.push(nextSlide);
}
return preloadSlides;
}
function scheduleSlideMarkupPreload(sourceSlides, targetIndex) {
if (window.__pulseThumbnailPreview) {
return;
}
const preloadSlides = getSlideMarkupPreloadSlides(sourceSlides, targetIndex);
if (!preloadSlides.length || typeof primeSlideMarkup !== 'function') {
return;
}
const slide = preloadSlides[0];
const preloadSignature = [
currentPlaylistSignature || '',
slide && slide.id ? slide.id : '',
slide && slide.template_id ? slide.template_id : '',
slide && slide.modified_at ? slide.modified_at : '',
window.innerWidth + 'x' + window.innerHeight,
videoRegionRenderVersion || 0
].join('|');
if (scheduleSlideMarkupPreload.signature === preloadSignature) {
return;
}
scheduleSlideMarkupPreload.signature = preloadSignature;
window.setTimeout(function () {
if (scheduleSlideMarkupPreload.signature !== preloadSignature) {
return;
}
primeSlideMarkup(slide);
}, 0);
}
// Mount hidden iframe preloads for the chosen webpage URLs.
function syncWebpagePreloads(sourceSlides, targetIndex) {
const urls = getWebpageUrls(getWebpagePreloadSlides(sourceSlides, targetIndex));
+49 -3
View File
@@ -893,6 +893,45 @@ function getSlideMarkupCacheKey(slide) {
return [currentPlaylistSignature || '', slide && slide.id ? slide.id : '', slide && slide.template_id ? slide.template_id : '', slide && slide.modified_at ? slide.modified_at : '', viewportKey, videoRegionRenderVersion || 0].join('|');
}
function restorePlayerCanvasDimensions(width, height) {
if (!document || !document.documentElement) {
return;
}
var style = document.documentElement.style;
if (width) {
style.setProperty('--player-canvas-width', width);
} else {
style.removeProperty('--player-canvas-width');
}
if (height) {
style.setProperty('--player-canvas-height', height);
} else {
style.removeProperty('--player-canvas-height');
}
}
function primeSlideMarkup(slide) {
if (!slide) {
return '';
}
var cachedMarkup = getCachedSlideMarkup(slide);
if (cachedMarkup) {
return cachedMarkup;
}
var previousWidth = document && document.documentElement && document.documentElement.style ? String(document.documentElement.style.getPropertyValue('--player-canvas-width') || '') : '';
var previousHeight = document && document.documentElement && document.documentElement.style ? String(document.documentElement.style.getPropertyValue('--player-canvas-height') || '') : '';
try {
return buildSlideMarkupForState(slide, false);
} finally {
restorePlayerCanvasDimensions(previousWidth.trim(), previousHeight.trim());
}
}
function notifyVideoRegionSourceReady() {
if (typeof videoRegionRenderVersion === 'number') {
videoRegionRenderVersion += 1;
@@ -919,9 +958,12 @@ function setCachedSlideMarkup(slide, markup) {
// Slide rendering and markup cache helpers.
// Choose the right slide renderer and cache the result.
function buildSlideMarkup(slide) {
lastRenderedSlide = slide || null;
syncBlackoutState();
function buildSlideMarkupForState(slide, updateCurrentState) {
if (updateCurrentState) {
lastRenderedSlide = slide || null;
syncBlackoutState();
}
var cachedMarkup = getCachedSlideMarkup(slide);
if (cachedMarkup) {
return cachedMarkup;
@@ -938,3 +980,7 @@ function buildSlideMarkup(slide) {
setCachedSlideMarkup(slide, markup);
return markup;
}
function buildSlideMarkup(slide) {
return buildSlideMarkupForState(slide, true);
}
+5 -10
View File
@@ -304,13 +304,9 @@ function getRtmpWarmupSlides(sourceSlides, targetIndex) {
}
var warmupSlides = [];
var currentSlide = availableSlides[normalizedIndex];
var nextSlide = availableSlides[normalizedIndex + 1];
if (currentSlide) {
warmupSlides.push(currentSlide);
}
if (nextSlide && nextSlide !== currentSlide) {
if (nextSlide) {
warmupSlides.push(nextSlide);
}
@@ -693,12 +689,11 @@ function startRtmpPlayback(video, sourceUrl, disableAudio, skipUnavailable, plac
if (window.Hls && window.Hls.isSupported && window.Hls.isSupported()) {
var hls = new window.Hls({
enableWorker: true,
lowLatencyMode: true,
liveSyncDurationCount: 4,
liveMaxLatencyDurationCount: 8,
maxBufferLength: 20,
liveSyncDurationCount: 6,
liveMaxLatencyDurationCount: 12,
maxBufferLength: 30,
maxLiveSyncPlaybackRate: 1,
backBufferLength: 30
backBufferLength: 60
});
video.__rtmpHls = hls;
hls.attachMedia(video);
+3 -3
View File
@@ -122,7 +122,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 () {})" onloadedmetadata="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') + '" autoplay' + (disableAudio ? ' muted' : '') + ' loop playsinline preload="auto" disablepictureinpicture oncanplay="this.play().catch(function () {})"></video></div>';
}
var requestedState = getVideoSourceAvailability(requestedSrc);
@@ -132,7 +132,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 () {})" onloadedmetadata="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') + '" autoplay' + (disableAudio ? ' muted' : '') + ' loop playsinline preload="auto" disablepictureinpicture oncanplay="this.play().catch(function () {})"></video></div>';
}
scheduleVideoSourceProbe(regionKey, requestedSrc, false);
@@ -141,7 +141,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 () {})" onloadedmetadata="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') + '" autoplay' + (disableAudio ? ' muted' : '') + ' loop playsinline preload="auto" disablepictureinpicture oncanplay="this.play().catch(function () {})"></video></div>';
}
return '';
@@ -18,50 +18,57 @@ function registerFontSweepTask(options) {
const uploadSyncService = options && options.uploadSyncService;
const pushUploadFileToPlayer = uploadSyncService && uploadSyncService.pushUploadFileToPlayer;
const removeUploadFileFromPlayer = uploadSyncService && uploadSyncService.removeUploadFileFromPlayer;
const getPlayerTaskMetadata = uploadSyncService && uploadSyncService.getPlayerTaskMetadata;
const mediaDir = String(options && options.mediaDir || '').trim();
if (!backgroundTaskQueue || typeof pushUploadFileToPlayer !== 'function' || typeof removeUploadFileFromPlayer !== 'function' || !mediaDir) {
throw new Error('registerFontSweepTask requires the font sweep dependencies.');
}
backgroundTaskQueue.registerRecurringTask({
key: TASK.key,
title: TASK.title,
category: TASK.category,
intervalMs: TASK.intervalMs,
metadata: {
mediaDir: mediaDir
},
run: async function () {
const desiredOperations = collectFontLibrarySyncOperations(mediaDir);
const desiredUploadPaths = new Set(desiredOperations.map(function (operation) {
return operation && operation.uploadPath ? operation.uploadPath : '';
}).filter(Boolean));
const currentUploadPaths = await collectFontLibraryDirectoryUploadPaths(mediaDir);
const metadataPromise = typeof getPlayerTaskMetadata === 'function'
? Promise.resolve(getPlayerTaskMetadata())
: Promise.resolve({});
for (let i = 0; i < desiredOperations.length; i += 1) {
const operation = desiredOperations[i] || {};
const uploadPath = String(operation.uploadPath || '').trim();
if (!uploadPath) {
continue;
return metadataPromise.then(function (metadata) {
backgroundTaskQueue.registerRecurringTask({
key: TASK.key,
title: TASK.title,
category: TASK.category,
intervalMs: TASK.intervalMs,
metadata: Object.assign({
mediaDir: mediaDir
}, metadata || {}),
run: async function () {
const desiredOperations = collectFontLibrarySyncOperations(mediaDir);
const desiredUploadPaths = new Set(desiredOperations.map(function (operation) {
return operation && operation.uploadPath ? operation.uploadPath : '';
}).filter(Boolean));
const currentUploadPaths = await collectFontLibraryDirectoryUploadPaths(mediaDir);
for (let i = 0; i < desiredOperations.length; i += 1) {
const operation = desiredOperations[i] || {};
const uploadPath = String(operation.uploadPath || '').trim();
if (!uploadPath) {
continue;
}
if (String(operation.type || '').trim().toLowerCase() === 'delete') {
await removeUploadFileFromPlayer(uploadPath, mediaDir);
} else {
await pushUploadFileToPlayer(uploadPath, mediaDir);
}
}
if (String(operation.type || '').trim().toLowerCase() === 'delete') {
for (let i = 0; i < currentUploadPaths.length; i += 1) {
const uploadPath = String(currentUploadPaths[i] || '').trim();
if (!uploadPath || desiredUploadPaths.has(uploadPath)) {
continue;
}
await removeUploadFileFromPlayer(uploadPath, mediaDir);
} else {
await pushUploadFileToPlayer(uploadPath, mediaDir);
}
}
for (let i = 0; i < currentUploadPaths.length; i += 1) {
const uploadPath = String(currentUploadPaths[i] || '').trim();
if (!uploadPath || desiredUploadPaths.has(uploadPath)) {
continue;
}
await removeUploadFileFromPlayer(uploadPath, mediaDir);
}
}
});
});
}
@@ -8,22 +8,30 @@ const TASK = {
function registerInitialFontSyncTask(options) {
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const mediaDir = String(options && options.mediaDir || '').trim();
const uploadSyncService = options && options.uploadSyncService;
if (!backgroundTaskQueue || !mediaDir) {
throw new Error('registerInitialFontSyncTask requires the initial font sync dependencies.');
}
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial font sync',
category: TASK.category,
taskType: 'font-sync',
payload: {
mode: 'initial',
uploadDir: mediaDir,
operations: collectFontLibrarySyncOperations(mediaDir)
},
persist: true
const metadataPromise = uploadSyncService && typeof uploadSyncService.getPlayerTaskMetadata === 'function'
? uploadSyncService.getPlayerTaskMetadata()
: Promise.resolve({});
return Promise.resolve(metadataPromise).then(function (metadata) {
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial font sync',
category: TASK.category,
taskType: 'font-sync',
metadata: Object.assign({}, metadata || {}),
payload: {
mode: 'initial',
uploadDir: mediaDir,
operations: collectFontLibrarySyncOperations(mediaDir)
},
persist: true
});
}).catch(function (error) {
console.warn('Unable to queue initial font sync:', error);
});
@@ -6,21 +6,29 @@ const TASK = {
function registerInitialMediaSyncTask(options) {
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const mediaDir = String(options && options.mediaDir || '').trim();
const uploadSyncService = options && options.uploadSyncService;
if (!backgroundTaskQueue || !mediaDir) {
throw new Error('registerInitialMediaSyncTask requires the initial media sync dependencies.');
}
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial media sync',
category: TASK.category,
taskType: 'media-sync',
payload: {
mode: 'initial',
uploadDir: mediaDir
},
persist: true
const metadataPromise = uploadSyncService && typeof uploadSyncService.getPlayerTaskMetadata === 'function'
? uploadSyncService.getPlayerTaskMetadata()
: Promise.resolve({});
return Promise.resolve(metadataPromise).then(function (metadata) {
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial media sync',
category: TASK.category,
taskType: 'media-sync',
metadata: Object.assign({}, metadata || {}),
payload: {
mode: 'initial',
uploadDir: mediaDir
},
persist: true
});
}).catch(function (error) {
console.warn('Unable to queue initial media sync:', error);
});
+146 -24
View File
@@ -6,12 +6,40 @@ const crypto = require('crypto');
const multer = require('multer');
const { createRequestAuthHeaders } = require('#src/request-auth');
const { collectFontLibrarySyncOperations } = require('./font-library');
const { getConfiguredPlayerIdentifier, resolvePlayerRegistration } = require('#src/data/player-registry');
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
function normalizeUploadRoot(uploadDir) {
return path.resolve(String(uploadDir || '').trim());
}
function isLocalLikeBaseUrl(value) {
let host = '';
try {
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
} catch (_error) {
return false;
}
return host === 'localhost'
|| host === '127.0.0.1'
|| host === '::1'
|| host === 'host.docker.internal'
|| host === 'player'
|| host === 'web'
|| host === 'player-bridge'
|| host.endsWith('.local')
|| host.endsWith('.internal')
|| host.endsWith('.docker.internal');
}
function normalizeBaseUrl(value) {
return String(value || '').trim().replace(/\/$/, '');
}
function normalizePlayerRowBaseUrl(player) {
return normalizeBaseUrl(player && player.internal_base_url);
}
function createUploadSyncService(options) {
const pool = options && options.pool;
const common = options && options.common;
@@ -21,54 +49,120 @@ function createUploadSyncService(options) {
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
const MAX_FIELD_BYTES = 10 * 1024 * 1024;
const PLAYER_UPLOAD_SYNC_RETRY_LOG_INTERVAL_MS = 60000;
const pendingPlayerUploadSyncs = new Map();
let pendingPlayerUploadSyncFlushTimer = null;
let pendingPlayerUploadSyncFlushInFlight = null;
let pendingPlayerUploadSyncRetryLogAt = 0;
const pendingPlaylistUploadSyncs = new Map();
let pendingPlaylistUploadSyncFlushTimer = null;
let pendingPlaylistUploadSyncFlushInFlight = null;
let playerInternalBaseUrl = null;
let playerInternalBaseUrlPromise = null;
let playerTaskMetadata = null;
let playerTaskMetadataPromise = null;
if (!common || !playerSnapshotCache || typeof notifyPlayerScreens !== 'function') {
throw new Error('createUploadSyncService requires the upload dependencies.');
}
async function getPlayerInternalBaseUrl() {
if (playerInternalBaseUrl) {
return playerInternalBaseUrl;
const metadata = await getPlayerTaskMetadata();
return metadata && metadata.playerInternalBaseUrl ? metadata.playerInternalBaseUrl : null;
}
async function getPlayerTaskMetadata() {
if (playerTaskMetadata) {
return playerTaskMetadata;
}
if (playerInternalBaseUrlPromise) {
return playerInternalBaseUrlPromise;
if (playerTaskMetadataPromise) {
return playerTaskMetadataPromise;
}
playerInternalBaseUrlPromise = (async function () {
if (!pool) {
return configuredPlayerInternalBaseUrl || null;
}
playerTaskMetadataPromise = (async function () {
try {
const configuredPlayerIdentifier = getConfiguredPlayerIdentifier();
const player = await resolvePlayerRegistration(pool, configuredPlayerIdentifier);
const resolvedBaseUrl = String(player && player.internal_base_url || '').trim().replace(/\/$/, '');
if (resolvedBaseUrl) {
playerInternalBaseUrl = resolvedBaseUrl;
return resolvedBaseUrl;
if (pool && typeof fetchPlayerRegistrations === 'function') {
const configuredPlayerIdentifier = getConfiguredPlayerIdentifier();
const players = await fetchPlayerRegistrations(pool);
const exactPlayer = Array.isArray(players)
? players.find(function (player) {
return String(player && player.identifier || '').trim() === configuredPlayerIdentifier;
})
: null;
const registeredPlayers = Array.isArray(players) ? players : [];
const preferredPlayer = registeredPlayers.find(function (player) {
const internalBaseUrl = normalizePlayerRowBaseUrl(player);
return internalBaseUrl && !isLocalLikeBaseUrl(internalBaseUrl);
}) || exactPlayer || registeredPlayers[0] || null;
const resolvedInternalBaseUrl = normalizePlayerRowBaseUrl(preferredPlayer);
const resolvedPublicBaseUrl = normalizeBaseUrl(preferredPlayer && preferredPlayer.public_base_url);
const resolvedIdentifier = String(preferredPlayer && preferredPlayer.identifier || '').trim();
if (resolvedInternalBaseUrl || resolvedPublicBaseUrl || resolvedIdentifier) {
playerInternalBaseUrl = resolvedInternalBaseUrl || null;
playerTaskMetadata = {
playerIdentifier: resolvedIdentifier || null,
playerPublicBaseUrl: resolvedPublicBaseUrl || null,
playerInternalBaseUrl: resolvedInternalBaseUrl || null,
playerLabel: resolvedIdentifier || resolvedPublicBaseUrl || resolvedInternalBaseUrl || null
};
return playerTaskMetadata;
}
}
} catch (_error) {
}
return configuredPlayerInternalBaseUrl || null;
})().then(function (baseUrl) {
playerInternalBaseUrlPromise = null;
return baseUrl || null;
playerInternalBaseUrl = configuredPlayerInternalBaseUrl || null;
playerTaskMetadata = {
playerIdentifier: getConfiguredPlayerIdentifier() || null,
playerPublicBaseUrl: null,
playerInternalBaseUrl: playerInternalBaseUrl,
playerLabel: getConfiguredPlayerIdentifier() || playerInternalBaseUrl || null
};
return playerTaskMetadata;
})().then(function (metadata) {
playerTaskMetadataPromise = null;
return metadata || null;
}, function () {
playerInternalBaseUrlPromise = null;
return configuredPlayerInternalBaseUrl || null;
playerTaskMetadataPromise = null;
return {
playerIdentifier: getConfiguredPlayerIdentifier() || null,
playerPublicBaseUrl: null,
playerInternalBaseUrl: configuredPlayerInternalBaseUrl || null,
playerLabel: getConfiguredPlayerIdentifier() || configuredPlayerInternalBaseUrl || null
};
});
return playerInternalBaseUrlPromise;
return playerTaskMetadataPromise;
}
function formatPlayerTaskLabel(metadata) {
const playerLabel = String(metadata && metadata.playerLabel || '').trim();
if (playerLabel) {
return playerLabel;
}
const playerIdentifier = String(metadata && metadata.playerIdentifier || '').trim();
if (playerIdentifier) {
return playerIdentifier;
}
const playerPublicBaseUrl = String(metadata && metadata.playerPublicBaseUrl || '').trim();
if (playerPublicBaseUrl) {
return playerPublicBaseUrl;
}
return '';
}
function logMediaSyncSummary(level, message, metadata, details) {
const suffix = formatPlayerTaskLabel(metadata);
const logger = level === 'warn' ? console.warn : console.info;
if (details !== undefined) {
logger(`[media-sync] ${message}${suffix ? ` for ${suffix}` : ''}`, details);
return;
}
logger(`[media-sync] ${message}${suffix ? ` for ${suffix}` : ''}`);
}
function createUploadMiddleware(uploadDir) {
@@ -561,6 +655,10 @@ function createUploadSyncService(options) {
pendingPlaylistUploadSyncs.delete(operation.key);
}
if (pendingEntries.length) {
logMediaSyncSummary('info', `Playlist sync flushed ${pendingEntries.length} task${pendingEntries.length === 1 ? '' : 's'}`, pendingEntries[0] && pendingEntries[0].metadata);
}
})().finally(function () {
pendingPlaylistUploadSyncFlushInFlight = null;
if (pendingPlaylistUploadSyncs.size) {
@@ -582,6 +680,11 @@ function createUploadSyncService(options) {
pendingPlayerUploadSyncFlushInFlight = (async function () {
const pendingEntries = Array.from(pendingPlayerUploadSyncs.values());
const playerMetadata = pendingEntries.length && pendingEntries[0] && pendingEntries[0].metadata
? pendingEntries[0].metadata
: await getPlayerTaskMetadata();
let successCount = 0;
let failureCount = 0;
for (let i = 0; i < pendingEntries.length; i += 1) {
const operation = pendingEntries[i];
let success = false;
@@ -591,9 +694,25 @@ function createUploadSyncService(options) {
success = await pushUploadFileToPlayer(operation.uploadPath, operation.uploadDir);
}
if (success) {
successCount += 1;
pendingPlayerUploadSyncs.delete(operation.uploadPath);
} else {
failureCount += 1;
}
}
if (successCount) {
logMediaSyncSummary('info', `Media sync completed ${successCount} upload${successCount === 1 ? '' : 's'}`, playerMetadata);
}
if (failureCount) {
const now = Date.now();
if (!pendingPlayerUploadSyncRetryLogAt || now - pendingPlayerUploadSyncRetryLogAt >= PLAYER_UPLOAD_SYNC_RETRY_LOG_INTERVAL_MS) {
pendingPlayerUploadSyncRetryLogAt = now;
logMediaSyncSummary('warn', `Player unavailable, retry queued for ${failureCount} upload${failureCount === 1 ? '' : 's'}`, playerMetadata);
}
} else if (!pendingPlayerUploadSyncs.size) {
pendingPlayerUploadSyncRetryLogAt = 0;
}
})().finally(function () {
pendingPlayerUploadSyncFlushInFlight = null;
if (pendingPlayerUploadSyncs.size) {
@@ -689,12 +808,14 @@ function createUploadSyncService(options) {
safePayload.operation = Object.assign({}, safePayload.operation);
delete safePayload.operation.pool;
}
const playerMetadata = await getPlayerTaskMetadata();
const definition = {
key: taskKey,
title: title,
category: 'media-sync',
taskType: 'media-sync',
metadata: Object.assign({}, playerMetadata || {}),
payload: safePayload,
persist: true
};
@@ -732,7 +853,8 @@ function createUploadSyncService(options) {
flushPendingPlaylistUploadSyncs: flushPendingPlaylistUploadSyncs,
flushPendingPlayerUploadSyncs: flushPendingPlayerUploadSyncs,
runMediaSyncTask: runMediaSyncTask,
queueMediaSyncTask: queueMediaSyncTask
queueMediaSyncTask: queueMediaSyncTask,
getPlayerTaskMetadata: getPlayerTaskMetadata
};
}
+49 -16
View File
@@ -1,5 +1,29 @@
const { createRequestAuthHeaders } = require('#src/request-auth');
const { getConfiguredPlayerIdentifier, resolvePlayerRegistration } = require('#src/data/player-registry');
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
function isLocalLikeBaseUrl(value) {
let host = '';
try {
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
} catch (_error) {
return false;
}
return host === 'localhost'
|| host === '127.0.0.1'
|| host === '::1'
|| host === 'host.docker.internal'
|| host === 'player'
|| host === 'web'
|| host === 'player-bridge'
|| host.endsWith('.local')
|| host.endsWith('.internal')
|| host.endsWith('.docker.internal');
}
function normalizeBaseUrl(value) {
return String(value || '').trim().replace(/\/$/, '');
}
function createPlayerActionService(options) {
const pool = options && options.pool;
@@ -14,10 +38,6 @@ function createPlayerActionService(options) {
let playerInternalBaseUrlPromise = null;
async function getPlayerInternalBaseUrl() {
if (configuredPlayerInternalBaseUrl) {
return configuredPlayerInternalBaseUrl;
}
if (playerInternalBaseUrl) {
return playerInternalBaseUrl;
}
@@ -27,22 +47,35 @@ function createPlayerActionService(options) {
}
playerInternalBaseUrlPromise = (async function () {
if (!pool) {
return configuredPlayerInternalBaseUrl || null;
}
try {
const configuredPlayerIdentifier = getConfiguredPlayerIdentifier();
const player = await resolvePlayerRegistration(pool, configuredPlayerIdentifier);
const resolvedBaseUrl = String(player && player.internal_base_url || '').trim().replace(/\/$/, '');
if (resolvedBaseUrl) {
playerInternalBaseUrl = resolvedBaseUrl;
return resolvedBaseUrl;
if (pool && typeof fetchPlayerRegistrations === 'function') {
const configuredPlayerIdentifier = getConfiguredPlayerIdentifier();
const players = await fetchPlayerRegistrations(pool);
const exactPlayer = Array.isArray(players)
? players.find(function (player) {
return String(player && player.identifier || '').trim() === configuredPlayerIdentifier;
})
: null;
const registeredPlayers = Array.isArray(players) ? players : [];
const preferredPlayer = registeredPlayers.find(function (player) {
const internalBaseUrl = normalizeBaseUrl(player && player.internal_base_url);
return internalBaseUrl && !isLocalLikeBaseUrl(internalBaseUrl);
}) || exactPlayer || registeredPlayers[0] || null;
const resolvedBaseUrl = normalizeBaseUrl(preferredPlayer && preferredPlayer.internal_base_url);
if (resolvedBaseUrl) {
playerInternalBaseUrl = resolvedBaseUrl;
return resolvedBaseUrl;
}
}
} catch (_error) {
}
return configuredPlayerInternalBaseUrl || null;
if (configuredPlayerInternalBaseUrl) {
playerInternalBaseUrl = configuredPlayerInternalBaseUrl;
return configuredPlayerInternalBaseUrl;
}
return null;
})().then(function (baseUrl) {
playerInternalBaseUrlPromise = null;
return baseUrl || null;
+38 -15
View File
@@ -7,6 +7,8 @@
var setButtonVariant = webUiHelpers.setButtonVariant;
var normalizeDisplayIp = webUiHelpers.normalizeDisplayIp;
var latestDashboardState = null;
var ALL_SCREENS_SLUG = '__all__';
var ALL_SCREENS_LABEL = 'All screens';
function getClientMoveModalElements() {
return {
@@ -507,13 +509,22 @@
}
select.disabled = false;
if (!select.value || !screenBySlug[select.value]) {
select.value = screens[0].slug || '';
}
var selectedScreen = screenBySlug[select.value] || screens[0];
var selectedSlug = String(selectedScreen && selectedScreen.slug || '').trim();
var selectedOption = select.options && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
var isAllSelected = Boolean(selectedOption && String(selectedOption.getAttribute('data-screen-target-all') || '').toLowerCase() === 'true') || select.value === ALL_SCREENS_SLUG;
var selectedScreen = isAllSelected
? {
slug: ALL_SCREENS_SLUG,
name: String(selectedOption && selectedOption.textContent || ALL_SCREENS_LABEL).trim() || ALL_SCREENS_LABEL
}
: screenBySlug[select.value] || null;
var selectedSlug = isAllSelected
? ALL_SCREENS_SLUG
: String(selectedScreen && selectedScreen.slug || '').trim();
var selectedClients = Array.isArray(state && state.clients) ? state.clients.filter(function (client) {
if (isAllSelected) {
return true;
}
return String(client && client.screen_slug || '').trim() === selectedSlug;
}) : [];
var connectionCount = selectedClients.length;
@@ -532,12 +543,20 @@
pill.textContent = connectionLabel;
}
if (nameNode) {
nameNode.textContent = String(selectedScreen && selectedScreen.name || 'Selected screen');
nameNode.textContent = selectedScreen
? String(selectedScreen.name || 'Selected screen')
: 'Select a target screen group';
}
if (metaNode) {
metaNode.textContent = 'Commands sent here target every client currently using this screen.';
metaNode.textContent = !selectedSlug
? 'Choose a screen group before sending commands.'
: isAllSelected
? 'Commands sent here target every client across every screen group.'
: 'Commands sent here target every client currently using this screen.';
}
var commandTargetSlug = selectedSlug || '';
forms.forEach(function (form) {
var command = String(form.getAttribute('data-screen-command-action') || '').trim().toLowerCase();
var commandInput = form.querySelector('input[name="command"]');
@@ -550,10 +569,12 @@
pauseStateInput.value = allPaused ? 'false' : 'true';
}
if (button) {
button.innerHTML = '<i class="bi ' + (allPaused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (allPaused ? 'Resume screen' : 'Pause screen');
button.innerHTML = '<i class="bi ' + (allPaused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (allPaused ? (isAllSelected ? 'Resume all screens' : 'Resume screen') : (isAllSelected ? 'Pause all screens' : 'Pause screen'));
}
setButtonVariant(button, ['btn-success', 'btn-info', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info');
form.setAttribute('data-confirm-message', allPaused ? 'Resume all connected clients on this screen?' : 'Pause all connected clients on this screen?');
form.setAttribute('data-confirm-message', allPaused
? (isAllSelected ? 'Resume all connected clients on all screens?' : 'Resume all connected clients on this screen?')
: (isAllSelected ? 'Pause all connected clients on all screens?' : 'Pause all connected clients on this screen?'));
} else if (command === 'blackout') {
commandInput.value = 'blackout';
var blackoutStateInput = form.querySelector('input[name="blackout"]');
@@ -561,23 +582,25 @@
blackoutStateInput.value = allBlackout ? 'false' : 'true';
}
if (button) {
button.innerHTML = '<i class="bi ' + (allBlackout ? 'bi-eye' : 'bi-eye-slash') + ' me-1" aria-hidden="true"></i>' + (allBlackout ? 'Restore screen' : 'Blackout screen');
button.innerHTML = '<i class="bi ' + (allBlackout ? 'bi-eye' : 'bi-eye-slash') + ' me-1" aria-hidden="true"></i>' + (allBlackout ? (isAllSelected ? 'Restore all screens' : 'Restore screen') : (isAllSelected ? 'Blackout all screens' : 'Blackout screen'));
}
setButtonVariant(button, ['btn-success', 'btn-secondary', 'btn-danger', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary');
form.setAttribute('data-confirm-message', allBlackout ? 'Restore all connected clients on this screen?' : 'Blackout all connected clients on this screen?');
form.setAttribute('data-confirm-message', allBlackout
? (isAllSelected ? 'Restore all connected clients on all screens?' : 'Restore all connected clients on this screen?')
: (isAllSelected ? 'Blackout all connected clients on all screens?' : 'Blackout all connected clients on this screen?'));
} else {
commandInput.value = command || commandInput.value || '';
}
}
form.action = selectedSlug ? '/clients/' + encodeURIComponent(selectedSlug) + '/commands' : '#';
form.action = commandTargetSlug ? '/clients/' + encodeURIComponent(commandTargetSlug) + '/commands' : '#';
if (command === 'reload') {
form.setAttribute('data-confirm-message', 'Reload selected screen?');
form.setAttribute('data-confirm-message', isAllSelected ? 'Reload all screens?' : 'Reload selected screen?');
if (button) {
button.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen';
button.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>' + (isAllSelected ? 'Reload all screens' : 'Reload screen');
}
}
Array.prototype.slice.call(form.querySelectorAll('button, input')).forEach(function (control) {
control.disabled = !selectedSlug;
control.disabled = !commandTargetSlug;
});
});
}
+49
View File
@@ -11,6 +11,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
const withClientNameReservation = deps.withClientNameReservation;
const broadcastDashboardState = deps.broadcastDashboardState;
const requirePermission = deps.requirePermission;
const ALL_SCREENS_SLUG = '__all__';
app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
try {
@@ -28,6 +29,46 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
return res.status(400).json({ error: 'Command is required' });
}
if (slug === ALL_SCREENS_SLUG) {
if (command === 'setclientname' || command === 'moveclient') {
return res.status(400).json({ error: 'This command requires a specific screen.' });
}
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug IS NOT NULL ORDER BY slug ASC');
if (!screenRows.length) {
return res.status(404).json({ error: 'No screens found' });
}
const commandPayload = req.body && typeof req.body === 'object' && !Array.isArray(req.body)
? Object.assign({}, req.body, { command: command })
: { command: command };
if (command === 'blackout' && blackoutValue !== undefined && commandPayload && typeof commandPayload === 'object') {
commandPayload.blackout = blackoutValue;
}
await Promise.all(screenRows.map(function (screenRow) {
return forwardPlayerCommand(String(screenRow && screenRow.slug || '').trim(), commandPayload);
}));
if (typeof broadcastDashboardState === 'function') {
await broadcastDashboardState();
}
return res.json({
screen: {
id: null,
name: 'All screens',
slug: ALL_SCREENS_SLUG
},
screenSlug: slug,
command: command,
connectionId: connectionId || null,
targetScreenCount: screenRows.length,
ok: true,
allScreens: true
});
}
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug = ?', [slug]);
if (!screenRows.length) {
return res.status(404).json({ error: 'Screen not found' });
@@ -208,7 +249,15 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
}
const status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections);
const liveConnection = Array.isArray(liveConnections)
? liveConnections.find(function (connection) {
const candidateConnectionId = String(connection && (connection.id || connection.clientId) || '').trim();
const candidateDeviceId = String(connection && connection.deviceId || '').trim();
return candidateConnectionId === connectionId || candidateConnectionId === deviceId || candidateDeviceId === deviceId;
})
: null;
const targetBaseUrl = String(
(liveConnection && liveConnection.playerPublicBaseUrl) ||
(typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') ||
''
).trim().replace(/\/$/, '');
@@ -143,6 +143,30 @@ function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
return buildQueryString(nextQuery);
}
function buildTaskPlayerLabel(metadata) {
const playerLabel = String(metadata && metadata.playerLabel || '').trim();
const playerIdentifier = String(metadata && metadata.playerIdentifier || '').trim();
const playerPublicBaseUrl = String(metadata && metadata.playerPublicBaseUrl || '').trim();
if (playerLabel) {
return playerLabel;
}
if (playerIdentifier && playerPublicBaseUrl) {
return `${playerIdentifier} (${playerPublicBaseUrl})`;
}
if (playerIdentifier) {
return playerIdentifier;
}
if (playerPublicBaseUrl) {
return playerPublicBaseUrl;
}
return '';
}
function buildQueuePageViewModel(data, message, currentUser) {
const tasks = (data && data.tasks) || [];
const summary = (data && data.summary) || { counts: {}, total: 0, activeCount: 0 };
@@ -171,7 +195,10 @@ function buildQueuePageViewModel(data, message, currentUser) {
'finishedAt',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
function (item) { return item && item.metadata && item.metadata.sourceId; },
function (item) { return item && item.metadata && item.metadata.playerLabel; },
function (item) { return item && item.metadata && item.metadata.playerIdentifier; },
function (item) { return item && item.metadata && item.metadata.playerPublicBaseUrl; }
])(task);
});
const sortedTasks = sortRows(filteredTasks, function (task) {
@@ -194,7 +221,7 @@ function buildQueuePageViewModel(data, message, currentUser) {
return task && task.finishedAt;
}
if (sort === 'source') {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId].map(function (value) {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId, task && task.metadata && task.metadata.playerLabel, task && task.metadata && task.metadata.playerIdentifier, task && task.metadata && task.metadata.playerPublicBaseUrl].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
@@ -234,11 +261,16 @@ function buildQueuePageViewModel(data, message, currentUser) {
});
const visibleTasksWithSources = visibleTasks.map(function (task) {
const playerLabel = buildTaskPlayerLabel(task.metadata);
const taskDescription = [playerLabel, String(task && task.key || '').trim()].filter(Boolean).join(':');
return Object.assign({}, task, {
sourceUrl: buildTaskSourceFilterUrl(queryState, task, sourceFilter),
sourceLabel: task.metadata && task.metadata.sourceName ? String(task.metadata.sourceName).trim() : '',
sourceTypeLabel: task.metadata && task.metadata.sourceType ? String(task.metadata.sourceType).trim() : '',
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : ''
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : '',
playerLabel: playerLabel,
taskDescription: taskDescription
});
});
const hasActiveFilters = Boolean(queryState.sourceType || queryState.sourceId);
@@ -287,7 +319,10 @@ function buildScheduledPageViewModel(data, message, currentUser) {
'lastError',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
function (item) { return item && item.metadata && item.metadata.sourceId; },
function (item) { return item && item.metadata && item.metadata.playerLabel; },
function (item) { return item && item.metadata && item.metadata.playerIdentifier; },
function (item) { return item && item.metadata && item.metadata.playerPublicBaseUrl; }
])(task);
});
const sortedRecurringTasks = sortRows(filteredRecurringTasks, function (task) {
@@ -309,7 +344,7 @@ function buildScheduledPageViewModel(data, message, currentUser) {
}).join(' ');
}
if (sort === 'source') {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId].map(function (value) {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId, task && task.metadata && task.metadata.playerLabel, task && task.metadata && task.metadata.playerIdentifier, task && task.metadata && task.metadata.playerPublicBaseUrl].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
@@ -325,7 +360,8 @@ function buildScheduledPageViewModel(data, message, currentUser) {
backgroundTasksMenuOpen: true,
recurringTasks: sortedRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
intervalLabel: formatIntervalLabel(task.intervalMs),
playerLabel: buildTaskPlayerLabel(task.metadata)
});
}),
summary: summary,
@@ -65,8 +65,8 @@
<tr data-table-search-row>
<td data-label="Task">
<div class="fw-semibold">{{title}}</div>
{{#if key}}
<div class="text-muted small text-break">{{key}}</div>
{{#if taskDescription}}
<div class="text-muted small text-break">{{taskDescription}}</div>
{{/if}}
{{#if errorMessage}}
<div class="text-danger small mt-1 text-break">{{errorMessage}}</div>
@@ -34,6 +34,9 @@
<tr data-table-search-row>
<td data-label="Task">
<div class="fw-semibold">{{title}}</div>
{{#if playerLabel}}
<div class="text-muted small text-break">Player: {{playerLabel}}</div>
{{/if}}
<div class="text-muted small text-break">{{key}}</div>
</td>
<td data-label="Interval">{{intervalLabel}}</td>
+3 -1
View File
@@ -9,7 +9,7 @@
<div class="card card-outline card-secondary mb-4 screen-command-card">
<div class="card-header">
<div class="dashboard-card-heading">
<h3 class="card-title">Screen-level controls</h3>
<h3 class="card-title">Screen Group controls</h3>
</div>
</div>
<div class="card-body">
@@ -26,6 +26,7 @@
aria-label="Target screen group"
data-screen-command-select
>
<option value="" selected disabled>Select Screen Group</option>
{{#each screens}}
<option
value="{{slug}}"
@@ -34,6 +35,7 @@
data-playlist-name="{{playlist_name}}"
>{{name}}</option>
{{/each}}
<option value="__all__" data-screen-name="All screens" data-screen-target-all="true">All Screens</option>
</select>
</div>
</div>