Add player control-plane and dashboard updates
This commit is contained in:
@@ -6,17 +6,82 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
const common = deps.common;
|
||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||
const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl;
|
||||
const getScreenConnections = deps.getScreenConnections;
|
||||
const isClientNameAvailable = deps.isClientNameAvailable;
|
||||
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) {
|
||||
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)));
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExplicitPlayerBaseUrl(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 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 blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout')
|
||||
? req.body.blackout
|
||||
: req.query.blackout;
|
||||
@@ -28,11 +93,82 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
return res.status(400).json({ error: 'Command is required' });
|
||||
}
|
||||
|
||||
if (slug === ALL_SCREENS_SLUG) {
|
||||
if (command === 'setclientname' || command === 'moveclient') {
|
||||
return res.status(400).json({ error: 'This command requires a specific screen.' });
|
||||
}
|
||||
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug IS NOT NULL ORDER BY slug ASC');
|
||||
if (!screenRows.length) {
|
||||
return res.status(404).json({ error: 'No screens found' });
|
||||
}
|
||||
|
||||
const commandPayload = req.body && typeof req.body === 'object' && !Array.isArray(req.body)
|
||||
? Object.assign({}, req.body, { command: command })
|
||||
: { command: command };
|
||||
if (command === 'blackout' && blackoutValue !== undefined && commandPayload && typeof commandPayload === 'object') {
|
||||
commandPayload.blackout = blackoutValue;
|
||||
}
|
||||
|
||||
await Promise.all(screenRows.map(function (screenRow) {
|
||||
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);
|
||||
}));
|
||||
}
|
||||
|
||||
return forwardPlayerCommand(screenSlug, commandPayload);
|
||||
});
|
||||
}));
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
}
|
||||
|
||||
return res.json({
|
||||
screen: {
|
||||
id: null,
|
||||
name: 'All screens',
|
||||
slug: ALL_SCREENS_SLUG
|
||||
},
|
||||
screenSlug: slug,
|
||||
command: command,
|
||||
connectionId: connectionId || null,
|
||||
targetScreenCount: screenRows.length,
|
||||
ok: true,
|
||||
allScreens: true
|
||||
});
|
||||
}
|
||||
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug FROM d_screens WHERE slug = ?', [slug]);
|
||||
if (!screenRows.length) {
|
||||
return res.status(404).json({ error: 'Screen not found' });
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -208,20 +344,32 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
}
|
||||
|
||||
const status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections);
|
||||
const targetPlayerRecord = typeof common.fetchScreenPlayerRecord === 'function'
|
||||
? await common.fetchScreenPlayerRecord(pool, targetScreenSlug)
|
||||
const liveConnection = Array.isArray(liveConnections)
|
||||
? liveConnections.find(function (connection) {
|
||||
const candidateConnectionId = String(connection && (connection.id || connection.clientId) || '').trim();
|
||||
const candidateDeviceId = String(connection && connection.deviceId || '').trim();
|
||||
return candidateConnectionId === connectionId || candidateConnectionId === deviceId || candidateDeviceId === deviceId;
|
||||
})
|
||||
: null;
|
||||
const targetBaseUrl = String(
|
||||
targetPlayerRecord && targetPlayerRecord.public_base_url ||
|
||||
const sourcePlayerBaseUrl = String(
|
||||
explicitPlayerBaseUrl ||
|
||||
(liveConnection && liveConnection.playerPublicBaseUrl) ||
|
||||
(typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') ||
|
||||
''
|
||||
).trim().replace(/\/$/, '');
|
||||
const targetPlayerUrl = targetBaseUrl ? `${targetBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}` : `/screen/${encodeURIComponent(targetScreenSlug)}`;
|
||||
const targetPlayerUrl = sourcePlayerBaseUrl ? `${sourcePlayerBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}` : `/screen/${encodeURIComponent(targetScreenSlug)}`;
|
||||
|
||||
await forwardPlayerCommand(slug, {
|
||||
command: 'redirect',
|
||||
url: targetPlayerUrl
|
||||
}, connectionId || deviceId || undefined);
|
||||
if (sourcePlayerBaseUrl && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
await forwardPlayerCommandToBaseUrl(sourcePlayerBaseUrl, slug, {
|
||||
command: 'redirect',
|
||||
url: targetPlayerUrl
|
||||
}, connectionId || deviceId || undefined);
|
||||
} else {
|
||||
await forwardPlayerCommand(slug, {
|
||||
command: 'redirect',
|
||||
url: targetPlayerUrl
|
||||
}, connectionId || deviceId || undefined);
|
||||
}
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
@@ -247,9 +395,16 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
commandPayload.blackout = blackoutValue;
|
||||
}
|
||||
|
||||
const result = connectionId
|
||||
? await forwardPlayerCommand(slug, commandPayload, connectionId)
|
||||
: await forwardPlayerCommand(slug, commandPayload);
|
||||
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));
|
||||
|
||||
if (typeof broadcastDashboardState === 'function') {
|
||||
await broadcastDashboardState();
|
||||
|
||||
Reference in New Issue
Block a user