Add multi-player and remote bridge support

This commit is contained in:
2026-08-07 02:58:18 +01:00
parent a4f8a807ff
commit 74318eb34e
58 changed files with 3785 additions and 432 deletions
@@ -1,16 +1,18 @@
const { resolvePlayerRegistration } = require('#src/data/player-registry');
const TASK = {
taskType: 'slide-thumbnail-refresh'
};
async function fetchPlayerInternalBaseUrl(pool) {
const [rows] = await pool.query(
`SELECT internal_base_url
FROM d_players
WHERE device_id = '1'
LIMIT 1`
);
async function fetchPlayerInternalBaseUrl(pool, configuredPlayerInternalBaseUrl) {
const configured = String(configuredPlayerInternalBaseUrl || '').trim().replace(/\/$/, '');
if (configured) {
return configured;
}
return String(rows && rows[0] && rows[0].internal_base_url || '').trim().replace(/\/$/, '') || null;
const { getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
const player = await resolvePlayerRegistration(pool, getConfiguredPlayerIdentifier());
return String(player && player.internal_base_url || '').trim().replace(/\/$/, '') || null;
}
function registerSlideThumbnailRefreshTask(options) {
@@ -19,6 +21,7 @@ function registerSlideThumbnailRefreshTask(options) {
const pool = options && options.pool;
const common = options && options.common;
const mediaDir = String(options && options.mediaDir || '').trim();
const configuredWebBaseUrl = String(options && options.webBaseUrl || '').trim().replace(/\/$/, '');
if (!backgroundTaskQueue || typeof captureSlideThumbnail !== 'function' || !pool || !common || !mediaDir) {
throw new Error('registerSlideThumbnailRefreshTask requires the slide thumbnail dependencies.');
@@ -32,18 +35,19 @@ function registerSlideThumbnailRefreshTask(options) {
throw new Error('Slide id is required.');
}
const playerInternalBaseUrl = await fetchPlayerInternalBaseUrl(pool);
if (!playerInternalBaseUrl) {
throw new Error('Player internal base URL is required.');
const webBaseUrl = configuredWebBaseUrl || `http://127.0.0.1:${Number(process.env.WEB_PORT || 8080)}`;
if (!webBaseUrl) {
throw new Error('Web base URL is required.');
}
return captureSlideThumbnail({
pool: pool,
common: common,
mediaDir: mediaDir,
baseUrl: playerInternalBaseUrl,
baseUrl: webBaseUrl,
slideId: slideId,
previousThumbnailPath: payload.previousThumbnailPath || null
previousThumbnailPath: payload.previousThumbnailPath || null,
fontStylesheetHref: payload.fontStylesheetHref || ''
});
});
}
@@ -1,16 +1,18 @@
const { resolvePlayerRegistration } = require('#src/data/player-registry');
const TASK = {
taskType: 'template-slide-thumbnail-refresh'
};
async function fetchPlayerInternalBaseUrl(pool) {
const [rows] = await pool.query(
`SELECT internal_base_url
FROM d_players
WHERE device_id = '1'
LIMIT 1`
);
async function fetchPlayerInternalBaseUrl(pool, configuredPlayerInternalBaseUrl) {
const configured = String(configuredPlayerInternalBaseUrl || '').trim().replace(/\/$/, '');
if (configured) {
return configured;
}
return String(rows && rows[0] && rows[0].internal_base_url || '').trim().replace(/\/$/, '') || null;
const { getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
const player = await resolvePlayerRegistration(pool, getConfiguredPlayerIdentifier());
return String(player && player.internal_base_url || '').trim().replace(/\/$/, '') || null;
}
function registerTemplateSlideThumbnailRefreshTask(options) {
@@ -19,6 +21,7 @@ function registerTemplateSlideThumbnailRefreshTask(options) {
const pool = options && options.pool;
const common = options && options.common;
const mediaDir = String(options && options.mediaDir || '').trim();
const configuredWebBaseUrl = String(options && options.webBaseUrl || '').trim().replace(/\/$/, '');
if (!backgroundTaskQueue || typeof captureSlideThumbnail !== 'function' || !pool || !common || !mediaDir) {
throw new Error('registerTemplateSlideThumbnailRefreshTask requires the template thumbnail dependencies.');
@@ -32,9 +35,9 @@ function registerTemplateSlideThumbnailRefreshTask(options) {
throw new Error('Template id is required.');
}
const playerInternalBaseUrl = await fetchPlayerInternalBaseUrl(pool);
if (!playerInternalBaseUrl) {
throw new Error('Player internal base URL is required.');
const webBaseUrl = configuredWebBaseUrl || `http://127.0.0.1:${Number(process.env.WEB_PORT || 8080)}`;
if (!webBaseUrl) {
throw new Error('Web base URL is required.');
}
const [slides] = await pool.query(
@@ -52,7 +55,7 @@ function registerTemplateSlideThumbnailRefreshTask(options) {
pool: pool,
common: common,
mediaDir: mediaDir,
baseUrl: playerInternalBaseUrl,
baseUrl: webBaseUrl,
slideId: slideId,
previousThumbnailPath: slide && slide.thumbnail_path ? slide.thumbnail_path : null
});
+4
View File
@@ -6,6 +6,8 @@ function createWebConfig() {
const thumbnailsDir = path.join(mediaDir, 'thumbnails');
const assetDir = path.join(__dirname, '..', 'public');
const playerInternalBaseUrl = (process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || 'http://player:8081').replace(/\/$/, '');
const thinClientBaseUrl = (process.env.THIN_CLIENT_BASE_URL || 'http://player-bridge:8090').replace(/\/$/, '');
const webBaseUrl = (process.env.WEB_BASE_URL || `http://127.0.0.1:${Number(process.env.WEB_PORT || 8080)}`).replace(/\/$/, '');
const sessionCookieName = 'digital_signage_session';
const sessionMaxAgeDays = Number(process.env.SESSION_MAX_AGE_DAYS || 14);
const sessionMaxAgeMs = (Number.isFinite(sessionMaxAgeDays) && sessionMaxAgeDays > 0 ? sessionMaxAgeDays : 14) * 24 * 60 * 60 * 1000;
@@ -19,6 +21,8 @@ function createWebConfig() {
thumbnailsDir: thumbnailsDir,
assetDir: assetDir,
playerInternalBaseUrl: playerInternalBaseUrl,
thinClientBaseUrl: thinClientBaseUrl,
webBaseUrl: webBaseUrl,
sessionCookieName: sessionCookieName,
sessionMaxAgeMs: sessionMaxAgeMs,
dataSourceStartupRefreshStaggerMs: dataSourceStartupRefreshStaggerMs
+68 -3
View File
@@ -6,6 +6,10 @@ function normalizeClientName(value) {
return String(value || '').trim();
}
function normalizePlayerBaseUrl(value) {
return String(value || '').trim().replace(/\/$/, '');
}
function enrichScreensWithConnections(screens, connectionsBySlug, onboardingNameBySlug, playerUrlsBySlug) {
return (screens || []).map(function (screen) {
const connectionState = connectionsBySlug[screen.slug] || { count: 0, connections: [] };
@@ -18,11 +22,12 @@ function enrichScreensWithConnections(screens, connectionsBySlug, onboardingName
});
}
function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, formatDashboardDate) {
function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, playerIdentifierByBaseUrl, formatDashboardDate) {
return (screens || []).flatMap(function (screen) {
const connectionState = connectionsBySlug[screen.slug] || { count: 0, connections: [] };
return (connectionState.connections || []).map(function (connection) {
const deviceId = String(connection.deviceId || '').trim();
const playerBaseUrl = normalizePlayerBaseUrl(connection.playerPublicBaseUrl);
return Object.assign({}, connection, {
screen_slug: screen.slug,
screen_name: screen.name,
@@ -30,12 +35,55 @@ function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboa
playlist_name: screen.playlist_name || null,
connectedAtLabel: formatDashboardDate(connection.connectedAt),
lastSeenAtLabel: formatDashboardDate(connection.lastSeenAt),
player_url: screen.player_url || String(connection.page || '').trim() || null
player_identifier: playerIdentifierByBaseUrl && playerBaseUrl ? (playerIdentifierByBaseUrl[playerBaseUrl] || null) : null,
player_url: playerBaseUrl || null
});
});
});
}
function isRecentPlayerRegistration(player, staleSeconds) {
const lastSeenAt = player && player.last_seen_at;
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
}
function buildKioskLauncherPlayers(playerRegistrations, staleSeconds) {
return (Array.isArray(playerRegistrations) ? playerRegistrations : [])
.filter(function (player) {
return Boolean(player && String(player.identifier || '').trim() && String(player.public_base_url || '').trim() && isRecentPlayerRegistration(player, staleSeconds));
})
.map(function (player) {
return {
player_identifier: String(player.identifier || '').trim(),
player_url: String(player.public_base_url || '').trim().replace(/\/$/, '')
};
});
}
async function fetchConnectedPlayerCount(pool, connectedPlayerCountStaleSeconds) {
const staleSeconds = Math.max(30, Number(connectedPlayerCountStaleSeconds || 60));
if (!pool || typeof pool.query !== 'function' || !Number.isFinite(staleSeconds)) {
return null;
}
try {
const [rows] = await pool.query(
`SELECT COUNT(*) AS connected_count
FROM d_players
WHERE last_seen_at IS NOT NULL
AND last_seen_at >= DATE_SUB(NOW(), INTERVAL ${staleSeconds} SECOND)`
);
const count = Number(rows && rows[0] && rows[0].connected_count);
return Number.isFinite(count) && count >= 0 ? count : null;
} catch (_error) {
return null;
}
}
function compareScreenNames(left, right) {
const leftName = String(left && left.name || '').trim();
const rightName = String(right && right.name || '').trim();
@@ -51,6 +99,7 @@ function compareScreenNames(left, right) {
function createDashboardStateService(options) {
const pool = options && options.pool;
const common = options && options.common;
const connectedPlayerCountStaleSeconds = options && options.connectedPlayerCountStaleSeconds;
const playerSnapshotCache = options && options.playerSnapshotCache;
const playerSnapshotSockets = options && options.playerSnapshotSockets;
const ensurePlayerSnapshotSubscription = options && options.ensurePlayerSnapshotSubscription;
@@ -63,9 +112,13 @@ function createDashboardStateService(options) {
async function buildDashboardState() {
const data = await common.fetchAdminData(pool);
const screensData = data.screens || [];
const connectedPlayersCount = await fetchConnectedPlayerCount(pool, connectedPlayerCountStaleSeconds);
const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function'
? await common.fetchScreenPlayerUrls(pool)
: {};
const playerRegistrations = typeof common.fetchPlayerRegistrations === 'function'
? await common.fetchPlayerRegistrations(pool)
: [];
screensData.forEach(function (screen) {
ensurePlayerSnapshotSubscription(screen.slug);
});
@@ -91,6 +144,15 @@ function createDashboardStateService(options) {
}
});
const playerIdentifierByBaseUrl = {};
(Array.isArray(playerRegistrations) ? playerRegistrations : []).forEach(function (player) {
const baseUrl = normalizePlayerBaseUrl(player && player.public_base_url);
const identifier = normalizeClientName(player && player.identifier);
if (baseUrl && identifier) {
playerIdentifierByBaseUrl[baseUrl] = identifier;
}
});
const connectionsBySlug = {};
screensData.forEach(function (screen) {
const cached = playerSnapshotCache.get(String(screen.slug || '').trim());
@@ -106,7 +168,8 @@ function createDashboardStateService(options) {
});
})
.sort(compareScreenNames);
const clients = buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, formatDashboardDate);
const clients = buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, playerIdentifierByBaseUrl, formatDashboardDate);
const kioskPlayers = buildKioskLauncherPlayers(playerRegistrations, connectedPlayerCountStaleSeconds);
const playerServiceConnected = Array.from(playerSnapshotSockets.values()).some(function (socket) {
return socket && socket.readyState === WebSocket.OPEN;
});
@@ -115,8 +178,10 @@ function createDashboardStateService(options) {
playlists: data.playlists || [],
screens: screens,
clients: clients,
kioskPlayers: kioskPlayers,
slides: data.slides || [],
playerServiceConnected: playerServiceConnected,
connectedPlayersCount: connectedPlayersCount !== null ? connectedPlayersCount : 0,
connectedClientsCount: screens.reduce(function (total, screen) {
return total + Number(screen.player_connection_count || 0);
}, 0)
+3 -1
View File
@@ -2,6 +2,8 @@
module.exports = {
createUploadSyncService: require('./upload-sync').createUploadSyncService,
captureSlideThumbnail: require('./slide-thumbnails').captureSlideThumbnail,
captureSlideThumbnail: function captureSlideThumbnail(options) {
return require('./slide-thumbnails').captureSlideThumbnail(options);
},
fontLibrary: require('./font-library')
};
@@ -0,0 +1,156 @@
// Pure thumbnail preview payload helpers shared by routes and screenshot capture.
const {
escapeHtml,
renderEditorJsContent,
sanitizeFontFamily,
sanitizeFontSize,
sanitizeTextColor
} = require('#src/player/render-helpers');
function normalizeBaseUrl(baseUrl) {
return String(baseUrl || '').trim().replace(/\/$/, '');
}
function resolveAssetUrl(baseUrl, value) {
const raw = String(value || '').trim();
if (!raw) {
return '';
}
if (/^(?:https?:)?\/\//i.test(raw) || raw.startsWith('data:')) {
return raw;
}
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
if (!normalizedBaseUrl) {
return raw;
}
if (raw.startsWith('/')) {
return normalizedBaseUrl + raw;
}
return normalizedBaseUrl + '/' + raw.replace(/^\/+/, '');
}
function getThumbnailCanvasSize(slide) {
const template = slide && slide.template ? slide.template : null;
return {
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
};
}
function getRegionContent(slide, region) {
const content = slide && slide.content && slide.content[region.region_key] ? slide.content[region.region_key] : {};
return content && typeof content === 'object' ? content : { value: content };
}
function buildThumbnailRegionStyle(region, canvasWidth, canvasHeight) {
const left = Number.isFinite(Number(region && region.x)) && canvasWidth > 0 ? (Number(region.x) / canvasWidth) * 100 : 0;
const top = Number.isFinite(Number(region && region.y)) && canvasHeight > 0 ? (Number(region.y) / canvasHeight) * 100 : 0;
const width = Number.isFinite(Number(region && region.width)) && canvasWidth > 0 ? (Number(region.width) / canvasWidth) * 100 : 0;
const height = Number.isFinite(Number(region && region.height)) && canvasHeight > 0 ? (Number(region.height) / canvasHeight) * 100 : 0;
return 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region && region.z_index || 0) + ';';
}
function hasVisibleContent(html) {
return Boolean(String(html || '').replace(/<[^>]+>/g, '').trim());
}
function buildTextRegionMarkup(region, regionContent) {
const fontFamily = sanitizeFontFamily(regionContent.font_family || region.font_family);
const fontSize = sanitizeFontSize(regionContent.font_size || region.font_size);
const fontColor = sanitizeTextColor(regionContent.font_color || region.font_color);
const renderedBody = renderEditorJsContent(regionContent.value || '');
if (!hasVisibleContent(renderedBody)) {
return '';
}
return '<div class="slide-preview-region slide-preview-text-region" style="' + region.baseStyle + '"><div class="slide-preview-text-content" style="width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;overflow:hidden;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor || '#000000') + ';">' + renderedBody + '</div></div>';
}
function buildRegionInnerHtml(region, regionContent, baseUrl) {
const regionType = String(regionContent.type || region.region_type || 'text').trim().toLowerCase();
const rawValue = regionContent && regionContent.value !== undefined ? regionContent.value : '';
if (regionType === 'image') {
const src = resolveAssetUrl(baseUrl, rawValue);
return src
? '<div class="slide-preview-region slide-preview-image-region" style="' + region.baseStyle + '"><img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'image') + '" /></div>'
: '';
}
if (regionType === 'video') {
const src = resolveAssetUrl(baseUrl, rawValue);
return src
? '<div class="slide-preview-region slide-preview-video-region" style="' + region.baseStyle + '"><video class="slide-preview-video" src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'video') + '" muted playsinline preload="metadata"></video></div>'
: '';
}
if (regionType === 'webpage') {
const src = resolveAssetUrl(baseUrl, rawValue);
return src
? '<div class="slide-preview-region slide-preview-webpage-region" style="' + region.baseStyle + '"><iframe src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'webpage') + '" loading="eager" referrerpolicy="no-referrer" scrolling="no"></iframe></div>'
: '';
}
if (regionType === 'qr-code') {
const src = String(regionContent.qr_preview || '').trim();
const borderRadius = Math.max(0, Math.round(Number(regionContent.qr_border_radius || 0)));
const radiusStyle = borderRadius > 0 ? ' style="border-radius:' + borderRadius + 'px;overflow:hidden;"' : '';
return src
? '<div class="slide-preview-region slide-preview-qr-code-region" style="' + region.baseStyle + radiusStyle + '"><img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'qr code') + '" /></div>'
: '';
}
if (regionType === 'html') {
const html = String(rawValue || '').trim();
return html
? '<div class="slide-preview-region slide-preview-html-region" style="' + region.baseStyle + '"><iframe sandbox="" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="' + escapeHtml(region.label || region.region_key || 'html') + '" loading="eager" scrolling="no"></iframe></div>'
: '';
}
if (regionType === 'rtmp') {
const label = String(rawValue || '').trim() || 'RTMP source';
return '<div class="slide-preview-region slide-preview-rtmp-region" style="' + region.baseStyle + '"><div class="slide-preview-rtmp-placeholder">' + escapeHtml(label) + '</div></div>';
}
return buildTextRegionMarkup(region, regionContent);
}
function buildThumbnailPreviewMarkup(slide, baseUrl) {
const template = slide && slide.template ? slide.template : null;
if (!template) {
return '';
}
const canvasSize = getThumbnailCanvasSize(slide);
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
return (Array.isArray(template.regions) ? template.regions : []).map(function (region) {
const regionContent = getRegionContent(slide, region);
const previewRegion = Object.assign({}, region, {
baseStyle: buildThumbnailRegionStyle(region, canvasSize.width, canvasSize.height),
pixelWidth: Math.max(1, Math.round(Number(region && region.width || 0) || 1)),
pixelHeight: Math.max(1, Math.round(Number(region && region.height || 0) || 1))
});
return buildRegionInnerHtml(previewRegion, regionContent, normalizedBaseUrl);
}).join('');
}
function buildThumbnailPreviewPayload(slide, options) {
const template = slide && slide.template ? slide.template : null;
const canvasSize = getThumbnailCanvasSize(slide);
return {
thumbnailPreview: true,
canvasWidth: canvasSize.width,
canvasHeight: canvasSize.height,
backgroundColor: template && template.background_color ? String(template.background_color) : '#111111',
backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '',
fontStylesheetHref: String(options && options.fontStylesheetHref || '').trim(),
html: buildThumbnailPreviewMarkup(slide, options && options.baseUrl)
};
}
module.exports = {
buildThumbnailPreviewPayload: buildThumbnailPreviewPayload,
buildThumbnailPreviewMarkup: buildThumbnailPreviewMarkup
};
+91 -23
View File
@@ -2,14 +2,6 @@
const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer-core');
const chromiumModule = require('@sparticuz/chromium');
const sharp = require('sharp');
const chromium = chromiumModule && typeof chromiumModule.executablePath === 'function'
? chromiumModule
: chromiumModule && chromiumModule.default && typeof chromiumModule.default.executablePath === 'function'
? chromiumModule.default
: chromiumModule;
const {
escapeHtml,
mediaKind,
@@ -73,6 +65,23 @@ function getRegionContent(slide, region) {
return content && typeof content === 'object' ? content : { value: content };
}
function getThumbnailCanvasSize(slide) {
const template = slide && slide.template ? slide.template : null;
return {
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
};
}
function buildThumbnailRegionStyle(region, canvasWidth, canvasHeight) {
const left = Number.isFinite(Number(region && region.x)) && canvasWidth > 0 ? (Number(region.x) / canvasWidth) * 100 : 0;
const top = Number.isFinite(Number(region && region.y)) && canvasHeight > 0 ? (Number(region.y) / canvasHeight) * 100 : 0;
const width = Number.isFinite(Number(region && region.width)) && canvasWidth > 0 ? (Number(region.width) / canvasWidth) * 100 : 0;
const height = Number.isFinite(Number(region && region.height)) && canvasHeight > 0 ? (Number(region.height) / canvasHeight) * 100 : 0;
return 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region && region.z_index || 0) + ';';
}
function hasVisibleContent(html) {
return Boolean(String(html || '').replace(/<[^>]+>/g, '').trim());
}
@@ -86,7 +95,7 @@ function buildTextRegionMarkup(region, regionContent) {
return '';
}
return '<div class="template-region text" style="' + region.baseStyle + '"><div class="template-region-text-scale" style="width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor || '#000000') + ';">' + renderedBody + '</div></div>';
return '<div class="slide-preview-region slide-preview-text-region" style="' + region.baseStyle + '"><div class="slide-preview-text-content" style="width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;overflow:hidden;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor || '#000000') + ';">' + renderedBody + '</div></div>';
}
function buildRegionInnerHtml(region, regionContent, baseUrl) {
@@ -96,21 +105,21 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) {
if (regionType === 'image') {
const src = resolveAssetUrl(baseUrl, rawValue);
return src
? '<img src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'image') + '" />'
? '<div class="slide-preview-region slide-preview-image-region" style="' + region.baseStyle + '"><img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'image') + '" /></div>'
: '';
}
if (regionType === 'video') {
const src = resolveAssetUrl(baseUrl, rawValue);
return src
? '<video src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'video') + '" muted playsinline preload="metadata"></video>'
? '<div class="slide-preview-region slide-preview-video-region" style="' + region.baseStyle + '"><video class="slide-preview-video" src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'video') + '" muted playsinline preload="metadata"></video></div>'
: '';
}
if (regionType === 'webpage') {
const src = resolveAssetUrl(baseUrl, rawValue);
return src
? '<iframe src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'webpage') + '" loading="eager" referrerpolicy="no-referrer" scrolling="no"></iframe>'
? '<div class="slide-preview-region slide-preview-webpage-region" style="' + region.baseStyle + '"><iframe src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'webpage') + '" loading="eager" referrerpolicy="no-referrer" scrolling="no"></iframe></div>'
: '';
}
@@ -119,26 +128,75 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) {
const borderRadius = Math.max(0, Math.round(Number(regionContent.qr_border_radius || 0)));
const radiusStyle = borderRadius > 0 ? ' style="border-radius:' + borderRadius + 'px;overflow:hidden;"' : '';
return src
? '<img src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'qr code') + '"' + radiusStyle + ' />'
? '<div class="slide-preview-region slide-preview-qr-code-region" style="' + region.baseStyle + radiusStyle + '"><img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'qr code') + '" /></div>'
: '';
}
if (regionType === 'html') {
const html = String(rawValue || '').trim();
return html
? '<iframe sandbox="" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="' + escapeHtml(region.label || region.region_key || 'html') + '" loading="eager" scrolling="no"></iframe>'
? '<div class="slide-preview-region slide-preview-html-region" style="' + region.baseStyle + '"><iframe sandbox="" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="' + escapeHtml(region.label || region.region_key || 'html') + '" loading="eager" scrolling="no"></iframe></div>'
: '';
}
if (regionType === 'rtmp') {
const label = String(rawValue || '').trim() || 'RTMP source';
return '<div class="template-region-rtmp-placeholder">' + escapeHtml(label) + '</div>';
return '<div class="slide-preview-region slide-preview-rtmp-region" style="' + region.baseStyle + '"><div class="slide-preview-rtmp-placeholder">' + escapeHtml(label) + '</div></div>';
}
return buildTextRegionMarkup(region, regionContent);
}
async function loadChromium() {
const chromiumModule = await import('@sparticuz/chromium');
const resolved = chromiumModule && chromiumModule.default ? chromiumModule.default : chromiumModule;
return resolved;
}
async function loadPuppeteer() {
return require('puppeteer-core');
}
function loadSharp() {
return require('sharp');
}
function buildThumbnailPreviewMarkup(slide, baseUrl) {
const template = slide && slide.template ? slide.template : null;
if (!template) {
return '';
}
const canvasSize = getThumbnailCanvasSize(slide);
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
return (Array.isArray(template.regions) ? template.regions : []).map(function (region) {
const regionContent = getRegionContent(slide, region);
const previewRegion = Object.assign({}, region, {
baseStyle: buildThumbnailRegionStyle(region, canvasSize.width, canvasSize.height),
pixelWidth: Math.max(1, Math.round(Number(region && region.width || 0) || 1)),
pixelHeight: Math.max(1, Math.round(Number(region && region.height || 0) || 1))
});
return buildRegionInnerHtml(previewRegion, regionContent, normalizedBaseUrl);
}).join('');
}
function buildThumbnailPreviewPayload(slide, options) {
const template = slide && slide.template ? slide.template : null;
const canvasSize = getThumbnailCanvasSize(slide);
return {
thumbnailPreview: true,
canvasWidth: canvasSize.width,
canvasHeight: canvasSize.height,
backgroundColor: template && template.background_color ? String(template.background_color) : '#111111',
backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '',
fontStylesheetHref: String(options && options.fontStylesheetHref || '').trim(),
html: buildThumbnailPreviewMarkup(slide, options && options.baseUrl)
};
}
async function launchBrowser() {
const puppeteer = await loadPuppeteer();
const chromium = await loadChromium();
let executablePath = SYSTEM_CHROMIUM_PATHS.find(function (candidate) {
return fs.existsSync(candidate);
}) || '';
@@ -178,6 +236,7 @@ async function launchBrowser() {
}
async function captureSlideThumbnail(options) {
const sharp = loadSharp();
const pool = options && options.pool;
const common = options && options.common;
const mediaDir = String(options && options.mediaDir || '').trim();
@@ -194,7 +253,6 @@ async function captureSlideThumbnail(options) {
throw new Error('Slide not found.');
}
const canvasSize = getCanvasSize(slide);
const thumbnailDir = path.join(mediaDir, 'thumbnails');
const thumbnailRelativePath = String(slide.thumbnail_path || '').trim() || '/media/thumbnails/slides/slide-' + slide.id + '.png';
const filePath = path.join(mediaDir, thumbnailRelativePath.replace(/^\/+media\//, ''));
@@ -206,11 +264,11 @@ async function captureSlideThumbnail(options) {
async function waitForThumbnailRender(page) {
await page.waitForFunction(function () {
return document.readyState === 'complete' && Boolean(document.querySelector('.slide-canvas'));
return document.readyState === 'complete' && Boolean(document.querySelector('#popup-preview-canvas'));
}, { timeout: 30000 });
await page.waitForFunction(function () {
var canvas = document.querySelector('.slide-canvas');
var canvas = document.querySelector('#popup-preview-canvas');
if (!canvas) {
return false;
}
@@ -244,18 +302,26 @@ async function captureSlideThumbnail(options) {
try {
const page = await browser.newPage();
try {
const previewPath = '/api/internal/slide-thumbnails/' + slide.id + '/preview';
const previewPath = '/api/internal/slide-thumbnails/' + slide.id + '/popup-preview';
const previewUrl = baseUrl + previewPath;
const previewPayload = buildThumbnailPreviewPayload(slide, {
baseUrl: baseUrl,
fontStylesheetHref: options && options.fontStylesheetHref ? options.fontStylesheetHref : ''
});
await page.setViewport({
width: Math.max(1, Number(previewPayload.canvasWidth || PLAYER_VIEWPORT.width)),
height: Math.max(1, Number(previewPayload.canvasHeight || PLAYER_VIEWPORT.height)),
deviceScaleFactor: 1
});
await page.setExtraHTTPHeaders(createRequestAuthHeaders({
method: 'GET',
pathname: previewPath
}));
await page.setViewport(PLAYER_VIEWPORT);
await page.goto(previewUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await waitForThumbnailRender(page);
const canvas = await page.$('.slide-canvas');
const canvas = await page.$('#popup-preview-canvas');
if (!canvas) {
throw new Error('Player render did not produce a slide canvas.');
throw new Error('Popup preview did not produce a slide canvas.');
}
await canvas.screenshot({ path: fullSizePath });
} finally {
@@ -298,5 +364,7 @@ async function captureSlideThumbnail(options) {
}
module.exports = {
captureSlideThumbnail: captureSlideThumbnail
captureSlideThumbnail: captureSlideThumbnail,
buildThumbnailPreviewPayload: buildThumbnailPreviewPayload,
buildThumbnailPreviewMarkup: buildThumbnailPreviewMarkup
};
+22 -13
View File
@@ -6,6 +6,7 @@ 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');
function normalizeUploadRoot(uploadDir) {
return path.resolve(String(uploadDir || '').trim());
@@ -48,21 +49,20 @@ function createUploadSyncService(options) {
}
try {
const [rows] = await pool.query(
`SELECT internal_base_url
FROM d_players
WHERE device_id = '1'
LIMIT 1`
);
const resolvedBaseUrl = String(rows && rows[0] && rows[0].internal_base_url || '').trim().replace(/\/$/, '');
return resolvedBaseUrl || configuredPlayerInternalBaseUrl || null;
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;
}
} catch (_error) {
return configuredPlayerInternalBaseUrl || null;
}
return configuredPlayerInternalBaseUrl || null;
})().then(function (baseUrl) {
playerInternalBaseUrl = baseUrl || null;
playerInternalBaseUrlPromise = null;
return playerInternalBaseUrl;
return baseUrl || null;
}, function () {
playerInternalBaseUrlPromise = null;
return configuredPlayerInternalBaseUrl || null;
@@ -278,6 +278,11 @@ function createUploadSyncService(options) {
return Boolean(localUploadDir);
}
function isPlayerUnavailableError(error) {
const code = String(error && error.cause && error.cause.code || error && error.code || '').trim().toUpperCase();
return code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'EAI_AGAIN' || code === 'ETIMEDOUT';
}
function queuePlayerUploadSync(operation) {
if (!operation || !operation.uploadPath) {
return;
@@ -350,7 +355,9 @@ function createUploadSyncService(options) {
}
return true;
} catch (error) {
console.warn('Unable to sync upload to player:', relativePath, error);
if (!isPlayerUnavailableError(error)) {
console.warn('Unable to sync upload to player:', relativePath, error);
}
return false;
}
}
@@ -387,7 +394,9 @@ function createUploadSyncService(options) {
}
return true;
} catch (error) {
console.warn('Unable to remove upload from player:', relativePath, error);
if (!isPlayerUnavailableError(error)) {
console.warn('Unable to remove upload from player:', relativePath, error);
}
return false;
}
}
+15 -11
View File
@@ -1,4 +1,5 @@
const { createRequestAuthHeaders } = require('#src/request-auth');
const { getConfiguredPlayerIdentifier, resolvePlayerRegistration } = require('#src/data/player-registry');
function createPlayerActionService(options) {
const pool = options && options.pool;
@@ -13,6 +14,10 @@ function createPlayerActionService(options) {
let playerInternalBaseUrlPromise = null;
async function getPlayerInternalBaseUrl() {
if (configuredPlayerInternalBaseUrl) {
return configuredPlayerInternalBaseUrl;
}
if (playerInternalBaseUrl) {
return playerInternalBaseUrl;
}
@@ -27,21 +32,20 @@ function createPlayerActionService(options) {
}
try {
const [rows] = await pool.query(
`SELECT internal_base_url
FROM d_players
WHERE device_id = '1'
LIMIT 1`
);
const resolvedBaseUrl = String(rows && rows[0] && rows[0].internal_base_url || '').trim().replace(/\/$/, '');
return resolvedBaseUrl || configuredPlayerInternalBaseUrl || null;
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;
}
} catch (_error) {
return configuredPlayerInternalBaseUrl || null;
}
return configuredPlayerInternalBaseUrl || null;
})().then(function (baseUrl) {
playerInternalBaseUrl = baseUrl || null;
playerInternalBaseUrlPromise = null;
return playerInternalBaseUrl;
return baseUrl || null;
}, function () {
playerInternalBaseUrlPromise = null;
return configuredPlayerInternalBaseUrl || null;
+1
View File
@@ -25,6 +25,7 @@ async function initializeWebServer(options) {
uploadSyncService: webBootstrap.uploadSyncService,
captureSlideThumbnail: captureSlideThumbnail,
mediaDir: mediaDir,
webBaseUrl: options && options.webBaseUrl ? options.webBaseUrl : null,
dataSourceStartupRefreshStaggerMs: dataSourceStartupRefreshStaggerMs
});