Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c28a1c028 | ||
|
|
c5172af62d |
@@ -2,6 +2,20 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 2.6.14 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Slide updates and template deletes now fan out media sync work across all live players instead of targeting only one player.
|
||||
|
||||
## 2.6.13 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- The player bundle now keeps the browser-only QR export dependencies lazy-loaded, so the remote player image no longer needs `puppeteer-core` or `@sparticuz/chromium` at startup.
|
||||
- Remote player startup sync now falls back to `WEB_INTERNAL_URL` when it is configured, which avoids 404s when the bridge is not the correct sync target.
|
||||
- The player startup sync and media-write info logs were removed to keep normal remote-player operation quieter.
|
||||
|
||||
## 2.6.12 - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-player",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.14",
|
||||
"private": false,
|
||||
"description": "Pulse Signage player application bundle",
|
||||
"main": "src/common.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-web",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.14",
|
||||
"private": false,
|
||||
"description": "Pulse Signage web and bridge application bundle",
|
||||
"main": "src/common.js",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.5.9",
|
||||
"version": "2.6.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pulse-signage",
|
||||
"version": "2.5.9",
|
||||
"version": "2.6.13",
|
||||
"dependencies": {
|
||||
"@sparticuz/chromium": "^137.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.14",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"repository": {
|
||||
|
||||
+7
-7
@@ -1,8 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const QRCodeStyling = require(path.join(__dirname, '..', 'web', 'public', 'vendor', 'qr-code-styling', 'qr-code-styling.js'));
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const chromiumModule = require('@sparticuz/chromium');
|
||||
const QR_PNG_WIDTH = 2048;
|
||||
const QR_STYLING_SCRIPT_PATH = path.join(__dirname, '..', 'web', 'public', 'vendor', 'qr-code-styling', 'qr-code-styling.js');
|
||||
const SYSTEM_CHROMIUM_PATHS = [
|
||||
@@ -13,11 +11,6 @@ const SYSTEM_CHROMIUM_PATHS = [
|
||||
'/usr/local/bin/chromium',
|
||||
'/snap/bin/chromium'
|
||||
].filter(Boolean);
|
||||
const chromium = chromiumModule && typeof chromiumModule.executablePath === 'function'
|
||||
? chromiumModule
|
||||
: chromiumModule && chromiumModule.default && typeof chromiumModule.default.executablePath === 'function'
|
||||
? chromiumModule.default
|
||||
: chromiumModule;
|
||||
let qrBrowserPromise = null;
|
||||
|
||||
function escapeXml(value) {
|
||||
@@ -353,6 +346,13 @@ function getQrBrowser() {
|
||||
}
|
||||
|
||||
qrBrowserPromise = (async function () {
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const chromiumModule = require('@sparticuz/chromium');
|
||||
const chromium = chromiumModule && typeof chromiumModule.executablePath === 'function'
|
||||
? chromiumModule
|
||||
: chromiumModule && chromiumModule.default && typeof chromiumModule.default.executablePath === 'function'
|
||||
? chromiumModule.default
|
||||
: chromiumModule;
|
||||
let executablePath = SYSTEM_CHROMIUM_PATHS.find(function (candidate) {
|
||||
return fs.existsSync(candidate);
|
||||
}) || '';
|
||||
|
||||
+7
-31
@@ -22,6 +22,7 @@ async function start() {
|
||||
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 BRIDGE_PUBLIC_URL = String(process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
const WEB_INTERNAL_URL = String(process.env.WEB_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||
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_DEVICE_ID = getConfiguredPlayerIdentifier();
|
||||
@@ -133,7 +134,8 @@ async function start() {
|
||||
}
|
||||
|
||||
async function triggerWebMediaSync() {
|
||||
if (!isRemotePlayer || !BRIDGE_PUBLIC_URL) {
|
||||
const syncBaseUrl = WEB_INTERNAL_URL || BRIDGE_PUBLIC_URL;
|
||||
if (!isRemotePlayer || !syncBaseUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -144,16 +146,12 @@ async function start() {
|
||||
};
|
||||
|
||||
try {
|
||||
console.info('[player] Triggering startup media sync', {
|
||||
playerIdentifier: PLAYER_DEVICE_ID,
|
||||
bridgeBaseUrl: BRIDGE_PUBLIC_URL
|
||||
});
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'POST',
|
||||
pathname: '/api/internal/sync/player-media',
|
||||
body: requestBody
|
||||
});
|
||||
const response = await fetch(`${BRIDGE_PUBLIC_URL}/api/internal/sync/player-media`, {
|
||||
const response = await fetch(`${syncBaseUrl}/api/internal/sync/player-media`, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({
|
||||
Accept: 'application/json',
|
||||
@@ -162,11 +160,6 @@ async function start() {
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
console.info('[player] Startup media sync response', {
|
||||
ok: Boolean(response && response.ok),
|
||||
status: response && response.status ? response.status : null
|
||||
});
|
||||
|
||||
return Boolean(response && response.ok);
|
||||
} catch (_error) {
|
||||
console.warn('[player] Startup media sync failed');
|
||||
@@ -175,7 +168,8 @@ async function start() {
|
||||
}
|
||||
|
||||
async function triggerWebFontSync() {
|
||||
if (!isRemotePlayer || !BRIDGE_PUBLIC_URL) {
|
||||
const syncBaseUrl = WEB_INTERNAL_URL || BRIDGE_PUBLIC_URL;
|
||||
if (!isRemotePlayer || !syncBaseUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -186,16 +180,12 @@ async function start() {
|
||||
};
|
||||
|
||||
try {
|
||||
console.info('[player] Triggering startup font sync', {
|
||||
playerIdentifier: PLAYER_DEVICE_ID,
|
||||
bridgeBaseUrl: BRIDGE_PUBLIC_URL
|
||||
});
|
||||
const authHeaders = createRequestAuthHeaders({
|
||||
method: 'POST',
|
||||
pathname: '/api/internal/sync/player-font',
|
||||
body: requestBody
|
||||
});
|
||||
const response = await fetch(`${BRIDGE_PUBLIC_URL}/api/internal/sync/player-font`, {
|
||||
const response = await fetch(`${syncBaseUrl}/api/internal/sync/player-font`, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({
|
||||
Accept: 'application/json',
|
||||
@@ -204,11 +194,6 @@ async function start() {
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
console.info('[player] Startup font sync response', {
|
||||
ok: Boolean(response && response.ok),
|
||||
status: response && response.status ? response.status : null
|
||||
});
|
||||
|
||||
return Boolean(response && response.ok);
|
||||
} catch (_error) {
|
||||
console.warn('[player] Startup font sync failed');
|
||||
@@ -257,18 +242,9 @@ async function start() {
|
||||
response.error = 'Invalid media payload.';
|
||||
} else {
|
||||
const bodyBuffer = Buffer.from(bodyBase64, 'base64');
|
||||
console.info('[player] Writing media file', {
|
||||
relativePath: relativePath,
|
||||
filePath: filePath,
|
||||
bytes: bodyBuffer.length
|
||||
});
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.promises.writeFile(filePath, bodyBuffer);
|
||||
response.ok = true;
|
||||
console.info('[player] Media file written', {
|
||||
relativePath: relativePath,
|
||||
filePath: filePath
|
||||
});
|
||||
}
|
||||
} else if (command === 'media-delete') {
|
||||
const relativePath = String(payload.relativePath || payload.filename || '').trim();
|
||||
|
||||
@@ -453,6 +453,23 @@ function createUploadSyncService(options) {
|
||||
return Boolean(localUploadDir);
|
||||
}
|
||||
|
||||
async function fetchLivePlayerRegistrations() {
|
||||
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 isPlayerUnavailableError(error) {
|
||||
const code = String(error && error.cause && error.cause.code || error && error.code || '').trim().toUpperCase();
|
||||
return code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'EAI_AGAIN' || code === 'ETIMEDOUT';
|
||||
@@ -462,16 +479,33 @@ function createUploadSyncService(options) {
|
||||
return Boolean(response) && Number(response.status) === 503;
|
||||
}
|
||||
|
||||
function buildPendingPlayerUploadSyncKey(operation) {
|
||||
const uploadPath = normalizeUploadReference(operation && operation.uploadPath);
|
||||
const metadata = operation && operation.metadata && typeof operation.metadata === 'object'
|
||||
? operation.metadata
|
||||
: null;
|
||||
const playerIdentifier = String((operation && operation.playerIdentifier) || (metadata && metadata.playerIdentifier) || '').trim();
|
||||
const playerInternalBaseUrl = normalizeBaseUrl((operation && operation.playerInternalBaseUrl) || (metadata && metadata.playerInternalBaseUrl) || '');
|
||||
|
||||
return [uploadPath, playerIdentifier, playerInternalBaseUrl].filter(Boolean).join('|');
|
||||
}
|
||||
|
||||
function queuePlayerUploadSync(operation) {
|
||||
if (!operation || !operation.uploadPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingPlayerUploadSyncs.set(normalizeUploadReference(operation.uploadPath), {
|
||||
const metadata = operation.metadata && typeof operation.metadata === 'object' ? operation.metadata : null;
|
||||
const playerIdentifier = String((operation && operation.playerIdentifier) || (metadata && metadata.playerIdentifier) || '').trim();
|
||||
const playerInternalBaseUrl = normalizeBaseUrl((operation && operation.playerInternalBaseUrl) || (metadata && metadata.playerInternalBaseUrl) || '');
|
||||
|
||||
pendingPlayerUploadSyncs.set(buildPendingPlayerUploadSyncKey(operation), {
|
||||
type: operation.type === 'delete' ? 'delete' : 'put',
|
||||
uploadPath: normalizeUploadReference(operation.uploadPath),
|
||||
uploadDir: operation.uploadDir || null,
|
||||
metadata: operation.metadata || null
|
||||
metadata: metadata,
|
||||
playerIdentifier: playerIdentifier || null,
|
||||
playerInternalBaseUrl: playerInternalBaseUrl || null
|
||||
});
|
||||
|
||||
schedulePendingPlayerUploadSyncFlush();
|
||||
@@ -592,20 +626,26 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function syncUploadRefsToPlayer(uploadRefs, localUploadDir) {
|
||||
async function syncUploadRefsToPlayer(uploadRefs, localUploadDir, preferredPlayerIdentifier, preferredPlayerInternalBaseUrl) {
|
||||
if (!shouldMirrorUploads(localUploadDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl(preferredPlayerIdentifier, preferredPlayerInternalBaseUrl);
|
||||
const uniqueRefs = Array.from(new Set((uploadRefs || []).map(normalizeUploadReference).filter(Boolean)));
|
||||
for (let i = 0; i < uniqueRefs.length; i += 1) {
|
||||
const success = await pushUploadFileToPlayer(uniqueRefs[i], localUploadDir, resolvedPlayerInternalBaseUrl);
|
||||
const success = await pushUploadFileToPlayer(uniqueRefs[i], localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier);
|
||||
if (!success) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uniqueRefs[i],
|
||||
uploadDir: localUploadDir
|
||||
uploadDir: localUploadDir,
|
||||
playerIdentifier: preferredPlayerIdentifier,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl,
|
||||
metadata: preferredPlayerIdentifier || preferredPlayerInternalBaseUrl ? {
|
||||
playerIdentifier: preferredPlayerIdentifier || null,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl || null
|
||||
} : null
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -723,10 +763,27 @@ function createUploadSyncService(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
return queueMediaSyncTask('media-sync:' + operation.key, 'Media sync', {
|
||||
mode: 'playlist',
|
||||
operation: operation
|
||||
});
|
||||
const players = await fetchLivePlayerRegistrations();
|
||||
if (!players.length) {
|
||||
return queueMediaSyncTask('media-sync:' + operation.key, 'Media sync', {
|
||||
mode: 'playlist',
|
||||
operation: operation
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(players.map(function (player) {
|
||||
const playerIdentifier = String(player && player.identifier || '').trim();
|
||||
const playerInternalBaseUrl = String(player && player.internal_base_url || '').trim().replace(/\/$/, '');
|
||||
const playerPublicBaseUrl = String(player && player.public_base_url || '').trim().replace(/\/$/, '');
|
||||
|
||||
return queueMediaSyncTask('media-sync:' + operation.key + (playerIdentifier ? ':' + playerIdentifier : ''), 'Media sync', {
|
||||
mode: 'playlist',
|
||||
operation: operation,
|
||||
playerIdentifier: playerIdentifier || null,
|
||||
playerInternalBaseUrl: playerInternalBaseUrl || null,
|
||||
playerPublicBaseUrl: playerPublicBaseUrl || null
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async function flushPendingPlaylistUploadSyncs() {
|
||||
@@ -787,22 +844,27 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
|
||||
pendingPlayerUploadSyncFlushInFlight = (async function () {
|
||||
const pendingEntries = Array.from(pendingPlayerUploadSyncs.values());
|
||||
const playerMetadata = pendingEntries.length && pendingEntries[0] && pendingEntries[0].metadata
|
||||
? pendingEntries[0].metadata
|
||||
: await getPlayerTaskMetadata();
|
||||
if (playerMetadata && playerMetadata.playerActive === false) {
|
||||
pendingPlayerUploadSyncs.clear();
|
||||
pendingPlayerUploadSyncRetryLogAt = 0;
|
||||
return;
|
||||
}
|
||||
const resolvedPlayerInternalBaseUrl = playerMetadata && playerMetadata.playerInternalBaseUrl
|
||||
? playerMetadata.playerInternalBaseUrl
|
||||
: await getPlayerInternalBaseUrl(playerMetadata && playerMetadata.playerIdentifier, playerMetadata && playerMetadata.playerInternalBaseUrl);
|
||||
const pendingEntries = Array.from(pendingPlayerUploadSyncs.entries());
|
||||
const firstOperation = pendingEntries.length && pendingEntries[0] ? pendingEntries[0][1] : null;
|
||||
const summaryPlayerMetadata = firstOperation && firstOperation.metadata
|
||||
? firstOperation.metadata
|
||||
: await getPlayerTaskMetadata(firstOperation && firstOperation.playerIdentifier, firstOperation && firstOperation.playerInternalBaseUrl);
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
for (let i = 0; i < pendingEntries.length; i += 1) {
|
||||
const operation = pendingEntries[i];
|
||||
const entry = pendingEntries[i];
|
||||
const pendingKey = entry[0];
|
||||
const operation = entry[1];
|
||||
const playerMetadata = operation && operation.metadata
|
||||
? operation.metadata
|
||||
: await getPlayerTaskMetadata(operation && operation.playerIdentifier, operation && operation.playerInternalBaseUrl);
|
||||
if (playerMetadata && playerMetadata.playerActive === false) {
|
||||
pendingPlayerUploadSyncs.delete(pendingKey);
|
||||
continue;
|
||||
}
|
||||
const resolvedPlayerInternalBaseUrl = playerMetadata && playerMetadata.playerInternalBaseUrl
|
||||
? playerMetadata.playerInternalBaseUrl
|
||||
: await getPlayerInternalBaseUrl(playerMetadata && playerMetadata.playerIdentifier, playerMetadata && playerMetadata.playerInternalBaseUrl);
|
||||
let success = false;
|
||||
if (operation.type === 'delete') {
|
||||
success = await removeUploadFileFromPlayer(operation.uploadPath, operation.uploadDir, resolvedPlayerInternalBaseUrl, playerMetadata && playerMetadata.playerIdentifier);
|
||||
@@ -811,20 +873,20 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
if (success) {
|
||||
successCount += 1;
|
||||
pendingPlayerUploadSyncs.delete(operation.uploadPath);
|
||||
pendingPlayerUploadSyncs.delete(pendingKey);
|
||||
} else {
|
||||
failureCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount) {
|
||||
logMediaSyncSummary('info', `Media sync completed ${successCount} upload${successCount === 1 ? '' : 's'}`, playerMetadata);
|
||||
logMediaSyncSummary('info', `Media sync completed ${successCount} upload${successCount === 1 ? '' : 's'}`, summaryPlayerMetadata);
|
||||
}
|
||||
if (failureCount) {
|
||||
const now = Date.now();
|
||||
if (!pendingPlayerUploadSyncRetryLogAt || now - pendingPlayerUploadSyncRetryLogAt >= PLAYER_UPLOAD_SYNC_RETRY_LOG_INTERVAL_MS) {
|
||||
pendingPlayerUploadSyncRetryLogAt = now;
|
||||
logMediaSyncSummary('warn', `Player unavailable, retry queued for ${failureCount} upload${failureCount === 1 ? '' : 's'}`, playerMetadata);
|
||||
logMediaSyncSummary('warn', `Player unavailable, retry queued for ${failureCount} upload${failureCount === 1 ? '' : 's'}`, summaryPlayerMetadata);
|
||||
}
|
||||
} else if (!pendingPlayerUploadSyncs.size) {
|
||||
pendingPlayerUploadSyncRetryLogAt = 0;
|
||||
@@ -892,7 +954,7 @@ function createUploadSyncService(options) {
|
||||
const operation = normalizePlaylistUploadSyncOperation(taskPayload.operation || taskPayload);
|
||||
|
||||
if (operation.nextUploadRefs.length) {
|
||||
await syncUploadRefsToPlayer(operation.nextUploadRefs, operation.localUploadDir);
|
||||
await syncUploadRefsToPlayer(operation.nextUploadRefs, operation.localUploadDir, taskPayload.playerIdentifier, taskPayload.playerInternalBaseUrl);
|
||||
}
|
||||
|
||||
if (operation.previousUploadRefs.length) {
|
||||
|
||||
@@ -274,4 +274,93 @@ test('stale player registrations stop media sync retries and warnings', async ()
|
||||
console.warn = originalWarn;
|
||||
fs.rmSync(uploadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('slide update sync queues one media task per live player and targets each player base url', async () => {
|
||||
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-multi-'));
|
||||
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
||||
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
||||
const liveLastSeenAt = new Date(Date.now() - 10_000).toISOString();
|
||||
const staleLastSeenAt = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
|
||||
const fetchCalls = [];
|
||||
const queuedTasks = [];
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = async function (url, init) {
|
||||
fetchCalls.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {
|
||||
get() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async text() {
|
||||
return JSON.stringify({ ok: true });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let uploadSyncService;
|
||||
uploadSyncService = createUploadSyncService({
|
||||
common: {},
|
||||
pool: {
|
||||
async query() {
|
||||
return [[
|
||||
{
|
||||
identifier: 'player-one',
|
||||
internal_base_url: 'http://player-one:8081',
|
||||
last_seen_at: liveLastSeenAt
|
||||
},
|
||||
{
|
||||
identifier: 'player-two',
|
||||
internal_base_url: 'http://player-two:8081',
|
||||
last_seen_at: liveLastSeenAt
|
||||
},
|
||||
{
|
||||
identifier: 'player-stale',
|
||||
internal_base_url: 'http://player-stale:8081',
|
||||
last_seen_at: staleLastSeenAt
|
||||
}
|
||||
]];
|
||||
}
|
||||
},
|
||||
playerSnapshotCache: new Map(),
|
||||
notifyPlayerScreens: async () => {},
|
||||
backgroundTaskQueue: {
|
||||
async enqueueTaskAndWait(definition) {
|
||||
queuedTasks.push(definition);
|
||||
await uploadSyncService.runMediaSyncTask(definition.payload);
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadSyncService.syncPlaylistUploadsOnChange({
|
||||
key: 'slide:update:123',
|
||||
pool: {},
|
||||
localUploadDir: uploadDir,
|
||||
nextUploadRefs: ['/media/uploads/sample.bin']
|
||||
});
|
||||
|
||||
assert.equal(queuedTasks.length, 2);
|
||||
assert.deepEqual(queuedTasks.map(function (task) {
|
||||
return task.metadata.playerIdentifier;
|
||||
}).sort(), ['player-one', 'player-two']);
|
||||
assert.ok(queuedTasks.every(function (task) {
|
||||
return task.key.startsWith('media-sync:slide:update:123:');
|
||||
}));
|
||||
assert.deepEqual(fetchCalls.map(function (call) {
|
||||
return call.url;
|
||||
}).sort(), [
|
||||
'http://player-one:8081/api/media/uploads%2Fsample.bin?deviceId=player-one',
|
||||
'http://player-two:8081/api/media/uploads%2Fsample.bin?deviceId=player-two'
|
||||
].sort());
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
fs.rmSync(uploadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user