Release v2.10.4
This commit is contained in:
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.10.4 - 2026-08-29
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed remote client move commands so they route through the player bridge to the correct browser tab using the physical player identity and connection ID.
|
||||||
|
- Removed the obsolete `PLAYER_BASE_URL` configuration fallback; command routing uses `PLAYER_INTERNAL_URL` locally or the player bridge remotely, while `PLAYER_PUBLIC_URL` remains available for kiosk launcher and direct player access.
|
||||||
|
- Removed the player URL list from screen-group add/edit pages and expanded the remaining form card to full width.
|
||||||
|
|
||||||
## 2.10.3 - 2026-08-28
|
## 2.10.3 - 2026-08-28
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.10.3",
|
"version": "2.10.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.10.3",
|
"version": "2.10.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ PULSE_SIGNAGE_SHARED_SECRET=""
|
|||||||
|
|
||||||
# Remote player
|
# Remote player
|
||||||
PLAYER_IDENTIFIER="player-remote"
|
PLAYER_IDENTIFIER="player-remote"
|
||||||
PLAYER_PUBLIC_URL="http://remote-player.example.com:8081"
|
|
||||||
|
# Optional URL used by the kiosk launcher when the remote player is directly reachable
|
||||||
|
# PLAYER_PUBLIC_URL="http://remote-player.example.com:8081"
|
||||||
|
|
||||||
PLAYER_AGENT_RECONNECT_DELAY_MS=5000
|
PLAYER_AGENT_RECONNECT_DELAY_MS=5000
|
||||||
|
|
||||||
# Remote bridge connectivity
|
# Remote bridge connectivity
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ Responsibilities:
|
|||||||
|
|
||||||
Key configuration:
|
Key configuration:
|
||||||
|
|
||||||
- `PLAYER_PUBLIC_URL`
|
|
||||||
- `PLAYER_INTERNAL_URL`
|
- `PLAYER_INTERNAL_URL`
|
||||||
- `PLAYER_IDENTIFIER`
|
- `PLAYER_IDENTIFIER`
|
||||||
- `BRIDGE_PUBLIC_URL` in remote mode
|
- `BRIDGE_PUBLIC_URL` in remote mode
|
||||||
@@ -118,7 +117,7 @@ Important values:
|
|||||||
- `WEB_PUBLIC_URL` - public URL of the web application
|
- `WEB_PUBLIC_URL` - public URL of the web application
|
||||||
- `WEB_INTERNAL_URL` - internal URL the bridge uses to call the web app directly
|
- `WEB_INTERNAL_URL` - internal URL the bridge uses to call the web app directly
|
||||||
- `PLAYER_IDENTIFIER` - unique local player identifier
|
- `PLAYER_IDENTIFIER` - unique local player identifier
|
||||||
- `PLAYER_PUBLIC_URL` - public URL the player advertises
|
- `PLAYER_PUBLIC_URL` - URL used by the kiosk launcher and direct player access
|
||||||
- `PLAYER_INTERNAL_URL` - internal URL the web app uses for local player calls
|
- `PLAYER_INTERNAL_URL` - internal URL the web app uses for local player calls
|
||||||
- `BRIDGE_INTERNAL_URL` - bridge URL the web app uses for player snapshot and command forwarding
|
- `BRIDGE_INTERNAL_URL` - bridge URL the web app uses for player snapshot and command forwarding
|
||||||
- `DEFAULT_ADMIN_*` - bootstrap admin account values
|
- `DEFAULT_ADMIN_*` - bootstrap admin account values
|
||||||
@@ -132,7 +131,7 @@ Important values:
|
|||||||
- `PULSE_SIGNAGE_PLAYER_IMAGE` - image to run on the device, typically `.../pulse-signage-player:latest`
|
- `PULSE_SIGNAGE_PLAYER_IMAGE` - image to run on the device, typically `.../pulse-signage-player:latest`
|
||||||
- `PULSE_SIGNAGE_SHARED_SECRET` - must match the public stack and should be the same long random value used everywhere in the deployment
|
- `PULSE_SIGNAGE_SHARED_SECRET` - must match the public stack and should be the same long random value used everywhere in the deployment
|
||||||
- `PLAYER_IDENTIFIER` - unique remote player identifier
|
- `PLAYER_IDENTIFIER` - unique remote player identifier
|
||||||
- `PLAYER_PUBLIC_URL` - public URL for the remote player
|
- `PLAYER_PUBLIC_URL` - optional URL used by the kiosk launcher when the remote player is directly reachable
|
||||||
- `BRIDGE_PUBLIC_URL` - bridge URL the player connects back to
|
- `BRIDGE_PUBLIC_URL` - bridge URL the player connects back to
|
||||||
- `PLAYER_AGENT_RECONNECT_DELAY_MS` - reconnect delay for the player agent
|
- `PLAYER_AGENT_RECONNECT_DELAY_MS` - reconnect delay for the player agent
|
||||||
|
|
||||||
@@ -169,7 +168,7 @@ Leave it blank only if you intentionally want to run without request signing in
|
|||||||
| `WEB_PUBLIC_URL` | web, player-bridge | Public URL of the web application. |
|
| `WEB_PUBLIC_URL` | web, player-bridge | Public URL of the web application. |
|
||||||
| `WEB_INTERNAL_URL` | player-bridge | Internal web URL used by the bridge to call the dashboard app directly. |
|
| `WEB_INTERNAL_URL` | player-bridge | Internal web URL used by the bridge to call the dashboard app directly. |
|
||||||
| `PLAYER_IDENTIFIER` | player | Stable player identifier. |
|
| `PLAYER_IDENTIFIER` | player | Stable player identifier. |
|
||||||
| `PLAYER_PUBLIC_URL` | player, remote player | Public URL advertised by the player. |
|
| `PLAYER_PUBLIC_URL` | player, remote player | URL used by the kiosk launcher and direct player access; optional for bridge-only remote players. |
|
||||||
| `PLAYER_INTERNAL_URL` | web, player | Internal player URL used by the dashboard and player runtime. |
|
| `PLAYER_INTERNAL_URL` | web, player | Internal player URL used by the dashboard and player runtime. |
|
||||||
| `BRIDGE_INTERNAL_URL` | web | Bridge URL used by the web app for player snapshot and command forwarding. |
|
| `BRIDGE_INTERNAL_URL` | web | Bridge URL used by the web app for player snapshot and command forwarding. |
|
||||||
| `DEFAULT_ADMIN_USERNAME` | web | Bootstrap admin username. |
|
| `DEFAULT_ADMIN_USERNAME` | web | Bootstrap admin username. |
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
- "8081:8081"
|
- "8081:8081"
|
||||||
environment:
|
environment:
|
||||||
PLAYER_IDENTIFIER: ${PLAYER_IDENTIFIER:-player-remote}
|
PLAYER_IDENTIFIER: ${PLAYER_IDENTIFIER:-player-remote}
|
||||||
PLAYER_PUBLIC_URL: ${PLAYER_PUBLIC_URL:?PLAYER_PUBLIC_URL must be set to a routable remote-player URL}
|
PLAYER_PUBLIC_URL: ${PLAYER_PUBLIC_URL:-}
|
||||||
BRIDGE_PUBLIC_URL: ${BRIDGE_PUBLIC_URL:?BRIDGE_PUBLIC_URL must be set to a routable bridge URL}
|
BRIDGE_PUBLIC_URL: ${BRIDGE_PUBLIC_URL:?BRIDGE_PUBLIC_URL must be set to a routable bridge URL}
|
||||||
PULSE_SIGNAGE_SHARED_SECRET: ${PULSE_SIGNAGE_SHARED_SECRET:-}
|
PULSE_SIGNAGE_SHARED_SECRET: ${PULSE_SIGNAGE_SHARED_SECRET:-}
|
||||||
PLAYER_AGENT_RECONNECT_DELAY_MS: ${PLAYER_AGENT_RECONNECT_DELAY_MS:-5000}
|
PLAYER_AGENT_RECONNECT_DELAY_MS: ${PLAYER_AGENT_RECONNECT_DELAY_MS:-5000}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.10.3",
|
"version": "2.10.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.10.3",
|
"version": "2.10.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sparticuz/chromium": "^149.0.0",
|
"@sparticuz/chromium": "^149.0.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.10.3",
|
"version": "2.10.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -916,8 +916,11 @@ async function start() {
|
|||||||
return res.status(404).json({ ok: false, error: 'Player is not connected.' });
|
return res.status(404).json({ ok: false, error: 'Player is not connected.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.send(JSON.stringify(payload));
|
const response = await sendPlayerCommandToSocket(socket, payload);
|
||||||
res.json({ ok: true, deviceId: deviceId, sent: true });
|
res.status(response && response.ok ? 200 : (response && response.status || 502)).json(Object.assign({
|
||||||
|
deviceId: deviceId,
|
||||||
|
connectionId: payload.connectionId || null
|
||||||
|
}, response && typeof response === 'object' ? response : { ok: false }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -975,9 +978,14 @@ async function start() {
|
|||||||
const snapshotPlayerPublicBaseUrl = String(payload.playerPublicBaseUrl || socket.publicBaseUrl || '').trim().replace(/\/$/, '');
|
const snapshotPlayerPublicBaseUrl = String(payload.playerPublicBaseUrl || socket.publicBaseUrl || '').trim().replace(/\/$/, '');
|
||||||
const connections = Array.isArray(payload.connections) ? payload.connections.map(function (connection) {
|
const connections = Array.isArray(payload.connections) ? payload.connections.map(function (connection) {
|
||||||
if (!connection || typeof connection !== 'object' || !snapshotPlayerPublicBaseUrl) {
|
if (!connection || typeof connection !== 'object' || !snapshotPlayerPublicBaseUrl) {
|
||||||
return connection;
|
return connection && typeof connection === 'object'
|
||||||
|
? Object.assign({}, connection, { playerDeviceId: deviceId })
|
||||||
|
: connection;
|
||||||
}
|
}
|
||||||
return Object.assign({}, connection, { playerPublicBaseUrl: snapshotPlayerPublicBaseUrl });
|
return Object.assign({}, connection, {
|
||||||
|
playerDeviceId: deviceId,
|
||||||
|
playerPublicBaseUrl: snapshotPlayerPublicBaseUrl
|
||||||
|
});
|
||||||
}) : [];
|
}) : [];
|
||||||
setScreenSnapshotSource(slug, deviceId, connections);
|
setScreenSnapshotSource(slug, deviceId, connections);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+2
-2
@@ -24,11 +24,11 @@ async function start() {
|
|||||||
const app = express();
|
const app = express();
|
||||||
const pool = String(process.env.BRIDGE_PUBLIC_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 PORT = Number(process.env.PLAYER_PORT || 8081);
|
||||||
const PLAYER_PUBLIC_URL = String(process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
const PLAYER_PUBLIC_URL = String(process.env.PLAYER_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||||
const BRIDGE_PUBLIC_URL = String(process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
const BRIDGE_PUBLIC_URL = String(process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||||
const WEB_INTERNAL_URL = String(process.env.WEB_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
const WEB_INTERNAL_URL = String(process.env.WEB_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||||
const isRemotePlayer = Boolean(BRIDGE_PUBLIC_URL);
|
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_INTERNAL_URL = String(isRemotePlayer ? BRIDGE_PUBLIC_URL : (process.env.PLAYER_INTERNAL_URL || '')).trim().replace(/\/$/, '');
|
||||||
const PLAYER_DEVICE_ID = getConfiguredPlayerIdentifier();
|
const PLAYER_DEVICE_ID = getConfiguredPlayerIdentifier();
|
||||||
const PLAYER_AGENT_RECONNECT_DELAY_MS = Number(process.env.PLAYER_AGENT_RECONNECT_DELAY_MS || 5000);
|
const PLAYER_AGENT_RECONNECT_DELAY_MS = Number(process.env.PLAYER_AGENT_RECONNECT_DELAY_MS || 5000);
|
||||||
const ASSET_DIR = path.join(__dirname, 'player', 'public');
|
const ASSET_DIR = path.join(__dirname, 'player', 'public');
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function getPublicBaseUrl(req, configuredUrl) {
|
|||||||
return `${protocol}://${host}`.replace(/\/$/, '');
|
return `${protocol}://${host}`.replace(/\/$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const configured = String(configuredUrl || process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
const configured = String(configuredUrl || process.env.PLAYER_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||||
return configured || null;
|
return configured || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ function getPlayerPublicBaseUrl(req, configuredUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPlayerInternalBaseUrl(configuredUrl) {
|
function getPlayerInternalBaseUrl(configuredUrl) {
|
||||||
const configured = String(configuredUrl || process.env.PLAYER_INTERNAL_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
const configured = String(configuredUrl || process.env.PLAYER_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||||
return configured || null;
|
return configured || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
|||||||
const common = options && options.common ? options.common : null;
|
const common = options && options.common ? options.common : null;
|
||||||
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
||||||
const onboardingStore = options && options.onboardingStore ? options.onboardingStore : null;
|
const onboardingStore = options && options.onboardingStore ? options.onboardingStore : null;
|
||||||
const playerPublicUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
const playerPublicUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||||
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||||
const playerDeviceId = normalizeDeviceId(options && options.playerDeviceId);
|
const playerDeviceId = normalizeDeviceId(options && options.playerDeviceId);
|
||||||
const onPairingCode = options && typeof options.onPairingCode === 'function' ? options.onPairingCode : null;
|
const onPairingCode = options && typeof options.onPairingCode === 'function' ? options.onPairingCode : null;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function registerPlayerRoutes(app, options) {
|
|||||||
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
||||||
const playerPlaylistService = options && options.playerPlaylistService ? options.playerPlaylistService : null;
|
const playerPlaylistService = options && options.playerPlaylistService ? options.playerPlaylistService : null;
|
||||||
const rtmpStreamService = options && options.rtmpStreamService ? options.rtmpStreamService : null;
|
const rtmpStreamService = options && options.rtmpStreamService ? options.rtmpStreamService : null;
|
||||||
const playerInternalUrl = String(options && options.playerInternalBaseUrl || process.env.PLAYER_INTERNAL_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
const playerInternalUrl = String(options && options.playerInternalBaseUrl || process.env.PLAYER_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||||
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||||
const playerDeviceId = String(options && options.playerDeviceId || '').trim() || null;
|
const playerDeviceId = String(options && options.playerDeviceId || '').trim() || null;
|
||||||
const snapshotDir = options && options.snapshotDir ? path.resolve(String(options.snapshotDir)) : null;
|
const snapshotDir = options && options.snapshotDir ? path.resolve(String(options.snapshotDir)) : null;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ function createWebConfig() {
|
|||||||
const uploadsDir = path.join(mediaDir, 'uploads');
|
const uploadsDir = path.join(mediaDir, 'uploads');
|
||||||
const thumbnailsDir = path.join(mediaDir, 'thumbnails');
|
const thumbnailsDir = path.join(mediaDir, 'thumbnails');
|
||||||
const assetDir = path.join(__dirname, '..', 'public');
|
const assetDir = path.join(__dirname, '..', 'public');
|
||||||
const playerInternalUrl = (process.env.PLAYER_INTERNAL_URL || process.env.PLAYER_BASE_URL || 'http://player:8081').replace(/\/$/, '');
|
const playerInternalUrl = (process.env.PLAYER_INTERNAL_URL || 'http://player:8081').replace(/\/$/, '');
|
||||||
const bridgeInternalUrl = (process.env.BRIDGE_INTERNAL_URL || 'http://player-bridge:8090').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 webInternalUrl = (process.env.WEB_INTERNAL_URL || `http://127.0.0.1:${Number(process.env.WEB_PORT || 8080)}`).replace(/\/$/, '');
|
||||||
const sessionCookieName = 'digital_signage_session';
|
const sessionCookieName = 'digital_signage_session';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
const common = deps.common;
|
const common = deps.common;
|
||||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||||
const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl;
|
const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl;
|
||||||
|
const forwardPlayerCommandToDevice = deps.forwardPlayerCommandToDevice;
|
||||||
const getScreenConnections = deps.getScreenConnections;
|
const getScreenConnections = deps.getScreenConnections;
|
||||||
const isClientNameAvailable = deps.isClientNameAvailable;
|
const isClientNameAvailable = deps.isClientNameAvailable;
|
||||||
const findAvailableClientName = deps.findAvailableClientName;
|
const findAvailableClientName = deps.findAvailableClientName;
|
||||||
@@ -104,12 +105,51 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function forwardPlayerCommandForConnections(screenSlug, connections, commandPayload, connectionId, deviceId) {
|
async function forwardPlayerCommandForConnections(screenSlug, connections, commandPayload, connectionId, deviceId) {
|
||||||
const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(connections);
|
const connectionList = Array.isArray(connections) ? connections : [];
|
||||||
|
const targetedConnections = connectionId
|
||||||
|
? connectionList.filter(function (connection) {
|
||||||
|
const candidateConnectionId = String(connection && connection.id || '').trim();
|
||||||
|
const candidateClientId = String(connection && connection.clientId || '').trim();
|
||||||
|
const candidateDeviceId = String(connection && connection.deviceId || '').trim();
|
||||||
|
return candidateConnectionId === connectionId
|
||||||
|
|| candidateClientId === connectionId
|
||||||
|
|| candidateDeviceId === connectionId;
|
||||||
|
})
|
||||||
|
: (deviceId
|
||||||
|
? connectionList.filter(function (connection) {
|
||||||
|
return String(connection && connection.deviceId || '').trim() === String(deviceId).trim();
|
||||||
|
})
|
||||||
|
: connectionList);
|
||||||
|
const remoteConnections = targetedConnections.filter(function (connection) {
|
||||||
|
return String(connection && connection.playerDeviceId || '').trim();
|
||||||
|
});
|
||||||
|
const localConnections = targetedConnections.filter(function (connection) {
|
||||||
|
return !String(connection && connection.playerDeviceId || '').trim();
|
||||||
|
});
|
||||||
|
const deviceTargets = Array.from(new Set(remoteConnections.map(function (connection) {
|
||||||
|
return String(connection && connection.playerDeviceId || '').trim();
|
||||||
|
}).filter(Boolean)));
|
||||||
|
const results = [];
|
||||||
|
if (deviceTargets.length && typeof forwardPlayerCommandToDevice === 'function') {
|
||||||
|
const remoteResults = await Promise.allSettled(deviceTargets.map(function (targetDeviceId) {
|
||||||
|
const payload = Object.assign({ screenSlug: screenSlug }, commandPayload || {});
|
||||||
|
if (connectionId) {
|
||||||
|
payload.connectionId = connectionId;
|
||||||
|
}
|
||||||
|
return forwardPlayerCommandToDevice(targetDeviceId, payload);
|
||||||
|
}));
|
||||||
|
results.push.apply(results, remoteResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(localConnections);
|
||||||
if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||||
const results = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) {
|
const localResults = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) {
|
||||||
return forwardPlayerCommandToBaseUrl(targetBaseUrl, screenSlug, commandPayload, connectionId || deviceId || undefined);
|
return forwardPlayerCommandToBaseUrl(targetBaseUrl, screenSlug, commandPayload, connectionId || deviceId || undefined);
|
||||||
}));
|
}));
|
||||||
|
results.push.apply(results, localResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
sent: results.filter(function (result) {
|
sent: results.filter(function (result) {
|
||||||
@@ -373,6 +413,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
return candidateConnectionId === connectionId || candidateClientId === connectionId;
|
return candidateConnectionId === connectionId || candidateClientId === connectionId;
|
||||||
}) || null;
|
}) || null;
|
||||||
const liveDeviceId = String(liveConnection && liveConnection.deviceId || '').trim();
|
const liveDeviceId = String(liveConnection && liveConnection.deviceId || '').trim();
|
||||||
|
const livePlayerDeviceId = String(liveConnection && liveConnection.playerDeviceId || '').trim();
|
||||||
const livePlayerBaseUrl = normalizeExplicitPlayerBaseUrl(liveConnection && liveConnection.playerPublicBaseUrl);
|
const livePlayerBaseUrl = normalizeExplicitPlayerBaseUrl(liveConnection && liveConnection.playerPublicBaseUrl);
|
||||||
|
|
||||||
if (!connectionId && !submittedPlayerBaseUrl && !legacyDeviceId && !liveDeviceId) {
|
if (!connectionId && !submittedPlayerBaseUrl && !legacyDeviceId && !liveDeviceId) {
|
||||||
@@ -382,7 +423,9 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
return res.status(400).json({ error: 'Target screen is required' });
|
return res.status(400).json({ error: 'Target screen is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (livePlayerBaseUrl || submittedPlayerBaseUrl) {
|
if (livePlayerDeviceId) {
|
||||||
|
physicalPlayerId = livePlayerDeviceId;
|
||||||
|
} else if (livePlayerBaseUrl || submittedPlayerBaseUrl) {
|
||||||
const [playerRows] = await pool.query(
|
const [playerRows] = await pool.query(
|
||||||
'SELECT identifier, public_base_url FROM d_players WHERE public_base_url = ? LIMIT 1',
|
'SELECT identifier, public_base_url FROM d_players WHERE public_base_url = ? LIMIT 1',
|
||||||
[livePlayerBaseUrl || submittedPlayerBaseUrl]
|
[livePlayerBaseUrl || submittedPlayerBaseUrl]
|
||||||
@@ -523,7 +566,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
|| candidateDeviceId === physicalPlayerId;
|
|| candidateDeviceId === physicalPlayerId;
|
||||||
}) || liveConnections[0] || null
|
}) || liveConnections[0] || null
|
||||||
: null;
|
: null;
|
||||||
const sourcePlayerBaseUrl = registeredPlayerBaseUrl
|
const sourcePlayerBaseUrl = livePlayerDeviceId ? '' : registeredPlayerBaseUrl
|
||||||
|| normalizeExplicitPlayerBaseUrl(matchedLiveConnection && matchedLiveConnection.playerPublicBaseUrl);
|
|| normalizeExplicitPlayerBaseUrl(matchedLiveConnection && matchedLiveConnection.playerPublicBaseUrl);
|
||||||
if (sourcePlayerBaseUrl) {
|
if (sourcePlayerBaseUrl) {
|
||||||
targetPlayerUrl = `${sourcePlayerBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}`;
|
targetPlayerUrl = `${sourcePlayerBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}`;
|
||||||
@@ -537,7 +580,13 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
connectionId: connectionId || null,
|
connectionId: connectionId || null,
|
||||||
screenSlug: targetScreenSlug
|
screenSlug: targetScreenSlug
|
||||||
});
|
});
|
||||||
if (sourcePlayerBaseUrl) {
|
if (livePlayerDeviceId) {
|
||||||
|
await forwardPlayerCommandForConnections(slug, liveConnections, {
|
||||||
|
command: 'redirect',
|
||||||
|
url: targetPlayerUrl,
|
||||||
|
moveToken: moveToken || null
|
||||||
|
}, connectionId || physicalPlayerId || undefined, physicalPlayerId || null);
|
||||||
|
} else if (sourcePlayerBaseUrl) {
|
||||||
await forwardPlayerCommandForPlayerBaseUrl(slug, sourcePlayerBaseUrl, {
|
await forwardPlayerCommandForPlayerBaseUrl(slug, sourcePlayerBaseUrl, {
|
||||||
command: 'redirect',
|
command: 'redirect',
|
||||||
url: targetPlayerUrl,
|
url: targetPlayerUrl,
|
||||||
@@ -585,9 +634,12 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
|||||||
: null;
|
: null;
|
||||||
const livePlayerBaseUrl = normalizeExplicitPlayerBaseUrl(liveConnection && liveConnection.playerPublicBaseUrl);
|
const livePlayerBaseUrl = normalizeExplicitPlayerBaseUrl(liveConnection && liveConnection.playerPublicBaseUrl);
|
||||||
const targetPlayerBaseUrl = livePlayerBaseUrl || playerBaseUrl;
|
const targetPlayerBaseUrl = livePlayerBaseUrl || playerBaseUrl;
|
||||||
const result = targetPlayerBaseUrl
|
const liveDeviceId = String(liveConnection && liveConnection.playerDeviceId || '').trim();
|
||||||
? await forwardPlayerCommandForPlayerBaseUrl(slug, targetPlayerBaseUrl, commandPayload, connectionId, null)
|
const result = liveDeviceId && typeof forwardPlayerCommandToDevice === 'function'
|
||||||
: await forwardPlayerCommandForConnections(slug, liveConnections, commandPayload, connectionId, null);
|
? await forwardPlayerCommandToDevice(liveDeviceId, Object.assign({ screenSlug: slug }, commandPayload, connectionId ? { connectionId: connectionId } : {}))
|
||||||
|
: targetPlayerBaseUrl
|
||||||
|
? await forwardPlayerCommandForPlayerBaseUrl(slug, targetPlayerBaseUrl, commandPayload, connectionId, null)
|
||||||
|
: await forwardPlayerCommandForConnections(slug, liveConnections, commandPayload, connectionId, null);
|
||||||
|
|
||||||
if (typeof broadcastDashboardState === 'function') {
|
if (typeof broadcastDashboardState === 'function') {
|
||||||
await broadcastDashboardState();
|
await broadcastDashboardState();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ module.exports = function registerManageRoutes(app, deps) {
|
|||||||
const getScreenConnections = deps.getScreenConnections;
|
const getScreenConnections = deps.getScreenConnections;
|
||||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||||
const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl;
|
const forwardPlayerCommandToBaseUrl = deps.forwardPlayerCommandToBaseUrl;
|
||||||
|
const forwardPlayerCommandToDevice = deps.forwardPlayerCommandToDevice;
|
||||||
const requirePermission = deps.requirePermission;
|
const requirePermission = deps.requirePermission;
|
||||||
|
|
||||||
const SCREEN_NAME_MAX_LENGTH = 255;
|
const SCREEN_NAME_MAX_LENGTH = 255;
|
||||||
@@ -88,12 +89,30 @@ module.exports = function registerManageRoutes(app, deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function forwardPlayerCommandForConnections(slug, connections, commandPayload) {
|
async function forwardPlayerCommandForConnections(slug, connections, commandPayload) {
|
||||||
const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(connections);
|
const connectionList = Array.isArray(connections) ? connections : [];
|
||||||
|
const remoteDeviceIds = Array.from(new Set(connectionList.map(function (connection) {
|
||||||
|
return String(connection && connection.playerDeviceId || '').trim();
|
||||||
|
}).filter(Boolean)));
|
||||||
|
const localConnections = connectionList.filter(function (connection) {
|
||||||
|
return !String(connection && connection.playerDeviceId || '').trim();
|
||||||
|
});
|
||||||
|
const results = [];
|
||||||
|
if (remoteDeviceIds.length && typeof forwardPlayerCommandToDevice === 'function') {
|
||||||
|
const remoteResults = await Promise.allSettled(remoteDeviceIds.map(function (deviceId) {
|
||||||
|
return forwardPlayerCommandToDevice(deviceId, Object.assign({ screenSlug: slug }, commandPayload || {}));
|
||||||
|
}));
|
||||||
|
results.push.apply(results, remoteResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetBaseUrls = await resolvePlayerBaseUrlsForConnections(localConnections);
|
||||||
if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
if (targetBaseUrls.length && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||||
const results = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) {
|
const localResults = await Promise.allSettled(targetBaseUrls.map(function (targetBaseUrl) {
|
||||||
return forwardPlayerCommandToBaseUrl(targetBaseUrl, slug, commandPayload);
|
return forwardPlayerCommandToBaseUrl(targetBaseUrl, slug, commandPayload);
|
||||||
}));
|
}));
|
||||||
|
results.push.apply(results, localResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
sent: results.filter(function (result) {
|
sent: results.filter(function (result) {
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ function registerSignageRoutes(app, deps) {
|
|||||||
common: deps.common,
|
common: deps.common,
|
||||||
forwardPlayerCommand: deps.playerActionService.forwardPlayerCommand,
|
forwardPlayerCommand: deps.playerActionService.forwardPlayerCommand,
|
||||||
forwardPlayerCommandToBaseUrl: deps.playerActionService.forwardPlayerCommandToBaseUrl,
|
forwardPlayerCommandToBaseUrl: deps.playerActionService.forwardPlayerCommandToBaseUrl,
|
||||||
|
forwardPlayerCommandToDevice: deps.playerActionService.forwardPlayerCommandToDevice,
|
||||||
getScreenConnections: deps.playerActionService.getScreenConnections,
|
getScreenConnections: deps.playerActionService.getScreenConnections,
|
||||||
isClientNameAvailable: deps.isClientNameAvailable,
|
isClientNameAvailable: deps.isClientNameAvailable,
|
||||||
findAvailableClientName: deps.findAvailableClientName,
|
findAvailableClientName: deps.findAvailableClientName,
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ function buildScreenFormViewModel(screen, data, message, currentUser, isEdit) {
|
|||||||
const viewScreen = Object.assign({
|
const viewScreen = Object.assign({
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
playlist_id: null,
|
playlist_id: null
|
||||||
player_urls: []
|
|
||||||
}, screen || {});
|
}, screen || {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Screen route registration and dashboard wiring.
|
// Screen route registration and dashboard wiring.
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { screenPlayerUrl } = require('../../../routes/common');
|
|
||||||
|
|
||||||
module.exports = function registerScreensRoutes(app, deps) {
|
module.exports = function registerScreensRoutes(app, deps) {
|
||||||
const pool = deps.pool;
|
const pool = deps.pool;
|
||||||
@@ -12,14 +11,6 @@ module.exports = function registerScreensRoutes(app, deps) {
|
|||||||
const { buildPagination } = require('../../../lib/pagination');
|
const { buildPagination } = require('../../../lib/pagination');
|
||||||
const requirePermission = deps.requirePermission;
|
const requirePermission = deps.requirePermission;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LIST_PAGE_SIZE = 25;
|
const LIST_PAGE_SIZE = 25;
|
||||||
const batTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.bat');
|
const batTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.bat');
|
||||||
const shTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.sh');
|
const shTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.sh');
|
||||||
@@ -90,26 +81,6 @@ module.exports = function registerScreensRoutes(app, deps) {
|
|||||||
return common.fetchPlayerPublicBaseUrl(pool);
|
return common.fetchPlayerPublicBaseUrl(pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildScreenPlayerUrls(screen) {
|
|
||||||
const playerRegistrations = typeof common.fetchPlayerRegistrations === 'function'
|
|
||||||
? await common.fetchPlayerRegistrations(pool)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (Array.isArray(playerRegistrations) ? playerRegistrations : []).filter(function (player) {
|
|
||||||
return isRecentPlayerRegistration(player, 60);
|
|
||||||
}).map(function (player) {
|
|
||||||
const baseUrl = String(player && player.public_base_url || '').trim().replace(/\/$/, '');
|
|
||||||
const playerUrl = screenPlayerUrl(screen && screen.slug ? screen.slug : '', baseUrl);
|
|
||||||
return {
|
|
||||||
identifier: String(player && player.identifier || '').trim(),
|
|
||||||
public_base_url: baseUrl || null,
|
|
||||||
player_url: playerUrl || null
|
|
||||||
};
|
|
||||||
}).sort(function (left, right) {
|
|
||||||
return String(left && left.identifier || '').localeCompare(String(right && right.identifier || ''), undefined, { sensitivity: 'base', numeric: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLauncherContent(templatePath, playerUrl) {
|
function buildLauncherContent(templatePath, playerUrl) {
|
||||||
const template = fs.readFileSync(templatePath, 'utf8');
|
const template = fs.readFileSync(templatePath, 'utf8');
|
||||||
const normalizedPlayerUrl = String(playerUrl || '').trim();
|
const normalizedPlayerUrl = String(playerUrl || '').trim();
|
||||||
@@ -139,10 +110,6 @@ module.exports = function registerScreensRoutes(app, deps) {
|
|||||||
if (!screen) {
|
if (!screen) {
|
||||||
return res.status(404).send('Screen not found');
|
return res.status(404).send('Screen not found');
|
||||||
}
|
}
|
||||||
screen.player_urls = await buildScreenPlayerUrls(screen);
|
|
||||||
if (screen.player_urls.length) {
|
|
||||||
screen.launcher_downloads = launcherDownloadPaths;
|
|
||||||
}
|
|
||||||
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
|
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
|
||||||
const editData = await common.fetchScreenEditData(pool);
|
const editData = await common.fetchScreenEditData(pool);
|
||||||
return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser));
|
return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||||
@@ -168,10 +135,6 @@ module.exports = function registerScreensRoutes(app, deps) {
|
|||||||
if (!screen) {
|
if (!screen) {
|
||||||
return res.status(404).send('Screen not found');
|
return res.status(404).send('Screen not found');
|
||||||
}
|
}
|
||||||
screen.player_urls = await buildScreenPlayerUrls(screen);
|
|
||||||
if (screen.player_urls.length) {
|
|
||||||
screen.launcher_downloads = launcherDownloadPaths;
|
|
||||||
}
|
|
||||||
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
|
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
|
||||||
const editData = await common.fetchScreenEditData(pool);
|
const editData = await common.fetchScreenEditData(pool);
|
||||||
return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser));
|
return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>{{#if isEdit}}Edit screen group - {{screen.name}}{{else}}Add screen group{{/if}}</h2>
|
<h2>{{#if isEdit}}Edit screen group - {{screen.name}}{{else}}Add screen group{{/if}}</h2>
|
||||||
<p>{{#if isEdit}}Update the display name and assigned playlist.{{else}}Register a player endpoint and connect it to a playlist.{{/if}}</p>
|
<p>{{#if isEdit}}Update the display name and assigned playlist.{{else}}Create a screen group and connect it to a playlist.{{/if}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-12 col-xl-6">
|
<div class="col-12">
|
||||||
<div class="card card-outline card-primary admin-form-card h-100">
|
<div class="card card-outline card-primary admin-form-card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">Screen group details</h3>
|
<h3 class="card-title">Screen group details</h3>
|
||||||
@@ -40,39 +40,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-xl-6">
|
|
||||||
<div class="card card-outline card-secondary admin-form-card h-100">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="card-title">Player URLs</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body table-responsive p-0">
|
|
||||||
{{#if screen.player_urls.length}}
|
|
||||||
<table class="table table-striped w-100 mb-0" data-table-searchable>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Player</th>
|
|
||||||
<th>URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each screen.player_urls}}
|
|
||||||
<tr>
|
|
||||||
<td data-label="Player" class="text-break">{{identifier}}</td>
|
|
||||||
<td class="text-break">
|
|
||||||
{{#if player_url}}
|
|
||||||
<a href="{{player_url}}" target="_blank" rel="noreferrer">{{player_url}}</a>
|
|
||||||
{{else}}
|
|
||||||
<span class="text-muted">Not available</span>
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{else}}
|
|
||||||
<p class="mb-0 p-3 text-muted">No player URLs are available yet.</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -210,6 +210,94 @@ test('move client requires a registered player identity', async () => {
|
|||||||
assert.equal(response.body.error, 'Registered player identity is required');
|
assert.equal(response.body.error, 'Registered player identity is required');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('move client routes a remote connection through its bridge player identity', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const { app, handlers } = createHandlers();
|
||||||
|
|
||||||
|
registerScreenCommandRoutes(app, {
|
||||||
|
pool: {
|
||||||
|
async query(sql, params) {
|
||||||
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?') && params[0] === 'source-screen') {
|
||||||
|
return [[{ id: 12, name: 'Source Screen', slug: 'source-screen' }]];
|
||||||
|
}
|
||||||
|
if (sql.includes('SELECT d.client_name, s.slug AS current_screen_slug')) {
|
||||||
|
return [[{ client_name: 'Remote Client', current_screen_slug: 'source-screen' }]];
|
||||||
|
}
|
||||||
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?') && params[0] === 'target-screen') {
|
||||||
|
return [[{ id: 27, name: 'Target Screen', slug: 'target-screen' }]];
|
||||||
|
}
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
common: { fetchPlayerRegistrations: async () => [] },
|
||||||
|
forwardPlayerCommand() {
|
||||||
|
throw new Error('direct player fallback should not be used');
|
||||||
|
},
|
||||||
|
forwardPlayerCommandToBaseUrl() {
|
||||||
|
throw new Error('public URL routing should not be used');
|
||||||
|
},
|
||||||
|
forwardPlayerCommandToDevice(deviceId, payload) {
|
||||||
|
calls.push({ deviceId, payload });
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
getScreenConnections: async () => ({
|
||||||
|
connections: [{
|
||||||
|
id: 'connection-1',
|
||||||
|
clientId: 'connection-1',
|
||||||
|
deviceId: 'browser-tab-1',
|
||||||
|
playerDeviceId: 'remote-player-1',
|
||||||
|
playerPublicBaseUrl: null
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
isClientNameAvailable: async () => true,
|
||||||
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
||||||
|
broadcastDashboardState: async () => {},
|
||||||
|
requirePermission() {
|
||||||
|
return function (_req, _res, next) {
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
statusCode: 200,
|
||||||
|
body: null,
|
||||||
|
status(code) {
|
||||||
|
this.statusCode = code;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
json(value) {
|
||||||
|
this.body = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await handlers['/clients/:slug/commands'][1]({
|
||||||
|
params: { slug: 'source-screen' },
|
||||||
|
body: {
|
||||||
|
command: 'moveclient',
|
||||||
|
clientId: 'browser-tab-1',
|
||||||
|
clientName: 'Remote Client',
|
||||||
|
targetScreenSlug: 'target-screen',
|
||||||
|
connectionId: 'connection-1'
|
||||||
|
},
|
||||||
|
query: {}
|
||||||
|
}, response, (error) => { throw error; });
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.equal(response.body.ok, true);
|
||||||
|
assert.deepEqual(calls, [{
|
||||||
|
deviceId: 'remote-player-1',
|
||||||
|
payload: {
|
||||||
|
screenSlug: 'source-screen',
|
||||||
|
command: 'redirect',
|
||||||
|
url: '/screen/target-screen',
|
||||||
|
moveToken: calls[0] && calls[0].payload.moveToken,
|
||||||
|
connectionId: 'connection-1'
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
test('screen control commands can target all screens', async () => {
|
test('screen control commands can target all screens', async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const { app, handlers } = createHandlers();
|
const { app, handlers } = createHandlers();
|
||||||
|
|||||||
@@ -195,3 +195,56 @@ test('client command route forwards a single screen command', async () => {
|
|||||||
assert.equal(calls[0].connectionId, undefined);
|
assert.equal(calls[0].connectionId, undefined);
|
||||||
assert.equal(response.body.ok, true);
|
assert.equal(response.body.ok, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('client command route uses the bridge device route without a public player URL', async () => {
|
||||||
|
const { app, handlers } = createAppHarness();
|
||||||
|
const calls = [];
|
||||||
|
registerScreenCommandRoutes(app, {
|
||||||
|
pool: {
|
||||||
|
async query(sql) {
|
||||||
|
if (String(sql).includes('SELECT id, name, slug FROM d_screens WHERE slug = ?')) {
|
||||||
|
return [[{ id: 1, name: 'Lobby', slug: 'lobby' }]];
|
||||||
|
}
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
common: { async fetchPlayerRegistrations() { return []; } },
|
||||||
|
forwardPlayerCommand() {
|
||||||
|
throw new Error('direct player fallback should not be used');
|
||||||
|
},
|
||||||
|
forwardPlayerCommandToBaseUrl() {
|
||||||
|
throw new Error('public URL fallback should not be used');
|
||||||
|
},
|
||||||
|
forwardPlayerCommandToDevice(deviceId, payload) {
|
||||||
|
calls.push({ deviceId, payload });
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
getScreenConnections: async () => ({
|
||||||
|
connections: [{ id: 'connection-1', deviceId: 'browser-tab-1', playerDeviceId: 'remote-player-1', playerPublicBaseUrl: null }]
|
||||||
|
}),
|
||||||
|
isClientNameAvailable() { return true; },
|
||||||
|
withClientNameReservation() {},
|
||||||
|
broadcastDashboardState() {},
|
||||||
|
requirePermission() {
|
||||||
|
return function (_req, _res, next) { next(); };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
statusCode: 200,
|
||||||
|
body: null,
|
||||||
|
status(code) { this.statusCode = code; return this; },
|
||||||
|
json(payload) { this.body = payload; return this; }
|
||||||
|
};
|
||||||
|
await handlers['/clients/:slug/commands'][1]({
|
||||||
|
params: { slug: 'lobby' },
|
||||||
|
body: { command: 'pause', connectionId: 'connection-1' },
|
||||||
|
query: {}
|
||||||
|
}, response, () => {});
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(calls, [{
|
||||||
|
deviceId: 'remote-player-1',
|
||||||
|
payload: { screenSlug: 'lobby', command: 'pause', connectionId: 'connection-1' }
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ require('../src/common');
|
|||||||
const registerScreensRoutes = require('../src/web/routes/signage/screens/routes');
|
const registerScreensRoutes = require('../src/web/routes/signage/screens/routes');
|
||||||
const { renderScreenEditPage } = require('../src/web/pages');
|
const { renderScreenEditPage } = require('../src/web/pages');
|
||||||
|
|
||||||
test('screen edit page includes shared launcher downloads and base player url', async () => {
|
test('screen edit page omits player URL data', async () => {
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
const app = {
|
const app = {
|
||||||
get(path, ...routeHandlers) {
|
get(path, ...routeHandlers) {
|
||||||
@@ -88,25 +88,11 @@ test('screen edit page includes shared launcher downloads and base player url',
|
|||||||
await handler[1]({ params: { id: '7' }, query: {}, currentUser: { id: 1 } }, res, () => {});
|
await handler[1]({ params: { id: '7' }, query: {}, currentUser: { id: 1 } }, res, () => {});
|
||||||
|
|
||||||
assert.equal(res.body, 'ok');
|
assert.equal(res.body, 'ok');
|
||||||
assert.deepEqual(renderedArgs.screen.player_urls, [
|
assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'player_urls'), false);
|
||||||
{
|
assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'launcher_downloads'), false);
|
||||||
identifier: 'player-alpha',
|
|
||||||
public_base_url: 'http://alpha.example',
|
|
||||||
player_url: 'http://alpha.example/screen/demo-conference'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: 'player-beta',
|
|
||||||
public_base_url: 'http://beta.example',
|
|
||||||
player_url: 'http://beta.example/screen/demo-conference'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
assert.deepEqual(renderedArgs.screen.launcher_downloads, {
|
|
||||||
windows: '/downloads/kiosk/pulse-signage-kiosk.bat',
|
|
||||||
linux: '/downloads/kiosk/pulse-signage-kiosk.sh'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('screen edit query route includes player urls for every registration', async () => {
|
test('screen edit query route omits player URLs', async () => {
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
const app = {
|
const app = {
|
||||||
get(path, ...routeHandlers) {
|
get(path, ...routeHandlers) {
|
||||||
@@ -172,20 +158,11 @@ test('screen edit query route includes player urls for every registration', asyn
|
|||||||
await handler[1]({ query: { edit: '7' }, currentUser: { id: 1 } }, res, () => {});
|
await handler[1]({ query: { edit: '7' }, currentUser: { id: 1 } }, res, () => {});
|
||||||
|
|
||||||
assert.equal(res.body, 'ok');
|
assert.equal(res.body, 'ok');
|
||||||
assert.deepEqual(renderedArgs.screen.player_urls, [
|
assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'player_urls'), false);
|
||||||
{
|
assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'launcher_downloads'), false);
|
||||||
identifier: 'player-alpha',
|
|
||||||
public_base_url: 'http://alpha.example',
|
|
||||||
player_url: 'http://alpha.example/screen/demo-conference'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
assert.deepEqual(renderedArgs.screen.launcher_downloads, {
|
|
||||||
windows: '/downloads/kiosk/pulse-signage-kiosk.bat',
|
|
||||||
linux: '/downloads/kiosk/pulse-signage-kiosk.sh'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('screen edit page hides stale player registrations', async () => {
|
test('screen edit page does not load player URL registrations', async () => {
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
const app = {
|
const app = {
|
||||||
get(path, ...routeHandlers) {
|
get(path, ...routeHandlers) {
|
||||||
@@ -258,22 +235,10 @@ test('screen edit page hides stale player registrations', async () => {
|
|||||||
await handler[1]({ params: { id: '7' }, query: {}, currentUser: { id: 1 } }, res, () => {});
|
await handler[1]({ params: { id: '7' }, query: {}, currentUser: { id: 1 } }, res, () => {});
|
||||||
|
|
||||||
assert.equal(res.body, 'ok');
|
assert.equal(res.body, 'ok');
|
||||||
assert.deepEqual(renderedArgs.screen.player_urls.map(function (player) {
|
assert.equal(Object.prototype.hasOwnProperty.call(renderedArgs.screen, 'player_urls'), false);
|
||||||
return {
|
|
||||||
identifier: player.identifier,
|
|
||||||
public_base_url: player.public_base_url,
|
|
||||||
player_url: player.player_url
|
|
||||||
};
|
|
||||||
}), [
|
|
||||||
{
|
|
||||||
identifier: 'player-recent',
|
|
||||||
public_base_url: 'http://recent.example',
|
|
||||||
player_url: 'http://recent.example/screen/demo-conference'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('screen edit page renders player urls as an adminlte table', async () => {
|
test('screen edit page does not render a player URL table', async () => {
|
||||||
const html = renderScreenEditPage(
|
const html = renderScreenEditPage(
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
@@ -282,24 +247,14 @@ test('screen edit page renders player urls as an adminlte table', async () => {
|
|||||||
playlist_id: null,
|
playlist_id: null,
|
||||||
slug_update_confirm_live_connection_count: 2,
|
slug_update_confirm_live_connection_count: 2,
|
||||||
slug_update_confirm_message: 'Are you sure you want to update the slug? This will refresh all screens using this slug.',
|
slug_update_confirm_message: 'Are you sure you want to update the slug? This will refresh all screens using this slug.',
|
||||||
player_urls: [
|
|
||||||
{
|
|
||||||
identifier: 'player-alpha',
|
|
||||||
public_base_url: 'http://alpha.example',
|
|
||||||
player_url: 'http://alpha.example/screen/demo-conference'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{ playlists: [] },
|
{ playlists: [] },
|
||||||
'',
|
'',
|
||||||
{ id: 1 }
|
{ id: 1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.match(html, /Player URLs/);
|
assert.doesNotMatch(html, /Player URLs/);
|
||||||
assert.match(html, /<table/i);
|
assert.doesNotMatch(html, /player-alpha/);
|
||||||
assert.match(html, /card-body table-responsive p-0/);
|
|
||||||
assert.match(html, /table table-striped w-100 mb-0/);
|
|
||||||
assert.match(html, /player-alpha/);
|
|
||||||
assert.match(html, /<input[^>]+id="screen-slug"[^>]+disabled/);
|
assert.match(html, /<input[^>]+id="screen-slug"[^>]+disabled/);
|
||||||
assert.match(html, /Slug cannot be changed after creation/);
|
assert.match(html, /Slug cannot be changed after creation/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user