Add player control-plane and dashboard updates
This commit is contained in:
+143
-38
@@ -1,4 +1,52 @@
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function 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 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;
|
||||
@@ -22,26 +70,38 @@ function createPlayerActionService(options) {
|
||||
}
|
||||
|
||||
playerInternalBaseUrlPromise = (async function () {
|
||||
if (!pool) {
|
||||
return configuredPlayerInternalBaseUrl || null;
|
||||
try {
|
||||
if (pool && typeof fetchPlayerRegistrations === 'function') {
|
||||
const configuredPlayerIdentifier = getConfiguredPlayerIdentifier();
|
||||
const players = await fetchPlayerRegistrations(pool);
|
||||
const exactPlayer = Array.isArray(players)
|
||||
? players.find(function (player) {
|
||||
return String(player && player.identifier || '').trim() === configuredPlayerIdentifier;
|
||||
})
|
||||
: null;
|
||||
const registeredPlayers = Array.isArray(players) ? players : [];
|
||||
const preferredPlayer = exactPlayer || registeredPlayers.find(function (player) {
|
||||
const internalBaseUrl = normalizeBaseUrl(player && player.internal_base_url);
|
||||
return internalBaseUrl && !isLocalLikeBaseUrl(internalBaseUrl);
|
||||
}) || registeredPlayers[0] || null;
|
||||
const resolvedBaseUrl = normalizeBaseUrl(preferredPlayer && preferredPlayer.internal_base_url);
|
||||
if (resolvedBaseUrl) {
|
||||
playerInternalBaseUrl = resolvedBaseUrl;
|
||||
return resolvedBaseUrl;
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT internal_base_url
|
||||
FROM d_players
|
||||
WHERE device_id = '1'
|
||||
LIMIT 1`
|
||||
);
|
||||
const resolvedBaseUrl = String(rows && rows[0] && rows[0].internal_base_url || '').trim().replace(/\/$/, '');
|
||||
return resolvedBaseUrl || configuredPlayerInternalBaseUrl || null;
|
||||
} catch (_error) {
|
||||
return configuredPlayerInternalBaseUrl || null;
|
||||
if (configuredPlayerInternalBaseUrl) {
|
||||
playerInternalBaseUrl = configuredPlayerInternalBaseUrl;
|
||||
return configuredPlayerInternalBaseUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
})().then(function (baseUrl) {
|
||||
playerInternalBaseUrl = baseUrl || null;
|
||||
playerInternalBaseUrlPromise = null;
|
||||
return playerInternalBaseUrl;
|
||||
return baseUrl || null;
|
||||
}, function () {
|
||||
playerInternalBaseUrlPromise = null;
|
||||
return configuredPlayerInternalBaseUrl || null;
|
||||
@@ -50,24 +110,26 @@ function createPlayerActionService(options) {
|
||||
return playerInternalBaseUrlPromise;
|
||||
}
|
||||
|
||||
async function forwardPlayerCommand(slug, commandOrPayload, connectionId) {
|
||||
async function forwardPlayerCommandToBaseUrl(baseUrl, slug, commandOrPayload, connectionId) {
|
||||
const targetBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
if (!targetBaseUrl) {
|
||||
throw new Error('Unable to resolve the player internal base URL.');
|
||||
}
|
||||
|
||||
const payload = typeof commandOrPayload === 'object' && commandOrPayload !== null
|
||||
? Object.assign({}, commandOrPayload)
|
||||
: { command: commandOrPayload };
|
||||
if (connectionId) {
|
||||
payload.connectionId = connectionId;
|
||||
}
|
||||
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'POST',
|
||||
pathname: `/api/screens/${encodeURIComponent(slug)}/commands`,
|
||||
body: payload
|
||||
});
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
if (!resolvedPlayerInternalBaseUrl) {
|
||||
throw new Error('Unable to resolve the player internal base URL.');
|
||||
}
|
||||
|
||||
const response = await fetch(`${resolvedPlayerInternalBaseUrl}/api/screens/${encodeURIComponent(slug)}/commands`, {
|
||||
const response = await fetch(`${targetBaseUrl}/api/screens/${encodeURIComponent(slug)}/commands`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -89,6 +151,11 @@ function createPlayerActionService(options) {
|
||||
});
|
||||
}
|
||||
|
||||
async function forwardPlayerCommand(slug, commandOrPayload, connectionId) {
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
return forwardPlayerCommandToBaseUrl(resolvedPlayerInternalBaseUrl, slug, commandOrPayload, connectionId);
|
||||
}
|
||||
|
||||
async function forwardAnnouncementRefresh(slug) {
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'POST',
|
||||
@@ -127,29 +194,66 @@ function createPlayerActionService(options) {
|
||||
method: 'GET',
|
||||
pathname: `/api/screens/${encodeURIComponent(slug)}/connections`
|
||||
});
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
if (!resolvedPlayerInternalBaseUrl) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetBaseUrls.length) {
|
||||
throw new Error('Unable to resolve the player internal base URL.');
|
||||
}
|
||||
|
||||
const response = await fetch(`${resolvedPlayerInternalBaseUrl}/api/screens/${encodeURIComponent(slug)}/connections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...authHeaders
|
||||
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 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;
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(function () { return ''; });
|
||||
const error = new Error(errorText || `Unable to load screen connections for ${slug}.`);
|
||||
error.statusCode = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json().catch(function () {
|
||||
return { connections: [] };
|
||||
});
|
||||
return {
|
||||
screen: screen,
|
||||
screenSlug: slug,
|
||||
count: mergedConnections.length,
|
||||
connections: mergedConnections,
|
||||
degraded: degraded
|
||||
};
|
||||
}
|
||||
|
||||
async function getScreenDeleteBlockMessage(pool, screen, getScreenConnections) {
|
||||
@@ -197,6 +301,7 @@ function createPlayerActionService(options) {
|
||||
forwardPlayerCommand: forwardPlayerCommand,
|
||||
forwardAnnouncementRefresh: forwardAnnouncementRefresh,
|
||||
getScreenConnections: getScreenConnections,
|
||||
forwardPlayerCommandToBaseUrl: forwardPlayerCommandToBaseUrl,
|
||||
getScreenDeleteBlockMessage: getScreenDeleteBlockMessage,
|
||||
getSlideDeleteBlockMessage: getSlideDeleteBlockMessage,
|
||||
getTemplateDeleteBlockMessage: getTemplateDeleteBlockMessage,
|
||||
|
||||
Reference in New Issue
Block a user