Release 2.6.7
This commit is contained in:
+238
-58
@@ -44,6 +44,55 @@ function formatPlayerConnectionLabel(deviceId, remoteAddress) {
|
||||
return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId;
|
||||
}
|
||||
|
||||
function normalizeProxyBaseUrl(value) {
|
||||
const normalized = String(value || '').trim().replace(/\/$/, '');
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') {
|
||||
url.hostname = 'host.docker.internal';
|
||||
}
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch (_error) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
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 === 'player-dev'
|
||||
|| host === 'player-local'
|
||||
|| host === 'player-bridge-dev'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function resolveSnapshotUpstreamBaseUrl(player) {
|
||||
const internalBaseUrl = normalizeProxyBaseUrl(player && player.internal_base_url);
|
||||
if (internalBaseUrl && isLocalLikeBaseUrl(internalBaseUrl)) {
|
||||
return internalBaseUrl;
|
||||
}
|
||||
|
||||
return normalizeProxyBaseUrl(player && player.public_base_url) || null;
|
||||
}
|
||||
|
||||
function resolveScreenCommandTargets(slug, playerSockets, screenPlayerDeviceIds) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key || !screenPlayerDeviceIds || typeof screenPlayerDeviceIds.get !== 'function' || !playerSockets || typeof playerSockets.get !== 'function') {
|
||||
@@ -51,24 +100,38 @@ function resolveScreenCommandTargets(slug, playerSockets, screenPlayerDeviceIds)
|
||||
}
|
||||
|
||||
const deviceIds = screenPlayerDeviceIds.get(key);
|
||||
if (!Array.isArray(deviceIds) || !deviceIds.length) {
|
||||
return [];
|
||||
const targets = Array.isArray(deviceIds)
|
||||
? Array.from(new Set(deviceIds.map(function (value) {
|
||||
return normalizeDeviceId(value);
|
||||
}).filter(Boolean))).map(function (deviceId) {
|
||||
const socket = playerSockets.get(deviceId);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { deviceId: deviceId, socket: socket };
|
||||
}).filter(Boolean)
|
||||
: [];
|
||||
if (targets.length) {
|
||||
return targets;
|
||||
}
|
||||
|
||||
return Array.from(new Set(deviceIds.map(function (value) {
|
||||
return normalizeDeviceId(value);
|
||||
}).filter(Boolean))).map(function (deviceId) {
|
||||
const socket = playerSockets.get(deviceId);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
const fallbackTargets = Array.from(playerSockets.values()).filter(function (socket) {
|
||||
return socket && socket.readyState === WebSocket.OPEN;
|
||||
}).map(function (socket) {
|
||||
return {
|
||||
deviceId: String(socket.playerDeviceId || '').trim(),
|
||||
socket: socket
|
||||
};
|
||||
}).filter(function (target) {
|
||||
return Boolean(target.deviceId);
|
||||
});
|
||||
|
||||
return { deviceId: deviceId, socket: socket };
|
||||
}).filter(Boolean);
|
||||
return fallbackTargets.length === 1 ? fallbackTargets : [];
|
||||
}
|
||||
|
||||
function resolveWebBaseUrl(req) {
|
||||
const configuredWebBaseUrl = String(process.env.WEB_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const configuredWebBaseUrl = String(process.env.WEB_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||
if (configuredWebBaseUrl) {
|
||||
return configuredWebBaseUrl;
|
||||
}
|
||||
@@ -118,24 +181,130 @@ async function start() {
|
||||
const screenSnapshotsWs = new WebSocketServer({ noServer: true });
|
||||
const playerSockets = new Map();
|
||||
const screenSnapshotCache = new Map();
|
||||
const screenSnapshotSourcesBySlug = new Map();
|
||||
const screenSnapshotSubscribersBySlug = new Map();
|
||||
const screenPlayerDeviceIds = new Map();
|
||||
const pendingPlayerCommands = new Map();
|
||||
|
||||
function normalizeProxyBaseUrl(value) {
|
||||
const normalized = String(value || '').trim().replace(/\/$/, '');
|
||||
if (!normalized) {
|
||||
return '';
|
||||
function getScreenSnapshotSourceBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') {
|
||||
url.hostname = 'host.docker.internal';
|
||||
}
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch (_error) {
|
||||
return normalized;
|
||||
if (!screenSnapshotSourcesBySlug.has(key)) {
|
||||
screenSnapshotSourcesBySlug.set(key, new Map());
|
||||
}
|
||||
|
||||
return screenSnapshotSourcesBySlug.get(key);
|
||||
}
|
||||
|
||||
function getScreenSnapshotSubscriberBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!screenSnapshotSubscribersBySlug.has(key)) {
|
||||
screenSnapshotSubscribersBySlug.set(key, new Set());
|
||||
}
|
||||
|
||||
return screenSnapshotSubscribersBySlug.get(key);
|
||||
}
|
||||
|
||||
function buildMergedScreenSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const sourceBucket = screenSnapshotSourcesBySlug.get(key);
|
||||
const connections = [];
|
||||
const deviceIds = [];
|
||||
|
||||
if (sourceBucket && typeof sourceBucket.forEach === 'function') {
|
||||
sourceBucket.forEach(function (payload) {
|
||||
if (payload && Array.isArray(payload.connections)) {
|
||||
connections.push.apply(connections, payload.connections);
|
||||
}
|
||||
});
|
||||
sourceBucket.forEach(function (_payload, sourceKey) {
|
||||
deviceIds.push(sourceKey);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
slug: key,
|
||||
count: connections.length,
|
||||
connections: connections,
|
||||
deviceIds: deviceIds
|
||||
};
|
||||
}
|
||||
|
||||
function broadcastScreenSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const snapshot = buildMergedScreenSnapshot(key);
|
||||
storeScreenSnapshot(key, snapshot.connections, snapshot.deviceIds);
|
||||
|
||||
const bucket = screenSnapshotSubscribersBySlug.get(key);
|
||||
if (!bucket || !bucket.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
type: 'snapshot',
|
||||
slug: key,
|
||||
connections: snapshot.connections,
|
||||
sentAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
bucket.forEach(function (socket) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setScreenSnapshotSource(slug, sourceKey, connections) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
if (!key || !normalizedSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bucket = getScreenSnapshotSourceBucket(key);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.set(normalizedSourceKey, {
|
||||
slug: key,
|
||||
connections: Array.isArray(connections) ? connections : []
|
||||
});
|
||||
broadcastScreenSnapshot(key);
|
||||
}
|
||||
|
||||
function clearScreenSnapshotSource(slug, sourceKey) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
const bucket = screenSnapshotSourcesBySlug.get(key);
|
||||
if (!bucket || !normalizedSourceKey || !bucket.has(normalizedSourceKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.delete(normalizedSourceKey);
|
||||
if (!bucket.size) {
|
||||
screenSnapshotSourcesBySlug.delete(key);
|
||||
}
|
||||
|
||||
broadcastScreenSnapshot(key);
|
||||
}
|
||||
|
||||
function clearPlayerSnapshotSources(sourceKey) {
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
if (!normalizedSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(screenSnapshotSourcesBySlug.keys()).forEach(function (slug) {
|
||||
clearScreenSnapshotSource(slug, normalizedSourceKey);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchPlayerSnapshotRegistrations() {
|
||||
@@ -641,6 +810,16 @@ async function start() {
|
||||
|
||||
socket.playerDeviceId = deviceId;
|
||||
|
||||
if (messageType === 'snapshot') {
|
||||
const slug = String(payload.slug || '').trim();
|
||||
if (!slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScreenSnapshotSource(slug, deviceId, Array.isArray(payload.connections) ? payload.connections : []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageType === 'register') {
|
||||
const player = await upsertPlayerRegistration(pool, {
|
||||
deviceId: deviceId,
|
||||
@@ -719,12 +898,14 @@ async function start() {
|
||||
});
|
||||
|
||||
socket.on('close', function () {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
if (removeConnectedPlayerSocket(socket)) {
|
||||
logPlayerDisconnect(socket);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', function () {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
if (removeConnectedPlayerSocket(socket)) {
|
||||
logPlayerDisconnect(socket);
|
||||
}
|
||||
@@ -734,33 +915,18 @@ async function start() {
|
||||
screenSnapshotsWs.on('connection', function (socket, request, slug) {
|
||||
const normalizedSlug = String(slug || '').trim();
|
||||
const upstreamSockets = new Map();
|
||||
const upstreamSnapshots = new Map();
|
||||
const upstreamDeviceIds = new Set();
|
||||
let refreshTimer = null;
|
||||
let closed = false;
|
||||
|
||||
function sendMergedSnapshot() {
|
||||
if (!normalizedSlug || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = [];
|
||||
upstreamSnapshots.forEach(function (payload) {
|
||||
if (payload && Array.isArray(payload.connections)) {
|
||||
connections.push.apply(connections, payload.connections);
|
||||
}
|
||||
});
|
||||
|
||||
storeScreenSnapshot(normalizedSlug, connections, Array.from(upstreamDeviceIds));
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: 'snapshot',
|
||||
slug: normalizedSlug,
|
||||
connections: connections,
|
||||
sentAt: new Date().toISOString()
|
||||
}));
|
||||
const subscriberBucket = getScreenSnapshotSubscriberBucket(normalizedSlug);
|
||||
if (!subscriberBucket) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
subscriberBucket.add(socket);
|
||||
|
||||
function closeUpstreamSockets() {
|
||||
upstreamSockets.forEach(function (upstreamSocket) {
|
||||
try {
|
||||
@@ -769,7 +935,6 @@ async function start() {
|
||||
}
|
||||
});
|
||||
upstreamSockets.clear();
|
||||
upstreamSnapshots.clear();
|
||||
}
|
||||
|
||||
async function refreshUpstreams() {
|
||||
@@ -786,12 +951,19 @@ async function start() {
|
||||
|
||||
const seenKeys = new Set();
|
||||
players.forEach(function (player) {
|
||||
const baseUrl = normalizeProxyBaseUrl(player && player.public_base_url);
|
||||
const sourceKey = String(player && player.identifier || player && player.id || '').trim();
|
||||
const connectedSocket = sourceKey ? playerSockets.get(sourceKey) : null;
|
||||
if (connectedSocket && connectedSocket.readyState === WebSocket.OPEN) {
|
||||
seenKeys.add(sourceKey);
|
||||
upstreamDeviceIds.add(sourceKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = resolveSnapshotUpstreamBaseUrl(player);
|
||||
if (!baseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceKey = String(player && player.identifier || player && player.id || baseUrl);
|
||||
seenKeys.add(sourceKey);
|
||||
upstreamDeviceIds.add(sourceKey);
|
||||
if (upstreamSockets.has(sourceKey)) {
|
||||
@@ -814,20 +986,15 @@ async function start() {
|
||||
if (!payload || payload.type !== 'snapshot' || String(payload.slug || '').trim() !== normalizedSlug) {
|
||||
return;
|
||||
}
|
||||
upstreamSnapshots.set(sourceKey, {
|
||||
slug: normalizedSlug,
|
||||
connections: Array.isArray(payload.connections) ? payload.connections : []
|
||||
});
|
||||
sendMergedSnapshot();
|
||||
setScreenSnapshotSource(normalizedSlug, sourceKey, Array.isArray(payload.connections) ? payload.connections : []);
|
||||
} catch (_error) {
|
||||
}
|
||||
};
|
||||
|
||||
upstreamSocket.onclose = function () {
|
||||
upstreamSockets.delete(sourceKey);
|
||||
upstreamSnapshots.delete(sourceKey);
|
||||
if (!closed) {
|
||||
sendMergedSnapshot();
|
||||
if (!closed && !(playerSockets.get(sourceKey) && playerSockets.get(sourceKey).readyState === WebSocket.OPEN)) {
|
||||
clearScreenSnapshotSource(normalizedSlug, sourceKey);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -852,7 +1019,7 @@ async function start() {
|
||||
}
|
||||
});
|
||||
|
||||
sendMergedSnapshot();
|
||||
broadcastScreenSnapshot(normalizedSlug);
|
||||
}
|
||||
|
||||
refreshUpstreams();
|
||||
@@ -863,6 +1030,10 @@ async function start() {
|
||||
|
||||
socket.on('close', function () {
|
||||
closed = true;
|
||||
subscriberBucket.delete(socket);
|
||||
if (!subscriberBucket.size) {
|
||||
screenSnapshotSubscribersBySlug.delete(normalizedSlug);
|
||||
}
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
@@ -872,6 +1043,10 @@ async function start() {
|
||||
|
||||
socket.on('error', function () {
|
||||
closed = true;
|
||||
subscriberBucket.delete(socket);
|
||||
if (!subscriberBucket.size) {
|
||||
screenSnapshotSubscribersBySlug.delete(normalizedSlug);
|
||||
}
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
@@ -915,7 +1090,12 @@ async function start() {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { start: start, resolveWebBaseUrl: resolveWebBaseUrl, resolveScreenCommandTargets: resolveScreenCommandTargets };
|
||||
module.exports = {
|
||||
start: start,
|
||||
resolveWebBaseUrl: resolveWebBaseUrl,
|
||||
resolveScreenCommandTargets: resolveScreenCommandTargets,
|
||||
resolveSnapshotUpstreamBaseUrl: resolveSnapshotUpstreamBaseUrl
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
start().catch(function (error) {
|
||||
|
||||
+64
-26
@@ -18,21 +18,37 @@ const { getConfiguredPlayerIdentifier, recordPlayerHeartbeat } = require('#src/d
|
||||
// Player runtime, media API, and websocket wiring.
|
||||
async function start() {
|
||||
const app = express();
|
||||
const pool = String(process.env.THIN_CLIENT_BASE_URL || '').trim().replace(/\/$/, '') ? null : common.createPool();
|
||||
const pool = String(process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '') ? null : common.createPool();
|
||||
const PORT = Number(process.env.PLAYER_PORT || 8081);
|
||||
const PLAYER_PUBLIC_BASE_URL = String(process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const THIN_CLIENT_BASE_URL = String(process.env.THIN_CLIENT_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const isRemotePlayer = Boolean(THIN_CLIENT_BASE_URL);
|
||||
const PLAYER_INTERNAL_BASE_URL = String(isRemotePlayer ? THIN_CLIENT_BASE_URL : (process.env.PLAYER_INTERNAL_BASE_URL || PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '')).trim().replace(/\/$/, '');
|
||||
const PLAYER_PUBLIC_URL = String(process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const BRIDGE_PUBLIC_URL = String(process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
const isRemotePlayer = Boolean(BRIDGE_PUBLIC_URL);
|
||||
const PLAYER_INTERNAL_URL = String(isRemotePlayer ? BRIDGE_PUBLIC_URL : (process.env.PLAYER_INTERNAL_URL || PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '')).trim().replace(/\/$/, '');
|
||||
const PLAYER_DEVICE_ID = getConfiguredPlayerIdentifier();
|
||||
const ASSET_DIR = path.join(__dirname, 'player', 'public');
|
||||
const MEDIA_DIR = path.join(__dirname, '..', 'media');
|
||||
const ONBOARDING_QUEUE_FILE = path.join(MEDIA_DIR, 'player-onboarding-queue.json');
|
||||
const DB_SYNC_INTERVAL_MS = Number(process.env.PLAYER_DB_SYNC_INTERVAL_MS || 15000);
|
||||
const onboardingStore = createOnboardingStore(ONBOARDING_QUEUE_FILE);
|
||||
let thinClientSocket = null;
|
||||
const playerRuntime = createPlayerRuntime({
|
||||
pool: pool,
|
||||
normalizeDeviceId: normalizeDeviceId
|
||||
normalizeDeviceId: normalizeDeviceId,
|
||||
notifySnapshot: function (snapshot) {
|
||||
if (!thinClientSocket || thinClientSocket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
thinClientSocket.send(JSON.stringify({
|
||||
type: 'snapshot',
|
||||
deviceId: PLAYER_DEVICE_ID,
|
||||
slug: snapshot && snapshot.slug ? String(snapshot.slug).trim() : '',
|
||||
connections: Array.isArray(snapshot && snapshot.connections) ? snapshot.connections : []
|
||||
}));
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
});
|
||||
const playerPlaylistService = isRemotePlayer
|
||||
? null
|
||||
@@ -59,9 +75,9 @@ async function start() {
|
||||
console.info('[player] startup', {
|
||||
mode: isRemotePlayer ? 'bridge client' : 'local',
|
||||
connected: connectionState && typeof connectionState.connected === 'boolean' ? connectionState.connected : false,
|
||||
publicBaseUrl: PLAYER_PUBLIC_BASE_URL || null,
|
||||
bridgeBaseUrl: PLAYER_INTERNAL_BASE_URL || null,
|
||||
bridgeWebSocketUrl: THIN_CLIENT_BASE_URL ? createThinClientWebSocketUrl() : null
|
||||
publicBaseUrl: PLAYER_PUBLIC_URL || null,
|
||||
bridgeBaseUrl: PLAYER_INTERNAL_URL || null,
|
||||
bridgeWebSocketUrl: BRIDGE_PUBLIC_URL ? createThinClientWebSocketUrl() : null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,7 +99,7 @@ async function start() {
|
||||
}
|
||||
|
||||
async function triggerWebMediaSync() {
|
||||
if (!isRemotePlayer || !THIN_CLIENT_BASE_URL) {
|
||||
if (!isRemotePlayer || !BRIDGE_PUBLIC_URL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -92,7 +108,7 @@ async function start() {
|
||||
method: 'POST',
|
||||
pathname: '/api/internal/sync/player-media'
|
||||
});
|
||||
const response = await fetch(`${THIN_CLIENT_BASE_URL}/api/internal/sync/player-media`, {
|
||||
const response = await fetch(`${BRIDGE_PUBLIC_URL}/api/internal/sync/player-media`, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({
|
||||
Accept: 'application/json'
|
||||
@@ -194,9 +210,9 @@ async function start() {
|
||||
common: common,
|
||||
playerRuntime: playerRuntime,
|
||||
onboardingStore: onboardingStore,
|
||||
playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL,
|
||||
thinClientBaseUrl: THIN_CLIENT_BASE_URL,
|
||||
playerPublicBaseUrl: PLAYER_PUBLIC_URL,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_URL,
|
||||
bridgeBaseUrl: BRIDGE_PUBLIC_URL,
|
||||
playerDeviceId: PLAYER_DEVICE_ID
|
||||
});
|
||||
registerPlayerRoutes(app, {
|
||||
@@ -207,18 +223,18 @@ async function start() {
|
||||
playerRuntime: playerRuntime,
|
||||
playerPlaylistService: playerPlaylistService,
|
||||
rtmpStreamService: rtmpStreamService,
|
||||
playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL,
|
||||
thinClientBaseUrl: THIN_CLIENT_BASE_URL,
|
||||
playerPublicBaseUrl: PLAYER_PUBLIC_URL,
|
||||
playerInternalBaseUrl: PLAYER_INTERNAL_URL,
|
||||
bridgeBaseUrl: BRIDGE_PUBLIC_URL,
|
||||
playerDeviceId: PLAYER_DEVICE_ID
|
||||
});
|
||||
|
||||
function createThinClientWebSocketUrl() {
|
||||
if (!THIN_CLIENT_BASE_URL) {
|
||||
if (!BRIDGE_PUBLIC_URL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return THIN_CLIENT_BASE_URL.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:') + '/ws/players';
|
||||
return BRIDGE_PUBLIC_URL.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:') + '/ws/players';
|
||||
}
|
||||
|
||||
function startThinClientRegistration() {
|
||||
@@ -256,6 +272,23 @@ async function start() {
|
||||
'x-pulse-request-timestamp': timestamp
|
||||
}, authHeaders)
|
||||
});
|
||||
thinClientSocket = socket;
|
||||
|
||||
function sendSnapshot(slug) {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'snapshot',
|
||||
deviceId: PLAYER_DEVICE_ID,
|
||||
slug: String(slug || '').trim(),
|
||||
connections: playerRuntime.snapshotConnections(slug)
|
||||
}));
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('open', function () {
|
||||
logPlayerStartup({
|
||||
@@ -265,10 +298,14 @@ async function start() {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'register',
|
||||
deviceId: PLAYER_DEVICE_ID,
|
||||
publicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
internalBaseUrl: PLAYER_INTERNAL_BASE_URL
|
||||
publicBaseUrl: PLAYER_PUBLIC_URL,
|
||||
internalBaseUrl: PLAYER_INTERNAL_URL
|
||||
}));
|
||||
|
||||
playerRuntime.snapshotSlugs().forEach(function (slug) {
|
||||
sendSnapshot(slug);
|
||||
});
|
||||
|
||||
if (!webMediaSyncCompleted) {
|
||||
triggerWebMediaSync().then(function (success) {
|
||||
webMediaSyncTriggered = Boolean(success);
|
||||
@@ -285,8 +322,8 @@ async function start() {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'heartbeat',
|
||||
deviceId: PLAYER_DEVICE_ID,
|
||||
publicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
internalBaseUrl: PLAYER_INTERNAL_BASE_URL
|
||||
publicBaseUrl: PLAYER_PUBLIC_URL,
|
||||
internalBaseUrl: PLAYER_INTERNAL_URL
|
||||
}));
|
||||
|
||||
if (!webMediaSyncTriggered && !webMediaSyncCompleted) {
|
||||
@@ -317,6 +354,7 @@ async function start() {
|
||||
|
||||
socket.on('close', function () {
|
||||
clearTimers();
|
||||
thinClientSocket = null;
|
||||
reconnectTimer = setTimeout(connect, 5000);
|
||||
});
|
||||
|
||||
@@ -363,8 +401,8 @@ async function start() {
|
||||
try {
|
||||
await recordPlayerHeartbeat(pool, {
|
||||
deviceId: PLAYER_DEVICE_ID,
|
||||
publicBaseUrl: PLAYER_PUBLIC_BASE_URL,
|
||||
internalBaseUrl: PLAYER_INTERNAL_BASE_URL
|
||||
publicBaseUrl: PLAYER_PUBLIC_URL,
|
||||
internalBaseUrl: PLAYER_INTERNAL_URL
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
@@ -392,7 +430,7 @@ async function start() {
|
||||
|
||||
if (PLAYER_DEVICE_ID && !isRemotePlayer) {
|
||||
const { upsertPlayerRegistration } = require('./player/onboarding');
|
||||
await upsertPlayerRegistration(pool, PLAYER_DEVICE_ID, PLAYER_PUBLIC_BASE_URL, PLAYER_INTERNAL_BASE_URL).catch(function (error) {
|
||||
await upsertPlayerRegistration(pool, PLAYER_DEVICE_ID, PLAYER_PUBLIC_URL, PLAYER_INTERNAL_URL).catch(function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ function normalizeDeviceId(value) {
|
||||
}
|
||||
|
||||
function getPublicBaseUrl(req, configuredUrl) {
|
||||
const configured = String(configuredUrl || process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const configured = String(configuredUrl || process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
if (configured) {
|
||||
return configured;
|
||||
}
|
||||
@@ -50,7 +50,7 @@ function getPlayerPublicBaseUrl(req, configuredUrl) {
|
||||
}
|
||||
|
||||
function getPlayerInternalBaseUrl(configuredUrl) {
|
||||
const configured = String(configuredUrl || process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const configured = String(configuredUrl || process.env.PLAYER_INTERNAL_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
return configured || null;
|
||||
}
|
||||
|
||||
@@ -203,22 +203,22 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
const common = options && options.common ? options.common : null;
|
||||
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
||||
const onboardingStore = options && options.onboardingStore ? options.onboardingStore : null;
|
||||
const playerPublicBaseUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const thinClientBaseUrl = String(options && options.thinClientBaseUrl || process.env.THIN_CLIENT_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const playerPublicUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
const playerDeviceId = normalizeDeviceId(options && options.playerDeviceId);
|
||||
|
||||
if (!app || !common) {
|
||||
throw new Error('registerPlayerOnboardingRoutes requires app and common.');
|
||||
}
|
||||
|
||||
if (!thinClientBaseUrl && (!pool || !playerRuntime)) {
|
||||
throw new Error('registerPlayerOnboardingRoutes requires pool and playerRuntime unless thinClientBaseUrl is configured.');
|
||||
if (!bridgeBaseUrl && (!pool || !playerRuntime)) {
|
||||
throw new Error('registerPlayerOnboardingRoutes requires pool and playerRuntime unless bridgeBaseUrl is configured.');
|
||||
}
|
||||
|
||||
const sharedSecret = getSharedSecret();
|
||||
|
||||
async function fetchThinClient(req, pathname, options) {
|
||||
if (!thinClientBaseUrl) {
|
||||
if (!bridgeBaseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
headers['content-type'] = requestOptions.contentType;
|
||||
}
|
||||
|
||||
return fetch(new URL(pathname, thinClientBaseUrl).toString(), {
|
||||
return fetch(new URL(pathname, bridgeBaseUrl).toString(), {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: body === undefined || body === null || method === 'GET' || method === 'HEAD' ? undefined : body
|
||||
@@ -305,7 +305,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
}, async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId) || playerDeviceId;
|
||||
if (thinClientBaseUrl) {
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchThinClient(req, '/api/onboarding/status?deviceId=' + encodeURIComponent(deviceId || ''), {
|
||||
method: 'GET'
|
||||
});
|
||||
@@ -317,7 +317,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
if (!payload) {
|
||||
return res.status(502).json({ error: 'Player bridge returned an invalid response.' });
|
||||
}
|
||||
payload.playerUrl = payload && payload.screenSlug ? `${getPublicBaseUrl(req, playerPublicBaseUrl)}/screen/${encodeURIComponent(payload.screenSlug)}` : null;
|
||||
payload.playerUrl = payload && payload.screenSlug ? `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(payload.screenSlug)}` : null;
|
||||
return res.json(payload);
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
screenId: status ? status.screen_id : null,
|
||||
screenSlug: status ? status.screen_slug : null,
|
||||
screenName: status ? status.screen_name : null,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req, playerPublicBaseUrl)}/screen/${encodeURIComponent(status.screen_slug)}` : null
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(status.screen_slug)}` : null
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -338,7 +338,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
|
||||
app.get('/api/onboarding/screens', requireOnboardingPageAuth, async function (_req, res, next) {
|
||||
try {
|
||||
if (thinClientBaseUrl) {
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchThinClient(_req, '/api/onboarding/screens', {
|
||||
method: 'GET'
|
||||
});
|
||||
@@ -361,7 +361,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID is required' });
|
||||
}
|
||||
const onboardingUrl = `${getPublicBaseUrl(req, playerPublicBaseUrl)}/onboard?deviceId=${encodeURIComponent(deviceId)}`;
|
||||
const onboardingUrl = `${getPublicBaseUrl(req, playerPublicUrl)}/onboard?deviceId=${encodeURIComponent(deviceId)}`;
|
||||
const svg = await createStyledQrCodeSvg({ value: onboardingUrl, qr_margin: 20 });
|
||||
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
@@ -382,11 +382,11 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
return res.status(429).json({ error: 'Too many onboarding attempts. Please try again later.' });
|
||||
}
|
||||
|
||||
if (thinClientBaseUrl) {
|
||||
if (bridgeBaseUrl) {
|
||||
const forwardedBody = Object.assign({}, req.body || {}, {
|
||||
deviceId: deviceId
|
||||
});
|
||||
const response = await fetch(new URL('/api/onboarding', thinClientBaseUrl).toString(), {
|
||||
const response = await fetch(new URL('/api/onboarding', bridgeBaseUrl).toString(), {
|
||||
method: 'POST',
|
||||
headers: Object.assign({
|
||||
'content-type': 'application/json'
|
||||
@@ -402,7 +402,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
if (!payload) {
|
||||
return res.status(502).json({ error: 'Player bridge returned an invalid response.' });
|
||||
}
|
||||
payload.playerUrl = payload && payload.screenSlug ? `${getPublicBaseUrl(req, playerPublicBaseUrl)}/screen/${encodeURIComponent(payload.screenSlug)}` : `${getPublicBaseUrl(req, playerPublicBaseUrl)}/screen/${encodeURIComponent(screenSlug)}`;
|
||||
payload.playerUrl = payload && payload.screenSlug ? `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(payload.screenSlug)}` : `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(screenSlug)}`;
|
||||
return res.json(payload);
|
||||
}
|
||||
if (!deviceId) {
|
||||
@@ -423,7 +423,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
screenId: status && status.screen_id ? status.screen_id : null,
|
||||
screenSlug: status ? status.screen_slug : screenSlug,
|
||||
screenName: status && status.screen_name ? status.screen_name : null,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req, playerPublicBaseUrl)}/screen/${encodeURIComponent(status.screen_slug)}` : `${getPublicBaseUrl(req, playerPublicBaseUrl)}/screen/${encodeURIComponent(screenSlug)}`,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(status.screen_slug)}` : `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(screenSlug)}`,
|
||||
queued: Boolean(status && status.queued)
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
var form = document.getElementById("onboarding-form");
|
||||
var message = document.getElementById("onboarding-message");
|
||||
var screenSelect = document.getElementById("onboarding-screen-select");
|
||||
function getSessionStorageItem(key) {
|
||||
try { return window.sessionStorage.getItem(key) || ""; } catch (_error) { return ""; }
|
||||
}
|
||||
function setSessionStorageItem(key, value) {
|
||||
try { window.sessionStorage.setItem(key, value); } catch (_error) {}
|
||||
}
|
||||
function setMessage(value) { if (message) { message.textContent = value || ""; } }
|
||||
function parseResponseError(response) {
|
||||
return response.text().then(function (text) {
|
||||
@@ -41,15 +47,15 @@
|
||||
}
|
||||
try {
|
||||
if (!deviceId) {
|
||||
deviceId = window.localStorage.getItem(deviceKey) || "";
|
||||
deviceId = window.sessionStorage.getItem(deviceKey) || "";
|
||||
}
|
||||
if (deviceId) {
|
||||
window.localStorage.setItem(deviceKey, deviceId);
|
||||
window.sessionStorage.setItem(deviceKey, deviceId);
|
||||
}
|
||||
} catch (_error) {}
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedClientName = window.localStorage.getItem(clientNameKey) || "";
|
||||
var storedClientName = getSessionStorageItem(clientNameKey) || "";
|
||||
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
|
||||
var clientNameInput = form.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput && storedClientName) { clientNameInput.value = storedClientName; }
|
||||
@@ -83,7 +89,7 @@
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.screenSlug) { throw new Error("Unable to save onboarding."); }
|
||||
if (payload.clientName) { try { window.localStorage.setItem(clientNameKey, payload.clientName); } catch (_error) {} }
|
||||
if (payload.clientName) { setSessionStorageItem(clientNameKey, payload.clientName); }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
setMessage("Onboarding complete.");
|
||||
if (form) {
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
(function () {
|
||||
var deviceKey = "pulse-signage-player-device-id";
|
||||
var clientNameKey = "pulse-signage-player-client-name";
|
||||
function getSessionStorageItem(key) {
|
||||
try { return window.sessionStorage.getItem(key) || ""; } catch (_error) { return ""; }
|
||||
}
|
||||
function setSessionStorageItem(key, value) {
|
||||
try { window.sessionStorage.setItem(key, value); } catch (_error) {}
|
||||
}
|
||||
function getClientNameStorageKey(_screenSlug) {
|
||||
return clientNameKey;
|
||||
}
|
||||
@@ -25,10 +31,10 @@
|
||||
}
|
||||
function getDeviceId() {
|
||||
var stored = "";
|
||||
try { stored = window.localStorage.getItem(deviceKey) || ""; } catch (_error) { stored = ""; }
|
||||
try { stored = window.sessionStorage.getItem(deviceKey) || ""; } catch (_error) { stored = ""; }
|
||||
if (stored) { return stored; }
|
||||
var next = (window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : "device-" + Date.now() + "-" + Math.random().toString(16).slice(2));
|
||||
try { window.localStorage.setItem(deviceKey, next); } catch (_error2) {}
|
||||
try { window.sessionStorage.setItem(deviceKey, next); } catch (_error2) {}
|
||||
return next;
|
||||
}
|
||||
function setStatus(message) { if (status) { status.textContent = message; } }
|
||||
@@ -83,8 +89,8 @@
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.screenSlug) { throw new Error("Unable to save onboarding."); }
|
||||
if (payload.clientName) { try { window.localStorage.setItem(clientNameKey, payload.clientName); } catch (_error) {} }
|
||||
if (payload.clientName && payload.screenSlug) { try { window.localStorage.setItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); } catch (_error2) {} }
|
||||
if (payload.clientName) { setSessionStorageItem(clientNameKey, payload.clientName); }
|
||||
if (payload.clientName && payload.screenSlug) { setSessionStorageItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
setLocalMessage("Onboarding complete.");
|
||||
if (localForm) {
|
||||
@@ -99,8 +105,8 @@
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
if (payload && payload.onboarded && payload.screenSlug) {
|
||||
if (payload.clientName) { try { window.localStorage.setItem(clientNameKey, payload.clientName); } catch (_error) {} }
|
||||
if (payload.clientName && payload.screenSlug) { try { window.localStorage.setItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); } catch (_error2) {} }
|
||||
if (payload.clientName) { setSessionStorageItem(clientNameKey, payload.clientName); }
|
||||
if (payload.clientName && payload.screenSlug) { setSessionStorageItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
window.location.replace("/screen/" + encodeURIComponent(payload.screenSlug));
|
||||
return true;
|
||||
@@ -127,8 +133,8 @@
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedScreenSlug = window.localStorage.getItem(screenKey) || "";
|
||||
var storedClientName = window.localStorage.getItem(clientNameKey) || "";
|
||||
if (!storedClientName && storedScreenSlug) { storedClientName = window.localStorage.getItem(getClientNameStorageKey(storedScreenSlug)) || ""; }
|
||||
var storedClientName = getSessionStorageItem(clientNameKey) || "";
|
||||
if (!storedClientName && storedScreenSlug) { storedClientName = getSessionStorageItem(getClientNameStorageKey(storedScreenSlug)) || ""; }
|
||||
if (storedClientName && localForm) {
|
||||
var clientNameInput = localForm.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput) { clientNameInput.value = storedClientName; }
|
||||
|
||||
@@ -4,9 +4,25 @@
|
||||
const onboardingClientNameStorageKey = 'pulse-signage-player-client-name';
|
||||
const onboardingDeviceIdStorageKey = 'pulse-signage-player-device-id';
|
||||
|
||||
function getSessionStorageItem(key) {
|
||||
try {
|
||||
return window.sessionStorage.getItem(key) || '';
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionStorageItem(key, value) {
|
||||
try {
|
||||
window.sessionStorage.setItem(key, value);
|
||||
} catch (_error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
function getOnboardingDeviceId() {
|
||||
try {
|
||||
var storedDeviceId = window.localStorage.getItem(onboardingDeviceIdStorageKey) || '';
|
||||
var storedDeviceId = getSessionStorageItem(onboardingDeviceIdStorageKey);
|
||||
return String(storedDeviceId || '').trim();
|
||||
} catch (_error) {
|
||||
return '';
|
||||
@@ -19,24 +35,14 @@
|
||||
return onboardingClientName;
|
||||
}
|
||||
try {
|
||||
var storedClientName = window.localStorage.getItem(onboardingClientNameStorageKey);
|
||||
var storedClientName = getSessionStorageItem(onboardingClientNameStorageKey);
|
||||
if (storedClientName) {
|
||||
onboardingClientName = storedClientName;
|
||||
try {
|
||||
window.localStorage.setItem('pulse-signage-player-client-name', storedClientName);
|
||||
} catch (_mirrorError) {
|
||||
// ignore storage errors
|
||||
}
|
||||
return onboardingClientName;
|
||||
}
|
||||
var genericClientName = window.localStorage.getItem('pulse-signage-player-client-name');
|
||||
var genericClientName = getSessionStorageItem('pulse-signage-player-client-name');
|
||||
if (genericClientName) {
|
||||
onboardingClientName = genericClientName;
|
||||
try {
|
||||
window.localStorage.setItem(onboardingClientNameStorageKey, genericClientName);
|
||||
} catch (_error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
return onboardingClientName;
|
||||
}
|
||||
} catch (_error) {
|
||||
@@ -51,12 +57,8 @@
|
||||
return;
|
||||
}
|
||||
onboardingClientName = normalizedName;
|
||||
try {
|
||||
window.localStorage.setItem('pulse-signage-player-client-name', normalizedName);
|
||||
window.localStorage.setItem(onboardingClientNameStorageKey, normalizedName);
|
||||
} catch (_error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
setSessionStorageItem('pulse-signage-player-client-name', normalizedName);
|
||||
setSessionStorageItem(onboardingClientNameStorageKey, normalizedName);
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
sendCommandState(socket);
|
||||
}
|
||||
|
||||
@@ -188,13 +188,13 @@ function syncWebpagePreloads(sourceSlides, targetIndex) {
|
||||
preloadSignature = signature;
|
||||
}
|
||||
|
||||
// Return a stable client id for this browser session.
|
||||
// Return a stable client id for this screen session.
|
||||
function getCommandClientId() {
|
||||
if (commandClientId) {
|
||||
return commandClientId;
|
||||
}
|
||||
try {
|
||||
var storedClientId = window.localStorage.getItem(commandClientStorageKey);
|
||||
var storedClientId = window.sessionStorage.getItem(commandClientStorageKey);
|
||||
if (storedClientId) {
|
||||
commandClientId = storedClientId;
|
||||
return commandClientId;
|
||||
@@ -204,7 +204,7 @@ function getCommandClientId() {
|
||||
}
|
||||
commandClientId = (window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : 'client-' + Date.now() + '-' + Math.random().toString(16).slice(2));
|
||||
try {
|
||||
window.localStorage.setItem(commandClientStorageKey, commandClientId);
|
||||
window.sessionStorage.setItem(commandClientStorageKey, commandClientId);
|
||||
} catch (_error2) {
|
||||
// ignore storage errors
|
||||
}
|
||||
|
||||
+18
-18
@@ -31,23 +31,23 @@ function registerPlayerRoutes(app, options) {
|
||||
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
||||
const playerPlaylistService = options && options.playerPlaylistService ? options.playerPlaylistService : null;
|
||||
const rtmpStreamService = options && options.rtmpStreamService ? options.rtmpStreamService : null;
|
||||
const playerPublicBaseUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const playerInternalBaseUrl = String(options && options.playerInternalBaseUrl || process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const thinClientBaseUrl = String(options && options.thinClientBaseUrl || process.env.THIN_CLIENT_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const playerPublicUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const playerInternalUrl = String(options && options.playerInternalBaseUrl || process.env.PLAYER_INTERNAL_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
const playerDeviceId = String(options && options.playerDeviceId || '').trim() || null;
|
||||
|
||||
if (!app || !common || !mediaDir || !assetDir || !playerRuntime || !rtmpStreamService) {
|
||||
throw new Error('registerPlayerRoutes requires app, common, mediaDir, assetDir, playerRuntime, and rtmpStreamService.');
|
||||
}
|
||||
|
||||
if (!thinClientBaseUrl && (!pool || !playerPlaylistService)) {
|
||||
throw new Error('registerPlayerRoutes requires pool and playerPlaylistService unless thinClientBaseUrl is configured.');
|
||||
if (!bridgeBaseUrl && (!pool || !playerPlaylistService)) {
|
||||
throw new Error('registerPlayerRoutes requires pool and playerPlaylistService unless bridgeBaseUrl is configured.');
|
||||
}
|
||||
|
||||
const sharedSecret = getSharedSecret();
|
||||
|
||||
async function fetchThinClient(req, pathname, options) {
|
||||
if (!thinClientBaseUrl) {
|
||||
async function fetchBridge(req, pathname, options) {
|
||||
if (!bridgeBaseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ function registerPlayerRoutes(app, options) {
|
||||
headers['content-type'] = requestOptions.contentType;
|
||||
}
|
||||
|
||||
return fetch(new URL(pathname, thinClientBaseUrl).toString(), {
|
||||
return fetch(new URL(pathname, bridgeBaseUrl).toString(), {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: body === undefined || body === null || method === 'GET' || method === 'HEAD' ? undefined : body
|
||||
@@ -179,8 +179,8 @@ function registerPlayerRoutes(app, options) {
|
||||
});
|
||||
|
||||
app.get('/api/media/config', requireRequestAuth, function (_req, res) {
|
||||
if (thinClientBaseUrl) {
|
||||
void fetch(new URL('/api/media/config', thinClientBaseUrl).toString(), {
|
||||
if (bridgeBaseUrl) {
|
||||
void fetch(new URL('/api/media/config', bridgeBaseUrl).toString(), {
|
||||
method: 'GET',
|
||||
headers: createRequestAuthHeaders({
|
||||
method: 'GET',
|
||||
@@ -298,9 +298,9 @@ function registerPlayerRoutes(app, options) {
|
||||
app.get('/screen/:slug', function (req, res) {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
res.set('Pragma', 'no-cache');
|
||||
if (thinClientBaseUrl) {
|
||||
if (bridgeBaseUrl) {
|
||||
const pageAuthToken = createPageAuthBundle({ scope: 'player', slug: String(req.params.slug || '').trim() }).token;
|
||||
void fetchThinClient(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/playlist?ts=' + Date.now(), {
|
||||
void fetchBridge(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/playlist?ts=' + Date.now(), {
|
||||
method: 'GET',
|
||||
headers: pageAuthToken ? { 'x-pulse-page-auth': pageAuthToken } : {}
|
||||
}).then(async function (response) {
|
||||
@@ -342,8 +342,8 @@ function registerPlayerRoutes(app, options) {
|
||||
|
||||
app.get('/api/internal/slide-thumbnails/:id/preview', requireRequestAuth, async function (req, res, next) {
|
||||
try {
|
||||
if (thinClientBaseUrl) {
|
||||
const response = await fetchThinClient(req, '/api/internal/slide-thumbnails/' + encodeURIComponent(req.params.id) + '/preview', {
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchBridge(req, '/api/internal/slide-thumbnails/' + encodeURIComponent(req.params.id) + '/preview', {
|
||||
method: 'GET'
|
||||
});
|
||||
if (!response) {
|
||||
@@ -397,8 +397,8 @@ function registerPlayerRoutes(app, options) {
|
||||
|
||||
app.get('/api/screens/:slug/playlist', requirePageAuth(['player']), async function (req, res, next) {
|
||||
try {
|
||||
if (thinClientBaseUrl) {
|
||||
const response = await fetchThinClient(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/playlist', {
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchBridge(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/playlist', {
|
||||
method: 'GET'
|
||||
});
|
||||
if (!response) {
|
||||
@@ -440,8 +440,8 @@ function registerPlayerRoutes(app, options) {
|
||||
|
||||
app.get('/api/screens/:slug/announcement', requirePageAuth(['player']), async function (req, res, next) {
|
||||
try {
|
||||
if (thinClientBaseUrl) {
|
||||
const response = await fetchThinClient(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/announcement', {
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchBridge(req, '/api/screens/' + encodeURIComponent(req.params.slug) + '/announcement', {
|
||||
method: 'GET'
|
||||
});
|
||||
if (!response) {
|
||||
|
||||
+17
-1
@@ -21,6 +21,7 @@ function normalizePlayerPublicBaseUrl(pageUrl) {
|
||||
|
||||
function createPlayerRuntime(options) {
|
||||
const pool = options && options.pool ? options.pool : null;
|
||||
const notifySnapshot = typeof options.notifySnapshot === 'function' ? options.notifySnapshot : null;
|
||||
const normalizeDeviceId = typeof options.normalizeDeviceId === 'function'
|
||||
? options.normalizeDeviceId
|
||||
: function (value) {
|
||||
@@ -222,6 +223,10 @@ function createPlayerRuntime(options) {
|
||||
return allConnections;
|
||||
}
|
||||
|
||||
function snapshotSlugs() {
|
||||
return Array.from(connectionsBySlug.keys());
|
||||
}
|
||||
|
||||
async function isClientNameAvailableOnScreen(poolArg, clientName, excludeDeviceId) {
|
||||
return isClientNameAvailable(poolArg || pool, clientName, excludeDeviceId, snapshotAllConnections());
|
||||
}
|
||||
@@ -229,6 +234,16 @@ function createPlayerRuntime(options) {
|
||||
function broadcastConnectionSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const bucket = dashboardListenersBySlug.get(key);
|
||||
const connections = snapshotConnections(slug);
|
||||
if (notifySnapshot) {
|
||||
try {
|
||||
notifySnapshot({
|
||||
slug: key,
|
||||
connections: connections
|
||||
});
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
if (!bucket || !bucket.size) {
|
||||
return;
|
||||
}
|
||||
@@ -236,7 +251,7 @@ function createPlayerRuntime(options) {
|
||||
const payload = JSON.stringify({
|
||||
type: 'snapshot',
|
||||
slug: key,
|
||||
connections: snapshotConnections(slug),
|
||||
connections: connections,
|
||||
sentAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
@@ -508,6 +523,7 @@ function createPlayerRuntime(options) {
|
||||
broadcastAnnouncementRefresh: broadcastAnnouncementRefresh,
|
||||
snapshotConnections: snapshotConnections,
|
||||
snapshotAllConnections: snapshotAllConnections,
|
||||
snapshotSlugs: snapshotSlugs,
|
||||
isClientNameAvailableOnScreen: isClientNameAvailableOnScreen,
|
||||
sendCommandToConnection: sendCommandToConnection,
|
||||
broadcastCommand: broadcastCommand
|
||||
|
||||
+5
-4
@@ -62,7 +62,8 @@ async function start() {
|
||||
const playerActionService = createPlayerActionService({
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerInternalBaseUrl: webConfig.thinClientBaseUrl
|
||||
playerInternalBaseUrl: webConfig.playerInternalUrl,
|
||||
bridgeInternalBaseUrl: webConfig.bridgeInternalUrl
|
||||
});
|
||||
const notifyPlayerScreens = createNotifyPlayerScreens(playerActionService.forwardPlayerCommand);
|
||||
|
||||
@@ -70,8 +71,8 @@ async function start() {
|
||||
const webBootstrap = createWebBootstrap({
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerInternalBaseUrl: webConfig.playerInternalBaseUrl,
|
||||
thinClientBaseUrl: webConfig.thinClientBaseUrl,
|
||||
playerInternalBaseUrl: webConfig.playerInternalUrl,
|
||||
bridgeInternalBaseUrl: webConfig.bridgeInternalUrl,
|
||||
uploadDir: webConfig.uploadsDir,
|
||||
formatDashboardDate: formatDashboardDate,
|
||||
notifyPlayerScreens: notifyPlayerScreens,
|
||||
@@ -210,7 +211,7 @@ async function start() {
|
||||
initializeBackgroundTasks: initializeBackgroundTasks,
|
||||
captureSlideThumbnail: captureSlideThumbnail,
|
||||
server: server,
|
||||
webBaseUrl: webConfig.webBaseUrl,
|
||||
webBaseUrl: webConfig.webInternalUrl,
|
||||
dataSourceStartupRefreshStaggerMs: webConfig.dataSourceStartupRefreshStaggerMs
|
||||
});
|
||||
|
||||
|
||||
Vendored
+6
-6
@@ -8,8 +8,8 @@ const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
function createWebBootstrap(options) {
|
||||
const pool = options && options.pool;
|
||||
const common = options && options.common;
|
||||
const configuredPlayerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||
const configuredThinClientBaseUrl = String(options && options.thinClientBaseUrl || process.env.THIN_CLIENT_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const configuredPlayerInternalUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||
const configuredBridgeInternalUrl = String(options && options.bridgeInternalBaseUrl || process.env.BRIDGE_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||
const uploadDir = String(options && options.uploadDir || '').trim();
|
||||
const dashboardRefreshIntervalMs = 5000;
|
||||
const formatDashboardDate = options && options.formatDashboardDate;
|
||||
@@ -27,7 +27,7 @@ function createWebBootstrap(options) {
|
||||
let dashboardRefreshInFlight = null;
|
||||
let broadcastDashboardState = null;
|
||||
function getPlayerSnapshotSocketUrl(slug) {
|
||||
const resolvedPlayerInternalBaseUrl = configuredThinClientBaseUrl || configuredPlayerInternalBaseUrl;
|
||||
const resolvedPlayerInternalBaseUrl = configuredBridgeInternalUrl || configuredPlayerInternalUrl;
|
||||
if (!resolvedPlayerInternalBaseUrl) {
|
||||
throw new Error('Unable to resolve the player internal base URL.');
|
||||
}
|
||||
@@ -109,7 +109,7 @@ function createWebBootstrap(options) {
|
||||
const dashboardStateService = createDashboardStateService({
|
||||
pool: pool,
|
||||
common: common,
|
||||
thinClientBaseUrl: configuredThinClientBaseUrl,
|
||||
thinClientBaseUrl: configuredBridgeInternalUrl,
|
||||
playerSnapshotCache: playerSnapshotCache,
|
||||
playerSnapshotSockets: playerSnapshotSockets,
|
||||
ensurePlayerSnapshotSubscription: ensurePlayerSnapshotSubscription,
|
||||
@@ -120,7 +120,7 @@ function createWebBootstrap(options) {
|
||||
const uploadSyncService = createUploadSyncService({
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerInternalBaseUrl: configuredPlayerInternalBaseUrl,
|
||||
playerInternalBaseUrl: configuredPlayerInternalUrl,
|
||||
playerSnapshotCache: playerSnapshotCache,
|
||||
notifyPlayerScreens: notifyPlayerScreens,
|
||||
backgroundTaskQueue: backgroundTaskQueue
|
||||
@@ -253,7 +253,7 @@ function createWebBootstrap(options) {
|
||||
return {
|
||||
upload: upload,
|
||||
uploadSyncService: uploadSyncService,
|
||||
playerInternalBaseUrl: configuredPlayerInternalBaseUrl || null,
|
||||
playerInternalBaseUrl: configuredPlayerInternalUrl || null,
|
||||
buildDashboardState: buildDashboardState,
|
||||
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
|
||||
collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate,
|
||||
|
||||
@@ -18,57 +18,50 @@ 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.');
|
||||
}
|
||||
|
||||
const metadataPromise = typeof getPlayerTaskMetadata === 'function'
|
||||
? Promise.resolve(getPlayerTaskMetadata())
|
||||
: Promise.resolve({});
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
for (let i = 0; i < desiredOperations.length; i += 1) {
|
||||
const operation = desiredOperations[i] || {};
|
||||
const uploadPath = String(operation.uploadPath || '').trim();
|
||||
if (!uploadPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < currentUploadPaths.length; i += 1) {
|
||||
const uploadPath = String(currentUploadPaths[i] || '').trim();
|
||||
if (!uploadPath || desiredUploadPaths.has(uploadPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (String(operation.type || '').trim().toLowerCase() === 'delete') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
const TASK = {
|
||||
key: 'onboarding-device-prune',
|
||||
title: 'Onboarding device prune',
|
||||
category: 'cleanup',
|
||||
trigger: 'scheduled recurring task, hourly',
|
||||
purpose: 'remove stale onboarding device bindings that have been idle for more than one minute.',
|
||||
taskType: 'recurring-run',
|
||||
intervalMs: 60 * 60 * 1000
|
||||
};
|
||||
|
||||
function registerOnboardingDevicePruneTask(options) {
|
||||
const backgroundTaskQueue = options && options.backgroundTaskQueue;
|
||||
const pool = options && options.pool;
|
||||
const common = options && options.common;
|
||||
|
||||
if (!backgroundTaskQueue || !pool || !common || typeof common.pruneStaleOnboardingDevices !== 'function') {
|
||||
throw new Error('registerOnboardingDevicePruneTask requires the onboarding prune dependencies.');
|
||||
}
|
||||
|
||||
backgroundTaskQueue.registerRecurringTask({
|
||||
key: TASK.key,
|
||||
title: TASK.title,
|
||||
category: TASK.category,
|
||||
intervalMs: TASK.intervalMs,
|
||||
metadata: {},
|
||||
run: async function () {
|
||||
await common.pruneStaleOnboardingDevices(pool);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { registerOnboardingDevicePruneTask };
|
||||
@@ -5,9 +5,9 @@ function createWebConfig() {
|
||||
const uploadsDir = path.join(mediaDir, 'uploads');
|
||||
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 playerInternalUrl = (process.env.PLAYER_INTERNAL_URL || process.env.PLAYER_BASE_URL || 'http://player:8081').replace(/\/$/, '');
|
||||
const bridgeInternalUrl = (process.env.BRIDGE_INTERNAL_URL || 'http://player-bridge:8090').replace(/\/$/, '');
|
||||
const webInternalUrl = (process.env.WEB_INTERNAL_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;
|
||||
@@ -20,9 +20,9 @@ function createWebConfig() {
|
||||
uploadsDir: uploadsDir,
|
||||
thumbnailsDir: thumbnailsDir,
|
||||
assetDir: assetDir,
|
||||
playerInternalBaseUrl: playerInternalBaseUrl,
|
||||
thinClientBaseUrl: thinClientBaseUrl,
|
||||
webBaseUrl: webBaseUrl,
|
||||
playerInternalUrl: playerInternalUrl,
|
||||
bridgeInternalUrl: bridgeInternalUrl,
|
||||
webInternalUrl: webInternalUrl,
|
||||
sessionCookieName: sessionCookieName,
|
||||
sessionMaxAgeMs: sessionMaxAgeMs,
|
||||
dataSourceStartupRefreshStaggerMs: dataSourceStartupRefreshStaggerMs
|
||||
|
||||
@@ -438,6 +438,9 @@ function createUploadSyncService(options) {
|
||||
console.warn('Unable to flush pending upload syncs:', error);
|
||||
});
|
||||
}, 5000);
|
||||
if (pendingPlayerUploadSyncFlushTimer && typeof pendingPlayerUploadSyncFlushTimer.unref === 'function') {
|
||||
pendingPlayerUploadSyncFlushTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
async function pushUploadFileToPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl) {
|
||||
@@ -677,6 +680,11 @@ function createUploadSyncService(options) {
|
||||
return pendingPlaylistUploadSyncFlushInFlight;
|
||||
}
|
||||
|
||||
if (pendingPlaylistUploadSyncFlushTimer) {
|
||||
clearTimeout(pendingPlaylistUploadSyncFlushTimer);
|
||||
pendingPlaylistUploadSyncFlushTimer = null;
|
||||
}
|
||||
|
||||
if (!pendingPlaylistUploadSyncs.size) {
|
||||
return null;
|
||||
}
|
||||
@@ -715,6 +723,11 @@ function createUploadSyncService(options) {
|
||||
return pendingPlayerUploadSyncFlushInFlight;
|
||||
}
|
||||
|
||||
if (pendingPlayerUploadSyncFlushTimer) {
|
||||
clearTimeout(pendingPlayerUploadSyncFlushTimer);
|
||||
pendingPlayerUploadSyncFlushTimer = null;
|
||||
}
|
||||
|
||||
if (!pendingPlayerUploadSyncs.size) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+119
-66
@@ -1,5 +1,5 @@
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
const { getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
@@ -33,24 +33,10 @@ function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
async function fetchRecentPlayerRegistrations(pool) {
|
||||
if (!pool || typeof fetchPlayerRegistrations !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const players = await fetchPlayerRegistrations(pool);
|
||||
return (Array.isArray(players) ? players : []).filter(function (player) {
|
||||
return isRecentPlayerRegistration(player, 60);
|
||||
});
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function createPlayerActionService(options) {
|
||||
const pool = options && options.pool;
|
||||
const configuredPlayerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||
const configuredBridgeInternalBaseUrl = String(options && options.bridgeInternalBaseUrl || '').trim().replace(/\/$/, '');
|
||||
const common = options && options.common;
|
||||
|
||||
if (!common) {
|
||||
@@ -151,6 +137,79 @@ function createPlayerActionService(options) {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchScreenConnectionsFromBaseUrl(baseUrl, slug) {
|
||||
const targetBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
if (!targetBaseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'GET',
|
||||
pathname: `/api/screens/${encodeURIComponent(slug)}/connections`
|
||||
});
|
||||
|
||||
const response = await fetch(`${targetBaseUrl}/api/screens/${encodeURIComponent(slug)}/connections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...authHeaders
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(function () { return ''; });
|
||||
const error = new Error(errorText || `Unable to fetch connections for player ${slug}.`);
|
||||
error.statusCode = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json().catch(function () {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
async function forwardPlayerCommandToDevice(deviceId, commandOrPayload) {
|
||||
const targetDeviceId = String(deviceId || '').trim();
|
||||
const targetBridgeBaseUrl = normalizeBaseUrl(configuredBridgeInternalBaseUrl);
|
||||
if (!targetDeviceId) {
|
||||
throw new Error('Device ID is required.');
|
||||
}
|
||||
if (!targetBridgeBaseUrl) {
|
||||
throw new Error('Unable to resolve the player bridge base URL.');
|
||||
}
|
||||
|
||||
const payload = typeof commandOrPayload === 'object' && commandOrPayload !== null
|
||||
? Object.assign({}, commandOrPayload)
|
||||
: { command: commandOrPayload };
|
||||
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'POST',
|
||||
pathname: `/api/players/${encodeURIComponent(targetDeviceId)}/commands`,
|
||||
body: payload
|
||||
});
|
||||
|
||||
const response = await fetch(`${targetBridgeBaseUrl}/api/players/${encodeURIComponent(targetDeviceId)}/commands`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...authHeaders
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(function () { return ''; });
|
||||
const error = new Error(errorText || `Unable to send command to player ${targetDeviceId}.`);
|
||||
error.statusCode = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json().catch(function () {
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
|
||||
async function forwardPlayerCommand(slug, commandOrPayload, connectionId) {
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
return forwardPlayerCommandToBaseUrl(resolvedPlayerInternalBaseUrl, slug, commandOrPayload, connectionId);
|
||||
@@ -190,69 +249,62 @@ function createPlayerActionService(options) {
|
||||
}
|
||||
|
||||
async function getScreenConnections(slug) {
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'GET',
|
||||
pathname: `/api/screens/${encodeURIComponent(slug)}/connections`
|
||||
});
|
||||
const recentPlayers = await fetchRecentPlayerRegistrations(pool);
|
||||
const targetBaseUrls = Array.from(new Set((recentPlayers.length ? recentPlayers : []).map(function (player) {
|
||||
return normalizeBaseUrl(player && player.public_base_url);
|
||||
}).filter(Boolean)));
|
||||
|
||||
if (!targetBaseUrls.length) {
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
if (resolvedPlayerInternalBaseUrl) {
|
||||
targetBaseUrls.push(resolvedPlayerInternalBaseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const bridgeBaseUrl = normalizeBaseUrl(configuredBridgeInternalBaseUrl);
|
||||
const playerBaseUrl = await getPlayerInternalBaseUrl();
|
||||
const targetBaseUrls = Array.from(new Set([bridgeBaseUrl, playerBaseUrl].map(normalizeBaseUrl).filter(Boolean)));
|
||||
if (!targetBaseUrls.length) {
|
||||
throw new Error('Unable to resolve the player internal base URL.');
|
||||
}
|
||||
|
||||
const results = await Promise.all(targetBaseUrls.map(async function (baseUrl) {
|
||||
const response = await fetch(`${baseUrl}/api/screens/${encodeURIComponent(slug)}/connections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...authHeaders
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json().catch(function () {
|
||||
return null;
|
||||
});
|
||||
const results = await Promise.allSettled(targetBaseUrls.map(function (baseUrl) {
|
||||
return fetchScreenConnectionsFromBaseUrl(baseUrl, slug);
|
||||
}));
|
||||
|
||||
const successfulResults = results.filter(function (result) {
|
||||
return result.status === 'fulfilled' && result.value;
|
||||
}).map(function (result) {
|
||||
return result.value;
|
||||
});
|
||||
|
||||
if (!successfulResults.length) {
|
||||
const rejection = results.find(function (result) {
|
||||
return result.status === 'rejected';
|
||||
});
|
||||
throw rejection ? rejection.reason : new Error(`Unable to fetch connections for player ${slug}.`);
|
||||
}
|
||||
|
||||
const mergedConnections = [];
|
||||
let screen = null;
|
||||
let degraded = false;
|
||||
results.forEach(function (result) {
|
||||
if (!result) {
|
||||
degraded = true;
|
||||
return;
|
||||
}
|
||||
if (!screen && result.screen) {
|
||||
screen = result.screen;
|
||||
}
|
||||
if (Array.isArray(result.connections)) {
|
||||
mergedConnections.push.apply(mergedConnections, result.connections);
|
||||
}
|
||||
if (result.degraded) {
|
||||
degraded = true;
|
||||
}
|
||||
const seenKeys = new Set();
|
||||
|
||||
successfulResults.forEach(function (result) {
|
||||
const connections = Array.isArray(result && result.connections) ? result.connections : [];
|
||||
connections.forEach(function (connection) {
|
||||
const key = [
|
||||
String(connection && connection.id || '').trim(),
|
||||
String(connection && connection.clientId || '').trim(),
|
||||
String(connection && connection.deviceId || '').trim(),
|
||||
String(connection && connection.playerPublicBaseUrl || '').trim()
|
||||
].join('|');
|
||||
if (!key || seenKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
seenKeys.add(key);
|
||||
mergedConnections.push(connection);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
screen: screen,
|
||||
screen: successfulResults.find(function (result) {
|
||||
return Boolean(result && result.screen);
|
||||
}) ? successfulResults.find(function (result) {
|
||||
return Boolean(result && result.screen);
|
||||
}).screen : null,
|
||||
screenSlug: slug,
|
||||
count: mergedConnections.length,
|
||||
connections: mergedConnections,
|
||||
degraded: degraded
|
||||
degraded: results.some(function (result) {
|
||||
return result.status === 'fulfilled' && Boolean(result.value && result.value.degraded);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -302,6 +354,7 @@ function createPlayerActionService(options) {
|
||||
forwardAnnouncementRefresh: forwardAnnouncementRefresh,
|
||||
getScreenConnections: getScreenConnections,
|
||||
forwardPlayerCommandToBaseUrl: forwardPlayerCommandToBaseUrl,
|
||||
forwardPlayerCommandToDevice: forwardPlayerCommandToDevice,
|
||||
getScreenDeleteBlockMessage: getScreenDeleteBlockMessage,
|
||||
getSlideDeleteBlockMessage: getSlideDeleteBlockMessage,
|
||||
getTemplateDeleteBlockMessage: getTemplateDeleteBlockMessage,
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
var normalizeDisplayIp = webUiHelpers.normalizeDisplayIp;
|
||||
var LIST_PAGE_SIZE = 25;
|
||||
var latestDashboardState = null;
|
||||
var ALL_SCREENS_SLUG = '__all__';
|
||||
var ALL_SCREENS_LABEL = 'All screens';
|
||||
|
||||
function getClientSearchInput() {
|
||||
var table = document.getElementById('dashboard-clients-table');
|
||||
@@ -20,6 +18,147 @@
|
||||
return container && container.querySelector ? container.querySelector('[data-table-search]') : document.querySelector('[data-table-search]');
|
||||
}
|
||||
|
||||
function getScreenCommandSelect() {
|
||||
return document.querySelector('[data-screen-command-select]');
|
||||
}
|
||||
|
||||
function getScreenCommandForms() {
|
||||
if (!document.querySelectorAll) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.prototype.slice.call(document.querySelectorAll('[data-screen-command-form]'));
|
||||
}
|
||||
|
||||
function getSelectedScreenClients(state) {
|
||||
var select = getScreenCommandSelect();
|
||||
var selectedSlug = select ? String(select.value || '').trim() : '';
|
||||
var clients = Array.isArray(state && state.clients) ? state.clients : [];
|
||||
|
||||
if (!selectedSlug || selectedSlug === '__all__') {
|
||||
return clients;
|
||||
}
|
||||
|
||||
return clients.filter(function (client) {
|
||||
return String(client && client.screen_slug || '').trim() === selectedSlug;
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedScreenLabel() {
|
||||
var select = getScreenCommandSelect();
|
||||
if (!select) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var selectedOption = select.options && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
|
||||
return selectedOption ? String(selectedOption.getAttribute('data-screen-name') || selectedOption.textContent || '').trim() : '';
|
||||
}
|
||||
|
||||
function updateToggleButton(button, form, state) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
var action = String(form && form.getAttribute('data-screen-command-action') || '').trim();
|
||||
var selectedLabel = getSelectedScreenLabel() || 'selected screen group';
|
||||
var isAllScreens = String(getScreenCommandSelect() && getScreenCommandSelect().value || '').trim() === '__all__';
|
||||
var clients = getSelectedScreenClients(state);
|
||||
var hasClients = clients.length > 0;
|
||||
var allPaused = hasClients && clients.every(function (client) {
|
||||
return Boolean(client && client.paused);
|
||||
});
|
||||
var allBlackout = hasClients && clients.every(function (client) {
|
||||
return Boolean(client && client.blackout);
|
||||
});
|
||||
|
||||
if (action === 'pause') {
|
||||
var pauseLabel = allPaused ? 'Resume ' + (isAllScreens ? 'all clients' : 'screen') : 'Pause ' + (isAllScreens ? 'all clients' : 'screen');
|
||||
var pauseConfirm = allPaused ? 'Resume ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?' : 'Pause ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?';
|
||||
var pauseIcon = allPaused ? 'bi-play-fill' : 'bi-pause-fill';
|
||||
button.innerHTML = '<i class="bi ' + pauseIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(pauseLabel);
|
||||
setButtonVariant(button, ['btn-success', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info');
|
||||
button.setAttribute('aria-label', pauseLabel);
|
||||
button.setAttribute('title', pauseLabel);
|
||||
if (form) {
|
||||
var pauseInput = form.querySelector('input[name="paused"]');
|
||||
if (pauseInput) {
|
||||
pauseInput.value = allPaused ? 'false' : 'true';
|
||||
}
|
||||
form.setAttribute('data-confirm-message', pauseConfirm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'blackout') {
|
||||
var blackoutLabel = allBlackout ? 'Restore ' + (isAllScreens ? 'all clients' : 'screen') : 'Blackout ' + (isAllScreens ? 'all clients' : 'screen');
|
||||
var blackoutConfirm = allBlackout ? 'Restore ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?' : 'Blackout ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?';
|
||||
var blackoutIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash';
|
||||
button.innerHTML = '<i class="bi ' + blackoutIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(blackoutLabel);
|
||||
setButtonVariant(button, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary');
|
||||
button.setAttribute('aria-label', blackoutLabel);
|
||||
button.setAttribute('title', blackoutLabel);
|
||||
if (form) {
|
||||
var blackoutInput = form.querySelector('input[name="blackout"]');
|
||||
if (blackoutInput) {
|
||||
blackoutInput.value = allBlackout ? 'false' : 'true';
|
||||
}
|
||||
form.setAttribute('data-confirm-message', blackoutConfirm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateScreenCommandControls() {
|
||||
var select = getScreenCommandSelect();
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedSlug = String(select.value || '').trim();
|
||||
var actionTarget = '/clients/' + encodeURIComponent(selectedSlug || '__all__') + '/commands';
|
||||
var selectedName = getSelectedScreenLabel();
|
||||
var selectedClients = getSelectedScreenClients(latestDashboardState);
|
||||
var allPaused = selectedClients.length > 0 && selectedClients.every(function (client) {
|
||||
return Boolean(client && client.paused);
|
||||
});
|
||||
var allBlackout = selectedClients.length > 0 && selectedClients.every(function (client) {
|
||||
return Boolean(client && client.blackout);
|
||||
});
|
||||
|
||||
getScreenCommandForms().forEach(function (form) {
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setAttribute('action', actionTarget);
|
||||
|
||||
var action = String(form.getAttribute('data-screen-command-action') || '').trim();
|
||||
var button = form.querySelector('button[type="submit"]');
|
||||
|
||||
if (action === 'pause' || action === 'blackout') {
|
||||
updateToggleButton(button, form, latestDashboardState);
|
||||
return;
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.setAttribute('aria-label', selectedName ? selectedName : 'Selected screen group');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initScreenCommandControls() {
|
||||
var select = getScreenCommandSelect();
|
||||
if (!select || (select.dataset && select.dataset.bound === 'true')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (select.dataset) {
|
||||
select.dataset.bound = 'true';
|
||||
}
|
||||
|
||||
select.addEventListener('change', updateScreenCommandControls);
|
||||
updateScreenCommandControls();
|
||||
}
|
||||
|
||||
function getClientListQueryState() {
|
||||
var searchParams = new URLSearchParams(String(window.location && window.location.search || ''));
|
||||
var searchInput = getClientSearchInput();
|
||||
@@ -170,77 +309,6 @@
|
||||
return clients.slice((query.page - 1) * LIST_PAGE_SIZE, ((query.page - 1) * LIST_PAGE_SIZE) + LIST_PAGE_SIZE);
|
||||
}
|
||||
|
||||
function getClientMoveModalElements() {
|
||||
return {
|
||||
modal: document.getElementById('client-move-screen-modal'),
|
||||
form: document.getElementById('client-move-screen-form'),
|
||||
targetSelect: document.getElementById('client-move-screen-target'),
|
||||
connectionInput: document.querySelector('[data-client-move-connection-id]'),
|
||||
deviceInput: document.querySelector('[data-client-move-device-id]'),
|
||||
clientNameInput: document.querySelector('[data-client-move-client-name]'),
|
||||
playerBaseUrlInput: document.querySelector('[data-client-move-player-base-url]')
|
||||
};
|
||||
}
|
||||
|
||||
function getClientMoveScreens() {
|
||||
if (latestDashboardState && Array.isArray(latestDashboardState.screens) && latestDashboardState.screens.length) {
|
||||
return latestDashboardState.screens.slice().sort(compareScreensByConnectedClients);
|
||||
}
|
||||
|
||||
var select = document.getElementById('client-move-screen-target');
|
||||
if (!select || !select.options) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.prototype.slice.call(select.options).map(function (option) {
|
||||
return {
|
||||
slug: String(option.value || '').trim(),
|
||||
name: String(option.textContent || option.value || '').trim()
|
||||
};
|
||||
}).filter(function (screen) {
|
||||
return Boolean(screen && screen.slug);
|
||||
});
|
||||
}
|
||||
|
||||
function updateClientMoveModalFromRow(row) {
|
||||
var elements = getClientMoveModalElements();
|
||||
if (!elements.form || !elements.targetSelect || !row) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentScreenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim();
|
||||
var connectionId = String(row.getAttribute('data-client-key') || '').trim();
|
||||
var deviceId = String(row.getAttribute('data-client-device-id') || '').trim();
|
||||
var playerBaseUrl = String(row.getAttribute('data-client-player-base-url') || '').trim();
|
||||
var clientNameCell = row.querySelector('td[data-label="Client"] > div');
|
||||
var clientName = String(clientNameCell && clientNameCell.textContent || '').trim();
|
||||
var options = Array.prototype.slice.call(elements.targetSelect.options || []);
|
||||
|
||||
options.forEach(function (option) {
|
||||
option.disabled = false;
|
||||
if (String(option.value || '').trim() === currentScreenSlug) {
|
||||
option.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
elements.form.action = currentScreenSlug ? '/clients/' + encodeURIComponent(currentScreenSlug) + '/commands' : '#';
|
||||
if (elements.connectionInput) {
|
||||
elements.connectionInput.value = connectionId;
|
||||
}
|
||||
if (elements.deviceInput) {
|
||||
elements.deviceInput.value = deviceId;
|
||||
}
|
||||
if (elements.clientNameInput) {
|
||||
elements.clientNameInput.value = clientName;
|
||||
}
|
||||
if (elements.playerBaseUrlInput) {
|
||||
elements.playerBaseUrlInput.value = playerBaseUrl;
|
||||
}
|
||||
elements.targetSelect.value = '';
|
||||
if (elements.form.querySelector('button[type="submit"]')) {
|
||||
elements.form.querySelector('button[type="submit"]').disabled = false;
|
||||
}
|
||||
}
|
||||
function renderClientActionCell(client) {
|
||||
var paused = Boolean(client.paused);
|
||||
var pauseButtonClass = 'btn btn-sm btn-info';
|
||||
@@ -257,7 +325,7 @@
|
||||
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) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button></form>',
|
||||
'<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" data-bs-toggle="modal" data-bs-target="#client-move-screen-modal" aria-label="Move client to another screen" title="Move client to another screen"><i class="bi bi-display" aria-hidden="true"></i></button>',
|
||||
'<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" aria-label="Move client to another screen" title="Move client to another screen"><i class="bi bi-display" aria-hidden="true"></i></button>',
|
||||
'<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) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="previous" aria-label="Previous slide" title="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) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="next" aria-label="Next slide" title="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) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause" aria-label="' + pauseButtonLabel + '" title="' + pauseButtonLabel + '"><i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonText + '</button></form>',
|
||||
@@ -266,6 +334,60 @@
|
||||
].join('');
|
||||
}
|
||||
|
||||
function getClientMoveModalElements() {
|
||||
return {
|
||||
modal: document.getElementById('client-move-screen-modal'),
|
||||
form: document.getElementById('client-move-screen-form'),
|
||||
targetSelect: document.getElementById('client-move-screen-target'),
|
||||
connectionInput: document.querySelector('[data-client-move-connection-id]'),
|
||||
deviceInput: document.querySelector('[data-client-move-device-id]'),
|
||||
clientNameInput: document.querySelector('[data-client-move-client-name]'),
|
||||
playerBaseUrlInput: document.querySelector('[data-client-move-player-base-url]')
|
||||
};
|
||||
}
|
||||
|
||||
function updateClientMoveModalFromRow(row) {
|
||||
var elements = getClientMoveModalElements();
|
||||
if (!elements.form || !elements.targetSelect || !row) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentScreenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim();
|
||||
var connectionId = String(row.getAttribute('data-client-id') || '').trim();
|
||||
var deviceId = String(row.getAttribute('data-client-device-id') || '').trim();
|
||||
var playerBaseUrl = String(row.getAttribute('data-client-player-base-url') || '').trim();
|
||||
var clientNameCell = row.querySelector('td[data-label="Client"] > div');
|
||||
var clientName = String(clientNameCell && clientNameCell.textContent || '').trim();
|
||||
var options = Array.prototype.slice.call(elements.targetSelect.options || []);
|
||||
|
||||
options.forEach(function (option) {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
option.disabled = String(option.value || '').trim() === currentScreenSlug;
|
||||
});
|
||||
|
||||
elements.form.action = currentScreenSlug ? '/clients/' + encodeURIComponent(currentScreenSlug) + '/commands' : '#';
|
||||
if (elements.connectionInput) {
|
||||
elements.connectionInput.value = connectionId;
|
||||
}
|
||||
if (elements.deviceInput) {
|
||||
elements.deviceInput.value = deviceId;
|
||||
}
|
||||
if (elements.clientNameInput) {
|
||||
elements.clientNameInput.value = clientName;
|
||||
}
|
||||
if (elements.playerBaseUrlInput) {
|
||||
elements.playerBaseUrlInput.value = playerBaseUrl;
|
||||
}
|
||||
if (elements.targetSelect) {
|
||||
elements.targetSelect.value = '';
|
||||
}
|
||||
if (elements.form.querySelector('button[type="submit"]')) {
|
||||
elements.form.querySelector('button[type="submit"]').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateClientActionCell(cell, client) {
|
||||
if (!cell) {
|
||||
return;
|
||||
@@ -440,7 +562,7 @@
|
||||
var actionCell = hasActionsColumn ? '<td data-label="Actions">' + renderClientActionCell(client) + '</td>' : '';
|
||||
|
||||
return [
|
||||
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '" data-client-id="' + escapeHtml(client.clientId || '') + '" data-client-device-id="' + escapeHtml(client.deviceId || '') + '" data-client-screen-slug="' + escapeHtml(client.screen_slug || '') + '" data-client-player-base-url="' + escapeHtml(client.player_url || '') + '">',
|
||||
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '" data-client-id="' + escapeHtml(client.id || '') + '" data-client-client-id="' + escapeHtml(client.clientId || '') + '" data-client-device-id="' + escapeHtml(client.deviceId || '') + '" data-client-screen-slug="' + escapeHtml(client.screen_slug || '') + '" data-client-player-base-url="' + escapeHtml(client.player_url || '') + '">',
|
||||
'<td data-label="Client"><div>' + clientName + '</div></td>',
|
||||
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
|
||||
'<td data-label="Current Slide">' + currentSlide + '</td>',
|
||||
@@ -467,7 +589,8 @@
|
||||
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : '<span class="empty">No slide currently showing</span>';
|
||||
|
||||
row.setAttribute('data-client-key', escapeHtml(getClientRowKey(client)));
|
||||
row.setAttribute('data-client-id', escapeHtml(client.clientId || ''));
|
||||
row.setAttribute('data-client-id', escapeHtml(client.id || ''));
|
||||
row.setAttribute('data-client-client-id', escapeHtml(client.clientId || ''));
|
||||
row.setAttribute('data-client-device-id', escapeHtml(client.deviceId || ''));
|
||||
row.setAttribute('data-client-screen-slug', escapeHtml(client.screen_slug || ''));
|
||||
row.setAttribute('data-client-player-base-url', escapeHtml(client.player_url || ''));
|
||||
@@ -627,6 +750,112 @@
|
||||
updateClientTable(latestDashboardState, true);
|
||||
}
|
||||
|
||||
function initClientMoveHandler() {
|
||||
var elements = getClientMoveModalElements();
|
||||
if (!elements.modal || !elements.form || !elements.targetSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof elements.modal.addEventListener === 'function') {
|
||||
elements.modal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event && event.relatedTarget ? event.relatedTarget : null;
|
||||
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key]') : null;
|
||||
if (row) {
|
||||
updateClientMoveModalFromRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var moveButton = event.target && event.target.closest ? event.target.closest('button[data-action="move-screen"]') : null;
|
||||
if (!moveButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null;
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateClientMoveModalFromRow(row);
|
||||
|
||||
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
|
||||
window.pulseModal.show(elements.modal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(elements.modal).show();
|
||||
}
|
||||
});
|
||||
|
||||
elements.form.addEventListener('submit', function (event) {
|
||||
if (elements.form.dataset && elements.form.dataset.busy === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var targetScreenSlug = String(elements.targetSelect.value || '').trim();
|
||||
if (!targetScreenSlug) {
|
||||
event.preventDefault();
|
||||
window.alert('Choose a target screen.');
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
elements.form.dataset.busy = 'true';
|
||||
|
||||
var formData = new FormData(elements.form);
|
||||
var body = new URLSearchParams();
|
||||
formData.forEach(function (value, key) {
|
||||
body.append(key, value);
|
||||
});
|
||||
|
||||
fetch(elements.form.action, {
|
||||
method: (elements.form.method || 'POST').toUpperCase(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
body: body.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return response.text().then(function (text) {
|
||||
var error = new Error(text || 'Unable to move client.');
|
||||
try {
|
||||
var payload = JSON.parse(text);
|
||||
if (payload && (payload.error || payload.message)) {
|
||||
error = new Error(String(payload.error || payload.message));
|
||||
}
|
||||
} catch (_error) {
|
||||
// fall back to the raw text body
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
|
||||
window.pulseModal.hide(elements.modal);
|
||||
} else if (window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(elements.modal).hide();
|
||||
}
|
||||
return response.json().catch(function () {
|
||||
return { ok: true };
|
||||
});
|
||||
}).catch(function (error) {
|
||||
window.alert(error && error.message ? error.message : 'Unable to move client.');
|
||||
}).finally(function () {
|
||||
delete elements.form.dataset.busy;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateKioskLauncherModal(state) {
|
||||
var modal = document.getElementById('dashboard-kiosk-launcher-modal');
|
||||
if (!modal) {
|
||||
@@ -696,163 +925,6 @@
|
||||
}
|
||||
grid.innerHTML = screens.map(renderScreenTile).join('');
|
||||
}
|
||||
|
||||
function updateScreenCommandControls(state) {
|
||||
var select = document.getElementById('screen-command-select');
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
var forms = Array.prototype.slice.call(document.querySelectorAll('[data-screen-command-form]'));
|
||||
var pill = document.querySelector('[data-screen-command-pill]');
|
||||
var nameNode = document.querySelector('[data-screen-command-name]');
|
||||
var metaNode = document.querySelector('[data-screen-command-meta]');
|
||||
var screens = Array.isArray(state && state.screens) ? state.screens : [];
|
||||
var screenBySlug = {};
|
||||
|
||||
screens.forEach(function (screen) {
|
||||
if (screen && screen.slug) {
|
||||
screenBySlug[String(screen.slug)] = screen;
|
||||
}
|
||||
});
|
||||
|
||||
if (!screens.length) {
|
||||
select.value = '';
|
||||
select.disabled = true;
|
||||
forms.forEach(function (form) {
|
||||
form.querySelectorAll('button, input').forEach(function (control) {
|
||||
control.disabled = true;
|
||||
});
|
||||
});
|
||||
if (pill) {
|
||||
pill.classList.remove('is-live');
|
||||
pill.classList.add('is-idle');
|
||||
pill.textContent = 'No screens';
|
||||
}
|
||||
if (nameNode) {
|
||||
nameNode.textContent = 'No target available';
|
||||
}
|
||||
if (metaNode) {
|
||||
metaNode.textContent = 'Create a screen before using screen-level commands.';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
select.disabled = false;
|
||||
|
||||
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;
|
||||
var hasClients = connectionCount > 0;
|
||||
var allPaused = hasClients && selectedClients.every(function (client) {
|
||||
return Boolean(client && client.paused);
|
||||
});
|
||||
var allBlackout = hasClients && selectedClients.every(function (client) {
|
||||
return Boolean(client && client.blackout);
|
||||
});
|
||||
var connectionLabel = hasClients ? connectionCount + ' connected client' + (connectionCount === 1 ? '' : 's') : 'No clients connected';
|
||||
|
||||
if (pill) {
|
||||
pill.classList.toggle('is-live', hasClients);
|
||||
pill.classList.toggle('is-idle', !hasClients);
|
||||
pill.textContent = connectionLabel;
|
||||
}
|
||||
if (nameNode) {
|
||||
nameNode.textContent = selectedScreen
|
||||
? String(selectedScreen.name || 'Selected screen')
|
||||
: 'Select a target screen group';
|
||||
}
|
||||
if (metaNode) {
|
||||
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"]');
|
||||
var button = form.querySelector('button[type="submit"]');
|
||||
if (commandInput) {
|
||||
if (command === 'pause') {
|
||||
commandInput.value = allPaused ? 'pause' : 'pause';
|
||||
var pauseStateInput = form.querySelector('input[name="paused"]');
|
||||
if (pauseStateInput) {
|
||||
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 ? (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
|
||||
? (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"]');
|
||||
if (blackoutStateInput) {
|
||||
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 ? (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
|
||||
? (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 = commandTargetSlug ? '/clients/' + encodeURIComponent(commandTargetSlug) + '/commands' : '#';
|
||||
if (command === 'reload') {
|
||||
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>' + (isAllSelected ? 'Reload all screens' : 'Reload screen');
|
||||
}
|
||||
}
|
||||
Array.prototype.slice.call(form.querySelectorAll('button, input')).forEach(function (control) {
|
||||
control.disabled = !commandTargetSlug;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readScreenCommandStateFromDom() {
|
||||
var select = document.getElementById('screen-command-select');
|
||||
if (!select) {
|
||||
return { screens: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
screens: Array.prototype.slice.call(select.options || []).map(function (option) {
|
||||
return {
|
||||
slug: String(option.value || '').trim(),
|
||||
name: String(option.getAttribute('data-screen-name') || option.textContent || option.value || '').trim(),
|
||||
player_connection_count: Number(option.getAttribute('data-player-connection-count') || 0),
|
||||
playlist_name: String(option.getAttribute('data-playlist-name') || '').trim()
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function updateDashboardQuickActions(state) {
|
||||
var pauseButton = document.getElementById('dashboard-pause-all-button');
|
||||
var blackoutButton = document.getElementById('dashboard-blackout-all-button');
|
||||
@@ -889,7 +961,7 @@
|
||||
var blackoutButtonIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash';
|
||||
|
||||
blackoutButton.innerHTML = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(label);
|
||||
setButtonVariant(blackoutButton, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], 'btn-secondary');
|
||||
setButtonVariant(blackoutButton, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary');
|
||||
if (blackoutInput) {
|
||||
blackoutInput.value = allBlackout ? 'false' : 'true';
|
||||
}
|
||||
@@ -907,10 +979,10 @@
|
||||
window.webLatestDashboardState = latestDashboardState;
|
||||
updateStats(state);
|
||||
updateScreenGrid(state);
|
||||
updateScreenCommandControls(state);
|
||||
updateClientTable(state);
|
||||
updateKioskLauncherModal(state);
|
||||
updateDashboardQuickActions(state);
|
||||
updateScreenCommandControls();
|
||||
}
|
||||
|
||||
function sendClientRename(screenSlug, connectionId, clientId, deviceId, clientName) {
|
||||
@@ -967,8 +1039,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionId = String(row.getAttribute('data-client-key') || '').trim();
|
||||
var clientId = String(row.getAttribute('data-client-id') || '').trim();
|
||||
var connectionId = String(row.getAttribute('data-client-id') || '').trim();
|
||||
var clientId = String(row.getAttribute('data-client-client-id') || '').trim();
|
||||
var deviceId = String(row.getAttribute('data-client-device-id') || '').trim();
|
||||
var screenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim();
|
||||
var currentName = String(cell.textContent || '').trim();
|
||||
@@ -1003,18 +1075,41 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof elements.modal.addEventListener === 'function') {
|
||||
elements.modal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event && event.relatedTarget ? event.relatedTarget : null;
|
||||
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key]') : null;
|
||||
if (row) {
|
||||
updateClientMoveModalFromRow(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var moveButton = event.target && event.target.closest ? event.target.closest('button[data-action="move-screen"]') : null;
|
||||
if (!moveButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event && typeof event.preventDefault === 'function') {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null;
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateClientMoveModalFromRow(row);
|
||||
|
||||
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
|
||||
window.pulseModal.show(elements.modal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(elements.modal).show();
|
||||
}
|
||||
});
|
||||
|
||||
elements.form.addEventListener('submit', function (event) {
|
||||
@@ -1131,11 +1226,5 @@
|
||||
initClientRenameHandler();
|
||||
initClientMoveHandler();
|
||||
initKioskLauncherModal();
|
||||
var screenCommandSelect = document.getElementById('screen-command-select');
|
||||
if (screenCommandSelect) {
|
||||
updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom());
|
||||
screenCommandSelect.addEventListener('change', function () {
|
||||
updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom());
|
||||
});
|
||||
}
|
||||
initScreenCommandControls();
|
||||
}());
|
||||
|
||||
@@ -37,7 +37,12 @@
|
||||
}
|
||||
|
||||
function getClientRowKey(client) {
|
||||
return String(client && client.id ? client.id : client && client.clientId ? client.clientId : '').trim();
|
||||
var clientName = String(client && client.client_name ? client.client_name : '').trim();
|
||||
if (clientName) {
|
||||
return clientName;
|
||||
}
|
||||
|
||||
return String(client && client.screen_slug ? client.screen_slug : client && client.deviceId ? client.deviceId : client && client.id ? client.id : client && client.clientId ? client.clientId : '').trim();
|
||||
}
|
||||
|
||||
function getClientDisplayName(client) {
|
||||
|
||||
@@ -12,40 +12,122 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
const withClientNameReservation = deps.withClientNameReservation;
|
||||
const broadcastDashboardState = deps.broadcastDashboardState;
|
||||
const requirePermission = deps.requirePermission;
|
||||
const ALL_SCREENS_SLUG = '__all__';
|
||||
|
||||
async function resolveScreenPlayerBaseUrls(screenSlug, connectionId) {
|
||||
if (typeof getScreenConnections !== 'function' || !screenSlug) {
|
||||
function normalizeExplicitPlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function normalizeConnectionBaseUrl(connection) {
|
||||
return normalizeExplicitPlayerBaseUrl(connection && connection.playerPublicBaseUrl);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
async function fetchPlayerRegistrations() {
|
||||
if (typeof common.fetchPlayerRegistrations !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getScreenConnections(screenSlug);
|
||||
const liveConnections = Array.isArray(response && response.connections) ? response.connections : [];
|
||||
if (!liveConnections.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedConnectionId = String(connectionId || '').trim();
|
||||
const liveConnection = normalizedConnectionId
|
||||
? liveConnections.find(function (connection) {
|
||||
const candidateConnectionId = String(connection && (connection.id || connection.clientId) || '').trim();
|
||||
const candidateDeviceId = String(connection && connection.deviceId || '').trim();
|
||||
return candidateConnectionId === normalizedConnectionId || candidateDeviceId === normalizedConnectionId;
|
||||
})
|
||||
: null;
|
||||
const targetConnections = liveConnection ? [liveConnection] : liveConnections;
|
||||
|
||||
return Array.from(new Set(targetConnections.map(function (connection) {
|
||||
return String(connection && connection.playerPublicBaseUrl || '').trim().replace(/\/$/, '');
|
||||
}).filter(Boolean)));
|
||||
const registrations = await common.fetchPlayerRegistrations(pool);
|
||||
return Array.isArray(registrations) ? registrations : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExplicitPlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
async function fetchScreenConnections(screenSlug) {
|
||||
if (typeof getScreenConnections !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getScreenConnections(screenSlug);
|
||||
return Array.isArray(response && response.connections) ? response.connections : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllScreenTargets() {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT slug
|
||||
FROM d_screens
|
||||
WHERE slug IS NOT NULL
|
||||
ORDER BY slug ASC`
|
||||
);
|
||||
|
||||
return (rows || [])
|
||||
.map(function (row) {
|
||||
return {
|
||||
slug: String(row && row.slug ? row.slug : '').trim()
|
||||
};
|
||||
})
|
||||
.filter(function (row) {
|
||||
return Boolean(row.slug);
|
||||
});
|
||||
}
|
||||
|
||||
async function resolvePlayerBaseUrlsForConnections(connections) {
|
||||
const connectionList = Array.isArray(connections) ? connections : [];
|
||||
const seen = new Set();
|
||||
const registrations = await fetchPlayerRegistrations();
|
||||
|
||||
return connectionList.map(function (connection) {
|
||||
const selectedPublicBaseUrl = normalizeConnectionBaseUrl(connection);
|
||||
if (!selectedPublicBaseUrl || seen.has(selectedPublicBaseUrl)) {
|
||||
return '';
|
||||
}
|
||||
seen.add(selectedPublicBaseUrl);
|
||||
const matchedPlayer = registrations.find(function (player) {
|
||||
return normalizeExplicitPlayerBaseUrl(player && player.public_base_url) === selectedPublicBaseUrl;
|
||||
}) || null;
|
||||
return normalizeExplicitPlayerBaseUrl(matchedPlayer && matchedPlayer.internal_base_url) || selectedPublicBaseUrl;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
async function resolvePlayerBaseUrlForExplicitPublicBaseUrl(playerBaseUrl) {
|
||||
const normalizedPublicBaseUrl = normalizeExplicitPlayerBaseUrl(playerBaseUrl);
|
||||
if (!normalizedPublicBaseUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const registrations = await fetchPlayerRegistrations();
|
||||
const matchedPlayer = registrations.find(function (player) {
|
||||
return normalizeExplicitPlayerBaseUrl(player && player.public_base_url) === normalizedPublicBaseUrl;
|
||||
}) || null;
|
||||
|
||||
return normalizeExplicitPlayerBaseUrl(matchedPlayer && matchedPlayer.internal_base_url) || normalizedPublicBaseUrl;
|
||||
}
|
||||
|
||||
async function forwardPlayerCommandForConnections(screenSlug, connections, commandPayload, connectionId, deviceId) {
|
||||
const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(connections);
|
||||
if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
const results = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) {
|
||||
return forwardPlayerCommandToBaseUrl(targetBaseUrl, screenSlug, commandPayload, connectionId || deviceId || undefined);
|
||||
}));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
sent: results.filter(function (result) {
|
||||
return result.status === 'fulfilled';
|
||||
}).length,
|
||||
results: results
|
||||
};
|
||||
}
|
||||
|
||||
return forwardPlayerCommand(screenSlug, commandPayload, connectionId || deviceId || undefined);
|
||||
}
|
||||
|
||||
async function forwardPlayerCommandForPlayerBaseUrl(screenSlug, playerBaseUrl, commandPayload, connectionId, deviceId) {
|
||||
const targetBaseUrl = await resolvePlayerBaseUrlForExplicitPublicBaseUrl(playerBaseUrl);
|
||||
if (targetBaseUrl && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
return forwardPlayerCommandToBaseUrl(targetBaseUrl, screenSlug, commandPayload, connectionId || deviceId || undefined);
|
||||
}
|
||||
|
||||
return forwardPlayerCommand(screenSlug, commandPayload, connectionId || deviceId || undefined);
|
||||
}
|
||||
|
||||
function isRecentPlayerRegistration(player, staleSeconds) {
|
||||
@@ -56,32 +138,13 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
|
||||
}
|
||||
|
||||
async function resolveAllPlayerBaseUrls() {
|
||||
if (!common || typeof common.fetchPlayerRegistrations !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const players = await common.fetchPlayerRegistrations(pool);
|
||||
return Array.from(new Set((Array.isArray(players) ? players : [])
|
||||
.filter(function (player) {
|
||||
return isRecentPlayerRegistration(player, 60);
|
||||
})
|
||||
.map(function (player) {
|
||||
return String(player && player.public_base_url || '').trim().replace(/\/$/, '');
|
||||
})
|
||||
.filter(Boolean)));
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) {
|
||||
try {
|
||||
const slug = String(req.params.slug || '').trim();
|
||||
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
||||
const connectionId = String((req.body && (req.body.connectionId || req.body.clientId)) || req.query.connectionId || req.query.clientId || '').trim();
|
||||
const explicitPlayerBaseUrl = normalizeExplicitPlayerBaseUrl((req.body && (req.body.playerBaseUrl || req.body.playerPublicBaseUrl)) || req.query.playerBaseUrl || req.query.playerPublicBaseUrl || '');
|
||||
const playerBaseUrl = String((req.body && req.body.playerBaseUrl) || req.query.playerBaseUrl || '').trim();
|
||||
const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout')
|
||||
? req.body.blackout
|
||||
: req.query.blackout;
|
||||
@@ -93,14 +156,25 @@ 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') {
|
||||
if (slug === '__all__') {
|
||||
if (command !== 'reload' && command !== 'pause' && command !== 'blackout') {
|
||||
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 targets = await fetchAllScreenTargets();
|
||||
if (!targets.length) {
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
}
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
allScreens: true,
|
||||
command: command,
|
||||
targetScreenCount: 0,
|
||||
targetPlayerCount: 0,
|
||||
sent: 0
|
||||
});
|
||||
}
|
||||
|
||||
const commandPayload = req.body && typeof req.body === 'object' && !Array.isArray(req.body)
|
||||
@@ -109,36 +183,41 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
if (command === 'blackout' && blackoutValue !== undefined && commandPayload && typeof commandPayload === 'object') {
|
||||
commandPayload.blackout = blackoutValue;
|
||||
}
|
||||
if (command === 'pause' && req.body && Object.prototype.hasOwnProperty.call(req.body, 'paused') && commandPayload && typeof commandPayload === 'object') {
|
||||
commandPayload.paused = req.body.paused;
|
||||
}
|
||||
|
||||
await Promise.all(screenRows.map(function (screenRow) {
|
||||
const screenSlug = String(screenRow && screenRow.slug || '').trim();
|
||||
return resolveScreenPlayerBaseUrls(screenSlug, connectionId).then(function (playerBaseUrls) {
|
||||
if (playerBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
return Promise.all(playerBaseUrls.map(function (playerBaseUrl) {
|
||||
return forwardPlayerCommandToBaseUrl(playerBaseUrl, screenSlug, commandPayload, connectionId || undefined);
|
||||
}));
|
||||
}
|
||||
const results = await Promise.allSettled(targets.map(async function (target) {
|
||||
const liveConnections = await fetchScreenConnections(target.slug);
|
||||
if (liveConnections.length) {
|
||||
return forwardPlayerCommandForConnections(target.slug, liveConnections, commandPayload);
|
||||
}
|
||||
|
||||
return forwardPlayerCommand(screenSlug, commandPayload);
|
||||
});
|
||||
return forwardPlayerCommand(target.slug, commandPayload);
|
||||
}));
|
||||
|
||||
const sentCount = results.reduce(function (total, result) {
|
||||
if (result.status !== 'fulfilled') {
|
||||
return total;
|
||||
}
|
||||
|
||||
const sentValue = Number(result.value && typeof result.value.sent === 'number' ? result.value.sent : 1);
|
||||
return total + (Number.isFinite(sentValue) && sentValue > 0 ? sentValue : 1);
|
||||
}, 0);
|
||||
|
||||
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
|
||||
allScreens: true,
|
||||
command: command,
|
||||
targetScreenCount: targets.length,
|
||||
targetPlayerCount: sentCount,
|
||||
sent: sentCount,
|
||||
blackout: command === 'blackout' ? Boolean(commandPayload.blackout) : undefined,
|
||||
paused: command === 'pause' ? Boolean(commandPayload.paused) : undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,28 +226,6 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
return res.status(404).json({ error: 'Screen not found' });
|
||||
}
|
||||
|
||||
if (explicitPlayerBaseUrl && typeof forwardPlayerCommandToBaseUrl === 'function' && command !== 'moveclient') {
|
||||
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;
|
||||
}
|
||||
|
||||
const result = await forwardPlayerCommandToBaseUrl(explicitPlayerBaseUrl, slug, commandPayload, connectionId || undefined);
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
}
|
||||
|
||||
return res.json(Object.assign({
|
||||
screen: screenRows[0],
|
||||
screenSlug: slug,
|
||||
command: command,
|
||||
connectionId: connectionId || null
|
||||
}, result && typeof result === 'object' ? result : {}));
|
||||
}
|
||||
|
||||
if (command === 'setclientname') {
|
||||
const deviceId = String((req.body && (req.body.deviceId || req.body.clientId)) || req.query.deviceId || req.query.clientId || '').trim();
|
||||
const clientName = String((req.body && req.body.clientName) || req.query.clientName || '').trim();
|
||||
@@ -231,12 +288,21 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
}
|
||||
|
||||
if (!onboardingRow) {
|
||||
await forwardPlayerCommand(slug, {
|
||||
command: command,
|
||||
clientName: clientName,
|
||||
clientId: connectionId || deviceId || null,
|
||||
deviceId: deviceId || null
|
||||
}, connectionId || deviceId || undefined);
|
||||
if (playerBaseUrl) {
|
||||
await forwardPlayerCommandForPlayerBaseUrl(slug, playerBaseUrl, {
|
||||
command: command,
|
||||
clientName: clientName,
|
||||
clientId: connectionId || deviceId || null,
|
||||
deviceId: deviceId || null
|
||||
}, connectionId || deviceId || undefined, deviceId || null);
|
||||
} else {
|
||||
await forwardPlayerCommandForConnections(slug, liveConnections, {
|
||||
command: command,
|
||||
clientName: clientName,
|
||||
clientId: connectionId || deviceId || null,
|
||||
deviceId: deviceId || null
|
||||
}, connectionId || deviceId || undefined, deviceId || null);
|
||||
}
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
@@ -323,52 +389,35 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
return res.status(500).json({ error: 'Client binding is unavailable.' });
|
||||
}
|
||||
|
||||
let liveConnections = [];
|
||||
try {
|
||||
const [screenSlugs] = await pool.query('SELECT slug FROM d_screens ORDER BY slug ASC');
|
||||
const liveResults = await Promise.all((screenSlugs || []).map(async function (row) {
|
||||
const screenSlug = String(row && row.slug ? row.slug : '').trim();
|
||||
if (!screenSlug || typeof getScreenConnections !== 'function') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const liveResponse = await getScreenConnections(screenSlug);
|
||||
return Array.isArray(liveResponse && liveResponse.connections) ? liveResponse.connections : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
liveConnections = liveResults.flat();
|
||||
} catch (_error) {
|
||||
liveConnections = [];
|
||||
}
|
||||
const liveConnections = await fetchScreenConnections(slug);
|
||||
|
||||
const status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections);
|
||||
let targetPlayerUrl = '';
|
||||
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;
|
||||
})
|
||||
}) || liveConnections[0] || null
|
||||
: null;
|
||||
const sourcePlayerBaseUrl = String(
|
||||
explicitPlayerBaseUrl ||
|
||||
(liveConnection && liveConnection.playerPublicBaseUrl) ||
|
||||
(typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') ||
|
||||
''
|
||||
).trim().replace(/\/$/, '');
|
||||
const targetPlayerUrl = sourcePlayerBaseUrl ? `${sourcePlayerBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}` : `/screen/${encodeURIComponent(targetScreenSlug)}`;
|
||||
const sourcePlayerBaseUrl = normalizeExplicitPlayerBaseUrl(playerBaseUrl || (liveConnection && liveConnection.playerPublicBaseUrl) || '');
|
||||
if (sourcePlayerBaseUrl) {
|
||||
targetPlayerUrl = `${sourcePlayerBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}`;
|
||||
}
|
||||
if (!targetPlayerUrl) {
|
||||
targetPlayerUrl = `/screen/${encodeURIComponent(targetScreenSlug)}`;
|
||||
}
|
||||
|
||||
if (sourcePlayerBaseUrl && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
await forwardPlayerCommandToBaseUrl(sourcePlayerBaseUrl, slug, {
|
||||
if (playerBaseUrl) {
|
||||
await forwardPlayerCommandForPlayerBaseUrl(slug, playerBaseUrl, {
|
||||
command: 'redirect',
|
||||
url: targetPlayerUrl
|
||||
}, connectionId || deviceId || undefined);
|
||||
}, connectionId || deviceId || undefined, deviceId || null);
|
||||
} else {
|
||||
await forwardPlayerCommand(slug, {
|
||||
await forwardPlayerCommandForConnections(slug, liveConnections, {
|
||||
command: 'redirect',
|
||||
url: targetPlayerUrl
|
||||
}, connectionId || deviceId || undefined);
|
||||
}, connectionId || deviceId || undefined, deviceId || null);
|
||||
}
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
@@ -395,16 +444,10 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
commandPayload.blackout = blackoutValue;
|
||||
}
|
||||
|
||||
const playerBaseUrls = await resolveScreenPlayerBaseUrls(slug, connectionId);
|
||||
const result = playerBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function'
|
||||
? await Promise.all(playerBaseUrls.map(function (playerBaseUrl) {
|
||||
return forwardPlayerCommandToBaseUrl(playerBaseUrl, slug, commandPayload, connectionId);
|
||||
})).then(function (results) {
|
||||
return Array.isArray(results) && results.length ? results[0] : { ok: true };
|
||||
})
|
||||
: (connectionId
|
||||
? await forwardPlayerCommand(slug, commandPayload, connectionId)
|
||||
: await forwardPlayerCommand(slug, commandPayload));
|
||||
const liveConnections = await fetchScreenConnections(slug);
|
||||
const result = playerBaseUrl
|
||||
? await forwardPlayerCommandForPlayerBaseUrl(slug, playerBaseUrl, commandPayload, connectionId, null)
|
||||
: await forwardPlayerCommandForConnections(slug, liveConnections, commandPayload, connectionId, null);
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
|
||||
+138
-14
@@ -11,6 +11,7 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
const getScreenDeleteBlockMessage = deps.getScreenDeleteBlockMessage;
|
||||
const getScreenConnections = deps.getScreenConnections;
|
||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||
const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl;
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
const SCREEN_NAME_MAX_LENGTH = 255;
|
||||
@@ -25,13 +26,106 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
return text.slice(0, limit);
|
||||
}
|
||||
|
||||
async function fetchAllScreenSlugs() {
|
||||
const [rows] = await pool.query('SELECT slug FROM d_screens ORDER BY slug ASC');
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function normalizeExplicitPlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
async function fetchPlayerRegistrations() {
|
||||
if (typeof common.fetchPlayerRegistrations !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const registrations = await common.fetchPlayerRegistrations(pool);
|
||||
return Array.isArray(registrations) ? registrations : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function fetchLiveConnectionsForScreen(slug) {
|
||||
if (typeof getScreenConnections !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getScreenConnections(slug);
|
||||
return Array.isArray(response && response.connections) ? response.connections : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function resolvePlayerBaseUrlsForConnections(connections) {
|
||||
const connectionList = Array.isArray(connections) ? connections : [];
|
||||
const seen = new Set();
|
||||
const registrations = await fetchPlayerRegistrations();
|
||||
|
||||
return connectionList.map(function (connection) {
|
||||
const selectedPublicBaseUrl = normalizeExplicitPlayerBaseUrl(connection && connection.playerPublicBaseUrl);
|
||||
if (!selectedPublicBaseUrl || seen.has(selectedPublicBaseUrl)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
seen.add(selectedPublicBaseUrl);
|
||||
const matchedPlayer = registrations.find(function (player) {
|
||||
return normalizeExplicitPlayerBaseUrl(player && player.public_base_url) === selectedPublicBaseUrl;
|
||||
}) || null;
|
||||
|
||||
return normalizeExplicitPlayerBaseUrl(matchedPlayer && matchedPlayer.internal_base_url) || selectedPublicBaseUrl;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
async function forwardPlayerCommandForConnections(slug, connections, commandPayload) {
|
||||
const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(connections);
|
||||
if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
const results = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) {
|
||||
return forwardPlayerCommandToBaseUrl(targetBaseUrl, slug, commandPayload);
|
||||
}));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
sent: results.filter(function (result) {
|
||||
return result.status === 'fulfilled';
|
||||
}).length,
|
||||
results: results
|
||||
};
|
||||
}
|
||||
|
||||
return forwardPlayerCommand(slug, commandPayload);
|
||||
}
|
||||
|
||||
async function fetchAllScreenTargets() {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT s.slug
|
||||
FROM d_screens s
|
||||
WHERE s.slug IS NOT NULL
|
||||
ORDER BY s.slug ASC`
|
||||
);
|
||||
return (rows || [])
|
||||
.map(function (row) {
|
||||
return String(row && row.slug ? row.slug : '').trim();
|
||||
return {
|
||||
slug: String(row && row.slug ? row.slug : '').trim(),
|
||||
playerId: '',
|
||||
publicBaseUrl: '',
|
||||
internalBaseUrl: ''
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
.filter(function (row) {
|
||||
return Boolean(row.slug);
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/commands', requirePermission('dashboard.allow'), async function (req, res, next) {
|
||||
@@ -52,8 +146,8 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
return res.status(400).json({ error: 'Unsupported command' });
|
||||
}
|
||||
|
||||
const slugs = await fetchAllScreenSlugs();
|
||||
if (!slugs.length) {
|
||||
const targets = await fetchAllScreenTargets();
|
||||
if (!targets.length) {
|
||||
await broadcastDashboardState();
|
||||
return res.json({ ok: true, command: command, sent: 0 });
|
||||
}
|
||||
@@ -70,7 +164,22 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
}
|
||||
: 'reload';
|
||||
|
||||
const sentCount = await notifyPlayerScreens(slugs, payload);
|
||||
const results = await Promise.allSettled(targets.map(async function (target) {
|
||||
const liveConnections = await fetchLiveConnectionsForScreen(target.slug);
|
||||
if (liveConnections.length) {
|
||||
return forwardPlayerCommandForConnections(target.slug, liveConnections, payload);
|
||||
}
|
||||
|
||||
return forwardPlayerCommand(target.slug, payload);
|
||||
}));
|
||||
const sentCount = results.reduce(function (total, result) {
|
||||
if (result.status !== 'fulfilled') {
|
||||
return total;
|
||||
}
|
||||
|
||||
const sentValue = Number(result.value && typeof result.value.sent === 'number' ? result.value.sent : 1);
|
||||
return total + (Number.isFinite(sentValue) && sentValue > 0 ? sentValue : 1);
|
||||
}, 0);
|
||||
await broadcastDashboardState();
|
||||
|
||||
return res.json({
|
||||
@@ -149,13 +258,28 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
await notifyPlayerScreens([previousSlug], 'refresh');
|
||||
}
|
||||
if (previousSlug && previousSlug !== slug) {
|
||||
const playerBaseUrl = typeof common.fetchPlayerPublicBaseUrl === 'function'
|
||||
? await common.fetchPlayerPublicBaseUrl(pool)
|
||||
: '';
|
||||
await forwardPlayerCommand(previousSlug, {
|
||||
command: 'redirect',
|
||||
url: playerBaseUrl ? `${playerBaseUrl}/screen/${encodeURIComponent(slug)}` : `/screen/${encodeURIComponent(slug)}`
|
||||
});
|
||||
const previousScreenTargets = await pool.query(
|
||||
`SELECT s.slug, s.player_id, p.public_base_url, p.internal_base_url
|
||||
FROM d_screens s
|
||||
LEFT JOIN d_players p ON p.device_id = s.player_id
|
||||
WHERE s.slug = ?
|
||||
LIMIT 1`,
|
||||
[previousSlug]
|
||||
);
|
||||
const previousTargetRow = previousScreenTargets[0] && previousScreenTargets[0][0] || null;
|
||||
const previousInternalBaseUrl = normalizeBaseUrl(previousTargetRow && previousTargetRow.internal_base_url) || '';
|
||||
const previousPublicBaseUrl = normalizeBaseUrl(previousTargetRow && previousTargetRow.public_base_url) || '';
|
||||
if (previousInternalBaseUrl && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
await forwardPlayerCommandToBaseUrl(previousInternalBaseUrl, previousSlug, {
|
||||
command: 'redirect',
|
||||
url: previousPublicBaseUrl ? `${previousPublicBaseUrl}/screen/${encodeURIComponent(slug)}` : `/screen/${encodeURIComponent(slug)}`
|
||||
});
|
||||
} else {
|
||||
await forwardPlayerCommand(previousSlug, {
|
||||
command: 'redirect',
|
||||
url: previousPublicBaseUrl ? `${previousPublicBaseUrl}/screen/${encodeURIComponent(slug)}` : `/screen/${encodeURIComponent(slug)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
redirectAfterSave(req, res, '/screens?edit=' + screen.id, {
|
||||
closeUrl: '/screens',
|
||||
|
||||
@@ -125,6 +125,8 @@ function registerSignageRoutes(app, deps) {
|
||||
getScreenDeleteBlockMessage: deps.playerActionService.getScreenDeleteBlockMessage,
|
||||
getScreenConnections: deps.playerActionService.getScreenConnections,
|
||||
forwardPlayerCommand: deps.playerActionService.forwardPlayerCommand,
|
||||
forwardPlayerCommandToBaseUrl: deps.playerActionService.forwardPlayerCommandToBaseUrl,
|
||||
forwardPlayerCommandToDevice: deps.playerActionService.forwardPlayerCommandToDevice,
|
||||
requirePermission: deps.requirePermission
|
||||
});
|
||||
|
||||
|
||||
@@ -40,16 +40,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="screen-command-actions">
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-async-command data-screen-command-form data-screen-command-action="reload">
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-screen-command-form data-screen-command-action="reload" data-async-command>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
||||
</form>
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-async-command data-screen-command-form data-screen-command-action="pause">
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-screen-command-form data-screen-command-action="pause" data-async-command>
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="paused" value="true" />
|
||||
<button type="submit" class="btn btn-sm btn-info"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
||||
</form>
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-async-command data-screen-command-form data-screen-command-action="blackout">
|
||||
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-screen-command-form data-screen-command-action="blackout" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="blackout" value="true" />
|
||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
||||
@@ -138,7 +138,7 @@
|
||||
<input type="hidden" name="playerBaseUrl" value="{{player_url}}" />
|
||||
<button type="submit" class="btn btn-sm btn-danger" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" data-bs-toggle="modal" data-bs-target="#client-move-screen-modal" aria-label="Move client to another screen group" title="Move client to another screen group"><i class="bi bi-display" aria-hidden="true"></i></button>
|
||||
<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" aria-label="Move client to another screen group" title="Move client to another screen group"><i class="bi bi-display" aria-hidden="true"></i></button>
|
||||
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="previous" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
|
||||
Reference in New Issue
Block a user