Release v2.10.6
This commit is contained in:
Vendored
+1
@@ -121,6 +121,7 @@ function createWebBootstrap(options) {
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerInternalBaseUrl: configuredPlayerInternalUrl,
|
||||
bridgeInternalBaseUrl: configuredBridgeInternalUrl,
|
||||
playerSnapshotCache: playerSnapshotCache,
|
||||
notifyPlayerScreens: notifyPlayerScreens,
|
||||
backgroundTaskQueue: backgroundTaskQueue
|
||||
|
||||
@@ -36,22 +36,6 @@ function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function appendPlayerDeviceIdToUrl(baseUrl, playerIdentifier) {
|
||||
const targetBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
const deviceId = String(playerIdentifier || '').trim();
|
||||
if (!targetBaseUrl || !deviceId) {
|
||||
return targetBaseUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(targetBaseUrl);
|
||||
url.searchParams.set('deviceId', deviceId);
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch (_error) {
|
||||
return targetBaseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePlayerRowBaseUrl(player) {
|
||||
return normalizeBaseUrl(player && player.internal_base_url);
|
||||
}
|
||||
@@ -68,6 +52,7 @@ function createUploadSyncService(options) {
|
||||
const pool = options && options.pool;
|
||||
const common = options && options.common;
|
||||
const configuredPlayerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||
const configuredBridgeInternalBaseUrl = String(options && options.bridgeInternalBaseUrl || '').trim().replace(/\/$/, '');
|
||||
const playerSnapshotCache = options && options.playerSnapshotCache;
|
||||
const notifyPlayerScreens = options && options.notifyPlayerScreens;
|
||||
const backgroundTaskQueue = options && options.backgroundTaskQueue;
|
||||
@@ -545,6 +530,9 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
|
||||
const playerMetadata = await getPlayerTaskMetadata(preferredPlayerIdentifier, targetBaseUrl);
|
||||
const mediaBaseUrl = playerMetadata && playerMetadata.playerIdentifier && !isLocalLikeBaseUrl(targetBaseUrl) && configuredBridgeInternalBaseUrl
|
||||
? configuredBridgeInternalBaseUrl
|
||||
: targetBaseUrl;
|
||||
const relativePath = getUploadRelativePath(uploadPath);
|
||||
const sourcePath = resolveUploadFilePath(localUploadDir, uploadPath);
|
||||
if (!relativePath || !sourcePath) {
|
||||
@@ -566,11 +554,13 @@ function createUploadSyncService(options) {
|
||||
pathname: `/api/media/${encodeURIComponent(relativePath)}`,
|
||||
body: fileBuffer
|
||||
});
|
||||
const mediaUploadUrl = appendPlayerDeviceIdToUrl(`${targetBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, playerMetadata && playerMetadata.playerIdentifier);
|
||||
const mediaUploadUrl = `${mediaBaseUrl}/api/media/${encodeURIComponent(relativePath)}`;
|
||||
const playerDeviceId = String(playerMetadata && playerMetadata.playerIdentifier || '').trim();
|
||||
const response = await fetch(mediaUploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
...(playerDeviceId ? { 'x-pulse-player-device-id': playerDeviceId } : {}),
|
||||
...authHeaders
|
||||
},
|
||||
body: fileBuffer
|
||||
@@ -601,6 +591,9 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
|
||||
const playerMetadata = await getPlayerTaskMetadata(preferredPlayerIdentifier, targetBaseUrl);
|
||||
const mediaBaseUrl = playerMetadata && playerMetadata.playerIdentifier && !isLocalLikeBaseUrl(targetBaseUrl) && configuredBridgeInternalBaseUrl
|
||||
? configuredBridgeInternalBaseUrl
|
||||
: targetBaseUrl;
|
||||
const relativePath = getUploadRelativePath(uploadPath);
|
||||
if (!relativePath) {
|
||||
return false;
|
||||
@@ -610,11 +603,13 @@ function createUploadSyncService(options) {
|
||||
method: 'DELETE',
|
||||
pathname: `/api/media/${encodeURIComponent(relativePath)}`
|
||||
});
|
||||
const mediaDeleteUrl = appendPlayerDeviceIdToUrl(`${targetBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, playerMetadata && playerMetadata.playerIdentifier);
|
||||
const mediaDeleteUrl = `${mediaBaseUrl}/api/media/${encodeURIComponent(relativePath)}`;
|
||||
const playerDeviceId = String(playerMetadata && playerMetadata.playerIdentifier || '').trim();
|
||||
const response = await fetch(mediaDeleteUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(playerDeviceId ? { 'x-pulse-player-device-id': playerDeviceId } : {}),
|
||||
...authHeaders
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Route screen refresh commands to the physical player hosting each screen.
|
||||
|
||||
function createNotifyPlayerScreens(forwardPlayerCommand, getScreenConnections, forwardPlayerCommandToBaseUrl, resolvePlayerBaseUrl) {
|
||||
function createNotifyPlayerScreens(forwardPlayerCommand, getScreenConnections, forwardPlayerCommandToBaseUrl, resolvePlayerBaseUrl, forwardPlayerCommandToDevice) {
|
||||
if (typeof forwardPlayerCommand !== 'function') {
|
||||
throw new Error('createNotifyPlayerScreens requires a player command sender.');
|
||||
}
|
||||
@@ -19,17 +19,35 @@ function createNotifyPlayerScreens(forwardPlayerCommand, getScreenConnections, f
|
||||
if (typeof getScreenConnections === 'function' && typeof forwardPlayerCommandToBaseUrl === 'function') {
|
||||
const screenState = await getScreenConnections(slug);
|
||||
const connections = Array.isArray(screenState && screenState.connections) ? screenState.connections : [];
|
||||
const deviceIds = Array.from(new Set(connections.map(function (connection) {
|
||||
return String(connection && connection.playerDeviceId || '').trim();
|
||||
}).filter(Boolean)));
|
||||
const playerBaseUrls = Array.from(new Set(connections.map(function (connection) {
|
||||
if (String(connection && connection.playerDeviceId || '').trim()) {
|
||||
return '';
|
||||
}
|
||||
return String(connection && connection.playerPublicBaseUrl || '').trim().replace(/\/$/, '');
|
||||
}).filter(Boolean)));
|
||||
const deviceResults = deviceIds.length && typeof forwardPlayerCommandToDevice === 'function'
|
||||
? await Promise.all(deviceIds.map(function (deviceId) {
|
||||
return forwardPlayerCommandToDevice(deviceId, {
|
||||
command: commandOrPayload || 'refresh',
|
||||
screenSlug: slug
|
||||
});
|
||||
}))
|
||||
: [];
|
||||
let baseUrlResults = [];
|
||||
if (playerBaseUrls.length) {
|
||||
const resolvedPlayerBaseUrls = typeof resolvePlayerBaseUrl === 'function'
|
||||
? await Promise.all(playerBaseUrls.map(function (playerBaseUrl) { return resolvePlayerBaseUrl(playerBaseUrl); }))
|
||||
: playerBaseUrls;
|
||||
return Promise.all(resolvedPlayerBaseUrls.filter(Boolean).map(function (playerBaseUrl) {
|
||||
baseUrlResults = await Promise.all(resolvedPlayerBaseUrls.filter(Boolean).map(function (playerBaseUrl) {
|
||||
return forwardPlayerCommandToBaseUrl(playerBaseUrl, slug, commandOrPayload || 'refresh');
|
||||
}));
|
||||
}
|
||||
if (deviceResults.length || baseUrlResults.length) {
|
||||
return deviceResults.concat(baseUrlResults);
|
||||
}
|
||||
}
|
||||
return forwardPlayerCommand(slug, commandOrPayload || 'refresh');
|
||||
})).then(function (results) {
|
||||
|
||||
@@ -243,6 +243,23 @@ function createPlayerActionService(options) {
|
||||
}
|
||||
|
||||
async function forwardAnnouncementRefresh(slug) {
|
||||
try {
|
||||
const screenState = await getScreenConnections(slug);
|
||||
const deviceIds = Array.from(new Set((screenState && Array.isArray(screenState.connections) ? screenState.connections : []).map(function (connection) {
|
||||
return String(connection && connection.playerDeviceId || '').trim();
|
||||
}).filter(Boolean)));
|
||||
if (deviceIds.length) {
|
||||
const deviceResults = await Promise.all(deviceIds.map(function (deviceId) {
|
||||
return forwardPlayerCommandToDevice(deviceId, {
|
||||
command: 'announcement-refresh',
|
||||
screenSlug: slug
|
||||
});
|
||||
}));
|
||||
return deviceResults;
|
||||
}
|
||||
} catch (_error) {
|
||||
}
|
||||
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
const targetBaseUrls = Array.from(new Set([
|
||||
resolvedPlayerInternalBaseUrl,
|
||||
|
||||
@@ -901,10 +901,6 @@
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.onboarding-pairing-card.is-pairing .card-body > :not(.onboarding-pairing-progress) {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.onboarding-pairing-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -44,6 +44,27 @@
|
||||
lines.push(new Date().toISOString().slice(11, 19) + ' ' + value);
|
||||
scannerDebugOutput.textContent = lines.slice(-8).join('\n');
|
||||
}
|
||||
function wait(milliseconds) {
|
||||
return new Promise(function (resolve) { window.setTimeout(resolve, milliseconds); });
|
||||
}
|
||||
function submitPairing(body, attempt) {
|
||||
return fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
|
||||
body: body
|
||||
}).then(function (response) {
|
||||
return response.text().then(function (text) {
|
||||
var payload = null;
|
||||
try { payload = JSON.parse(text); } catch (_error) {}
|
||||
var transientCodeError = response.status === 401 && payload && String(payload.error || '').indexOf('valid kiosk pairing code') !== -1;
|
||||
var transientTransportError = [502, 503, 504].indexOf(response.status) !== -1;
|
||||
if ((!response.ok && (transientCodeError || transientTransportError)) && attempt < 2) {
|
||||
return wait(300 * (attempt + 1)).then(function () { return submitPairing(body, attempt + 1); });
|
||||
}
|
||||
return { response: response, payload: payload };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var codeInputs = codeInputsContainer ? Array.prototype.slice.call(codeInputsContainer.querySelectorAll('[data-pairing-code-input]')) : [];
|
||||
function normalizeCode(value) {
|
||||
@@ -321,28 +342,21 @@
|
||||
if (firstEmptyInput) { firstEmptyInput.focus(); }
|
||||
return;
|
||||
}
|
||||
message.className = 'alert alert-info';
|
||||
message.textContent = 'Pairing player...';
|
||||
message.className = 'alert d-none';
|
||||
message.textContent = '';
|
||||
var pairingBody = new URLSearchParams(new FormData(form)).toString();
|
||||
if (pairingCard) { pairingCard.classList.add('is-pairing'); }
|
||||
if (pairingProgress) { pairingProgress.classList.remove('d-none'); }
|
||||
Array.prototype.slice.call(form.elements).forEach(function (element) {
|
||||
if (element !== anotherButton && element !== manualButton) { element.disabled = true; }
|
||||
});
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
|
||||
body: pairingBody
|
||||
}).then(function (response) {
|
||||
return response.text().then(function (text) {
|
||||
var payload = null;
|
||||
try { payload = JSON.parse(text); } catch (_error) {}
|
||||
submitPairing(pairingBody, 0).then(function (result) {
|
||||
var response = result.response;
|
||||
var payload = result.payload;
|
||||
if (!response.ok) {
|
||||
throw new Error(payload && payload.error ? payload.error : 'Unable to pair player.');
|
||||
}
|
||||
if (payload && payload.queued) {
|
||||
message.className = 'alert alert-info';
|
||||
message.textContent = 'Pairing is still being completed. Keep this page open and wait for confirmation.';
|
||||
return;
|
||||
}
|
||||
message.className = 'alert alert-success';
|
||||
@@ -363,7 +377,6 @@
|
||||
}
|
||||
if (anotherButtonLabel) { anotherButtonLabel.classList.remove('d-none'); }
|
||||
if (manualButton) { manualButton.classList.remove('d-none'); }
|
||||
});
|
||||
}).catch(function (error) {
|
||||
if (pairingCard) { pairingCard.classList.remove('is-pairing'); }
|
||||
if (pairingProgress) { pairingProgress.classList.add('d-none'); }
|
||||
|
||||
@@ -4,6 +4,12 @@ const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
const { renderView } = require('../view');
|
||||
|
||||
function wait(milliseconds) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function registerOnboardingRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
const playerInternalBaseUrl = String(deps.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||
@@ -42,11 +48,19 @@ module.exports = function registerOnboardingRoutes(app, deps) {
|
||||
{
|
||||
const resolvePath = '/api/onboarding/resolve?pairingCode=' + encodeURIComponent(pairingCode);
|
||||
const resolveAuthHeaders = createRequestAuthHeaders({ method: 'GET', pathname: '/api/onboarding/resolve' });
|
||||
const resolveResponse = await fetch(`${targetBaseUrl}${resolvePath}`, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({ Accept: 'application/json' }, resolveAuthHeaders)
|
||||
});
|
||||
const resolveBody = await resolveResponse.text();
|
||||
let resolveResponse = null;
|
||||
let resolveBody = '';
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
resolveResponse = await fetch(`${targetBaseUrl}${resolvePath}`, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({ Accept: 'application/json' }, resolveAuthHeaders)
|
||||
});
|
||||
resolveBody = await resolveResponse.text();
|
||||
if (resolveResponse.status !== 401 || resolveBody.indexOf('valid kiosk pairing code') === -1 || attempt === 4) {
|
||||
break;
|
||||
}
|
||||
await wait(200 * (attempt + 1));
|
||||
}
|
||||
let resolved = null;
|
||||
try { resolved = JSON.parse(resolveBody); } catch (_error) {}
|
||||
if (!resolveResponse.ok || !resolved || !resolved.deviceId) {
|
||||
@@ -61,11 +75,19 @@ module.exports = function registerOnboardingRoutes(app, deps) {
|
||||
for (const remoteRegistration of remoteRegistrations) {
|
||||
targetBaseUrl = String(remoteRegistration.internal_base_url).replace(/\/$/, '');
|
||||
const bridgeResolveHeaders = createRequestAuthHeaders({ method: 'GET', pathname: '/api/onboarding/resolve' });
|
||||
const bridgeResolveResponse = await fetch(`${targetBaseUrl}/api/onboarding/resolve?pairingCode=${encodeURIComponent(pairingCode)}`, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({ Accept: 'application/json' }, bridgeResolveHeaders)
|
||||
});
|
||||
const bridgeResolveBody = await bridgeResolveResponse.text();
|
||||
let bridgeResolveResponse = null;
|
||||
let bridgeResolveBody = '';
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
bridgeResolveResponse = await fetch(`${targetBaseUrl}/api/onboarding/resolve?pairingCode=${encodeURIComponent(pairingCode)}`, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({ Accept: 'application/json' }, bridgeResolveHeaders)
|
||||
});
|
||||
bridgeResolveBody = await bridgeResolveResponse.text();
|
||||
if (bridgeResolveResponse.status !== 401 || bridgeResolveBody.indexOf('valid kiosk pairing code') === -1 || attempt === 4) {
|
||||
break;
|
||||
}
|
||||
await wait(200 * (attempt + 1));
|
||||
}
|
||||
try { resolved = JSON.parse(bridgeResolveBody); } catch (_error) { resolved = null; }
|
||||
if (bridgeResolveResponse.ok && resolved && resolved.deviceId) {
|
||||
break;
|
||||
@@ -86,12 +108,20 @@ module.exports = function registerOnboardingRoutes(app, deps) {
|
||||
}
|
||||
const payload = { clientId: resolvedClientId, pairingCode: pairingCode, clientName: clientName, screenSlug: screenSlug };
|
||||
const authHeaders = createRequestAuthHeaders({ method: 'POST', pathname: '/api/onboarding', body: payload });
|
||||
const response = await fetch(`${targetBaseUrl}/api/onboarding`, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json', Accept: 'application/json' }, authHeaders),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const responseBody = await response.text();
|
||||
let response = null;
|
||||
let responseBody = '';
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
response = await fetch(`${targetBaseUrl}/api/onboarding`, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json', Accept: 'application/json' }, authHeaders),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
responseBody = await response.text();
|
||||
if (response.status !== 401 || responseBody.indexOf('valid kiosk pairing code') === -1 || attempt === 4) {
|
||||
break;
|
||||
}
|
||||
await wait(200 * (attempt + 1));
|
||||
}
|
||||
res.status(response.status).type(response.headers.get('content-type') || 'application/json').send(responseBody);
|
||||
} catch (error) {
|
||||
res.status(502).json({ error: error && error.message ? error.message : 'Player unavailable.' });
|
||||
|
||||
Reference in New Issue
Block a user