Add multi-player and remote bridge support

This commit is contained in:
2026-08-07 02:58:18 +01:00
parent a4f8a807ff
commit 74318eb34e
58 changed files with 3785 additions and 432 deletions
+68 -3
View File
@@ -6,6 +6,10 @@ function normalizeClientName(value) {
return String(value || '').trim();
}
function normalizePlayerBaseUrl(value) {
return String(value || '').trim().replace(/\/$/, '');
}
function enrichScreensWithConnections(screens, connectionsBySlug, onboardingNameBySlug, playerUrlsBySlug) {
return (screens || []).map(function (screen) {
const connectionState = connectionsBySlug[screen.slug] || { count: 0, connections: [] };
@@ -18,11 +22,12 @@ function enrichScreensWithConnections(screens, connectionsBySlug, onboardingName
});
}
function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, formatDashboardDate) {
function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, playerIdentifierByBaseUrl, formatDashboardDate) {
return (screens || []).flatMap(function (screen) {
const connectionState = connectionsBySlug[screen.slug] || { count: 0, connections: [] };
return (connectionState.connections || []).map(function (connection) {
const deviceId = String(connection.deviceId || '').trim();
const playerBaseUrl = normalizePlayerBaseUrl(connection.playerPublicBaseUrl);
return Object.assign({}, connection, {
screen_slug: screen.slug,
screen_name: screen.name,
@@ -30,12 +35,55 @@ function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboa
playlist_name: screen.playlist_name || null,
connectedAtLabel: formatDashboardDate(connection.connectedAt),
lastSeenAtLabel: formatDashboardDate(connection.lastSeenAt),
player_url: screen.player_url || String(connection.page || '').trim() || null
player_identifier: playerIdentifierByBaseUrl && playerBaseUrl ? (playerIdentifierByBaseUrl[playerBaseUrl] || null) : null,
player_url: playerBaseUrl || null
});
});
});
}
function isRecentPlayerRegistration(player, staleSeconds) {
const lastSeenAt = player && player.last_seen_at;
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
}
function buildKioskLauncherPlayers(playerRegistrations, staleSeconds) {
return (Array.isArray(playerRegistrations) ? playerRegistrations : [])
.filter(function (player) {
return Boolean(player && String(player.identifier || '').trim() && String(player.public_base_url || '').trim() && isRecentPlayerRegistration(player, staleSeconds));
})
.map(function (player) {
return {
player_identifier: String(player.identifier || '').trim(),
player_url: String(player.public_base_url || '').trim().replace(/\/$/, '')
};
});
}
async function fetchConnectedPlayerCount(pool, connectedPlayerCountStaleSeconds) {
const staleSeconds = Math.max(30, Number(connectedPlayerCountStaleSeconds || 60));
if (!pool || typeof pool.query !== 'function' || !Number.isFinite(staleSeconds)) {
return null;
}
try {
const [rows] = await pool.query(
`SELECT COUNT(*) AS connected_count
FROM d_players
WHERE last_seen_at IS NOT NULL
AND last_seen_at >= DATE_SUB(NOW(), INTERVAL ${staleSeconds} SECOND)`
);
const count = Number(rows && rows[0] && rows[0].connected_count);
return Number.isFinite(count) && count >= 0 ? count : null;
} catch (_error) {
return null;
}
}
function compareScreenNames(left, right) {
const leftName = String(left && left.name || '').trim();
const rightName = String(right && right.name || '').trim();
@@ -51,6 +99,7 @@ function compareScreenNames(left, right) {
function createDashboardStateService(options) {
const pool = options && options.pool;
const common = options && options.common;
const connectedPlayerCountStaleSeconds = options && options.connectedPlayerCountStaleSeconds;
const playerSnapshotCache = options && options.playerSnapshotCache;
const playerSnapshotSockets = options && options.playerSnapshotSockets;
const ensurePlayerSnapshotSubscription = options && options.ensurePlayerSnapshotSubscription;
@@ -63,9 +112,13 @@ function createDashboardStateService(options) {
async function buildDashboardState() {
const data = await common.fetchAdminData(pool);
const screensData = data.screens || [];
const connectedPlayersCount = await fetchConnectedPlayerCount(pool, connectedPlayerCountStaleSeconds);
const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function'
? await common.fetchScreenPlayerUrls(pool)
: {};
const playerRegistrations = typeof common.fetchPlayerRegistrations === 'function'
? await common.fetchPlayerRegistrations(pool)
: [];
screensData.forEach(function (screen) {
ensurePlayerSnapshotSubscription(screen.slug);
});
@@ -91,6 +144,15 @@ function createDashboardStateService(options) {
}
});
const playerIdentifierByBaseUrl = {};
(Array.isArray(playerRegistrations) ? playerRegistrations : []).forEach(function (player) {
const baseUrl = normalizePlayerBaseUrl(player && player.public_base_url);
const identifier = normalizeClientName(player && player.identifier);
if (baseUrl && identifier) {
playerIdentifierByBaseUrl[baseUrl] = identifier;
}
});
const connectionsBySlug = {};
screensData.forEach(function (screen) {
const cached = playerSnapshotCache.get(String(screen.slug || '').trim());
@@ -106,7 +168,8 @@ function createDashboardStateService(options) {
});
})
.sort(compareScreenNames);
const clients = buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, formatDashboardDate);
const clients = buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboardingNameByDeviceId, playerIdentifierByBaseUrl, formatDashboardDate);
const kioskPlayers = buildKioskLauncherPlayers(playerRegistrations, connectedPlayerCountStaleSeconds);
const playerServiceConnected = Array.from(playerSnapshotSockets.values()).some(function (socket) {
return socket && socket.readyState === WebSocket.OPEN;
});
@@ -115,8 +178,10 @@ function createDashboardStateService(options) {
playlists: data.playlists || [],
screens: screens,
clients: clients,
kioskPlayers: kioskPlayers,
slides: data.slides || [],
playerServiceConnected: playerServiceConnected,
connectedPlayersCount: connectedPlayersCount !== null ? connectedPlayersCount : 0,
connectedClientsCount: screens.reduce(function (total, screen) {
return total + Number(screen.player_connection_count || 0);
}, 0)