@xiboplayer/cache 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/cache-proxy.js +18 -0
- package/src/download-manager.js +88 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Offline caching and download management with parallel chunk downloads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
"./cache-proxy": "./src/cache-proxy.js",
|
|
11
11
|
"./download-manager": "./src/download-manager.js"
|
|
12
12
|
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:coverage": "vitest run --coverage"
|
|
17
|
+
},
|
|
13
18
|
"dependencies": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
19
|
+
"@xiboplayer/utils": "workspace:*",
|
|
20
|
+
"spark-md5": "^3.0.2"
|
|
16
21
|
},
|
|
17
22
|
"devDependencies": {
|
|
18
23
|
"vitest": "^2.0.0",
|
|
@@ -32,10 +37,5 @@
|
|
|
32
37
|
"type": "git",
|
|
33
38
|
"url": "git+https://github.com/xibo-players/xiboplayer.git",
|
|
34
39
|
"directory": "packages/cache"
|
|
35
|
-
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"test": "vitest run",
|
|
38
|
-
"test:watch": "vitest",
|
|
39
|
-
"test:coverage": "vitest run --coverage"
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
package/src/cache-proxy.js
CHANGED
|
@@ -237,6 +237,15 @@ class ServiceWorkerBackend extends EventEmitter {
|
|
|
237
237
|
return false;
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Prioritize layout files — reorder queue and hold other downloads until done.
|
|
243
|
+
* @param {string[]} mediaIds - Media IDs needed by the current layout
|
|
244
|
+
*/
|
|
245
|
+
async prioritizeLayoutFiles(mediaIds) {
|
|
246
|
+
if (!this.controller) return;
|
|
247
|
+
this.controller.postMessage({ type: 'PRIORITIZE_LAYOUT_FILES', data: { mediaIds } });
|
|
248
|
+
}
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
// DirectCacheBackend removed - Service Worker only architecture
|
|
@@ -387,6 +396,15 @@ export class CacheProxy extends EventEmitter {
|
|
|
387
396
|
return await this.backend.isCached(type, id);
|
|
388
397
|
}
|
|
389
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Prioritize layout files — reorder queue and hold other downloads until done.
|
|
401
|
+
* @param {string[]} mediaIds - Media IDs needed by the current layout
|
|
402
|
+
*/
|
|
403
|
+
async prioritizeLayoutFiles(mediaIds) {
|
|
404
|
+
if (!this.backend?.prioritizeLayoutFiles) return;
|
|
405
|
+
return await this.backend.prioritizeLayoutFiles(mediaIds);
|
|
406
|
+
}
|
|
407
|
+
|
|
390
408
|
/**
|
|
391
409
|
* Get backend type for debugging
|
|
392
410
|
* @returns {string} 'service-worker' or 'direct'
|
package/src/download-manager.js
CHANGED
|
@@ -159,18 +159,19 @@ export class DownloadTask {
|
|
|
159
159
|
chunkRanges.push({ start, end, index: chunkRanges.length });
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
if (chunkRanges.length >
|
|
166
|
-
|
|
167
|
-
chunkRanges.splice(1, 0, lastChunk); // insert after chunk 0
|
|
162
|
+
// Phase 1: Download chunk 0 (ftyp header) and last chunk (moov atom) in parallel.
|
|
163
|
+
// Browsers need both extremes to start MP4 playback.
|
|
164
|
+
const priorityChunks = [chunkRanges[0]];
|
|
165
|
+
if (chunkRanges.length > 1) {
|
|
166
|
+
priorityChunks.push(chunkRanges[chunkRanges.length - 1]);
|
|
168
167
|
}
|
|
169
|
-
console.log('[DownloadTask] Downloading', chunkRanges.length, 'chunks (chunk 0 + last prioritized)');
|
|
170
168
|
|
|
171
|
-
//
|
|
169
|
+
// Remaining chunks in sequential order (1, 2, 3, ..., N-2) for gap-free playback
|
|
170
|
+
const remainingChunks = chunkRanges.slice(1, chunkRanges.length > 1 ? -1 : 1);
|
|
171
|
+
|
|
172
|
+
console.log('[DownloadTask] Downloading', chunkRanges.length, 'chunks: phase 1 (chunk 0 + last parallel), phase 2 (sequential)');
|
|
173
|
+
|
|
172
174
|
const chunkMap = new Map();
|
|
173
|
-
let nextChunkIndex = 0;
|
|
174
175
|
|
|
175
176
|
const downloadChunk = async (range) => {
|
|
176
177
|
const rangeHeader = `bytes=${range.start}-${range.end}`;
|
|
@@ -213,20 +214,13 @@ export class DownloadTask {
|
|
|
213
214
|
}
|
|
214
215
|
};
|
|
215
216
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
while (nextChunkIndex < chunkRanges.length) {
|
|
219
|
-
const range = chunkRanges[nextChunkIndex++];
|
|
220
|
-
await downloadChunk(range);
|
|
221
|
-
}
|
|
222
|
-
};
|
|
217
|
+
// Phase 1: chunk 0 + last chunk in parallel (both needed for playback start)
|
|
218
|
+
await Promise.all(priorityChunks.map(range => downloadChunk(range)));
|
|
223
219
|
|
|
224
|
-
//
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
downloaders.push(downloadNext());
|
|
220
|
+
// Phase 2: remaining chunks strictly sequential (guarantees gap-free playback)
|
|
221
|
+
for (const range of remainingChunks) {
|
|
222
|
+
await downloadChunk(range);
|
|
228
223
|
}
|
|
229
|
-
await Promise.all(downloaders);
|
|
230
224
|
|
|
231
225
|
// If progressive caching was used, skip reassembly (chunks are already cached)
|
|
232
226
|
if (this.onChunkDownloaded) {
|
|
@@ -293,39 +287,83 @@ export class DownloadQueue {
|
|
|
293
287
|
}
|
|
294
288
|
|
|
295
289
|
/**
|
|
296
|
-
*
|
|
290
|
+
* Prioritize files for the current layout — reorder queue and hold other downloads.
|
|
291
|
+
* All prioritized files must complete (including all chunks) before others start.
|
|
292
|
+
* @param {Array<string|number>} fileIds - Media IDs to prioritize
|
|
293
|
+
*/
|
|
294
|
+
prioritizeLayoutFiles(fileIds) {
|
|
295
|
+
const idSet = new Set(fileIds.map(String));
|
|
296
|
+
this._layoutHoldIds = idSet;
|
|
297
|
+
|
|
298
|
+
// Reorder queue: matching IDs first, rest after
|
|
299
|
+
const prioritized = [];
|
|
300
|
+
const rest = [];
|
|
301
|
+
for (const task of this.queue) {
|
|
302
|
+
if (idSet.has(String(task.fileInfo.id))) {
|
|
303
|
+
prioritized.push(task);
|
|
304
|
+
} else {
|
|
305
|
+
rest.push(task);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
this.queue = [...prioritized, ...rest];
|
|
309
|
+
console.log('[DownloadQueue] Layout hold:', idSet.size, 'files prioritized,', rest.length, 'held back');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Process queue - start downloads up to concurrency limit.
|
|
314
|
+
* When layout hold is active, only starts held files until all complete.
|
|
297
315
|
*/
|
|
298
316
|
async processQueue() {
|
|
299
|
-
|
|
317
|
+
// Layout hold: only start held files while any are still pending/active
|
|
318
|
+
if (this._layoutHoldIds?.size > 0) {
|
|
319
|
+
const hasHeldInQueue = this.queue.some(t => this._layoutHoldIds.has(String(t.fileInfo.id)));
|
|
320
|
+
const hasHeldActive = [...this.active.values()].some(t =>
|
|
321
|
+
this._layoutHoldIds.has(String(t.fileInfo.id))
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (hasHeldInQueue || hasHeldActive) {
|
|
325
|
+
// Only start held files
|
|
326
|
+
while (this.running < this.concurrency && this.queue.length > 0) {
|
|
327
|
+
const idx = this.queue.findIndex(t => this._layoutHoldIds.has(String(t.fileInfo.id)));
|
|
328
|
+
if (idx === -1) break;
|
|
329
|
+
const task = this.queue.splice(idx, 1)[0];
|
|
330
|
+
this._startTask(task);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// All held files done — clear hold
|
|
335
|
+
console.log('[DownloadQueue] Layout hold cleared, resuming normal downloads');
|
|
336
|
+
this._layoutHoldIds = null;
|
|
337
|
+
}
|
|
300
338
|
|
|
339
|
+
// Normal FIFO processing
|
|
301
340
|
while (this.running < this.concurrency && this.queue.length > 0) {
|
|
302
341
|
const task = this.queue.shift();
|
|
303
|
-
this.
|
|
304
|
-
|
|
305
|
-
console.log('[DownloadQueue] Starting:', task.fileInfo.path, `(${this.running}/${this.concurrency} active)`);
|
|
306
|
-
|
|
307
|
-
// Start download (don't await - let it run in background)
|
|
308
|
-
// .catch is safe here: errors are already propagated to waiters inside start()
|
|
309
|
-
task.start()
|
|
310
|
-
.catch(() => {}) // Suppress — error handled internally via waiters
|
|
311
|
-
.finally(() => {
|
|
312
|
-
this.running--;
|
|
313
|
-
this.active.delete(task.fileInfo.path);
|
|
314
|
-
console.log('[DownloadQueue] Complete:', task.fileInfo.path, `(${this.running} active, ${this.queue.length} pending)`);
|
|
315
|
-
|
|
316
|
-
// Process next in queue
|
|
317
|
-
this.processQueue();
|
|
318
|
-
});
|
|
342
|
+
this._startTask(task);
|
|
319
343
|
}
|
|
320
344
|
|
|
321
|
-
if (this.running >= this.concurrency) {
|
|
322
|
-
console.log('[DownloadQueue] Concurrency limit reached:', this.running, '/', this.concurrency);
|
|
323
|
-
}
|
|
324
345
|
if (this.queue.length === 0 && this.running === 0) {
|
|
325
346
|
console.log('[DownloadQueue] All downloads complete');
|
|
326
347
|
}
|
|
327
348
|
}
|
|
328
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Start a download task (extracted to avoid duplication between hold and normal paths)
|
|
352
|
+
*/
|
|
353
|
+
_startTask(task) {
|
|
354
|
+
this.running++;
|
|
355
|
+
console.log('[DownloadQueue] Starting:', task.fileInfo.path, `(${this.running}/${this.concurrency} active)`);
|
|
356
|
+
|
|
357
|
+
task.start()
|
|
358
|
+
.catch(() => {}) // Suppress — error handled internally via waiters
|
|
359
|
+
.finally(() => {
|
|
360
|
+
this.running--;
|
|
361
|
+
this.active.delete(task.fileInfo.path);
|
|
362
|
+
console.log('[DownloadQueue] Complete:', task.fileInfo.path, `(${this.running} active, ${this.queue.length} pending)`);
|
|
363
|
+
this.processQueue();
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
329
367
|
/**
|
|
330
368
|
* Move a file to the front of the queue (if still queued, not yet started)
|
|
331
369
|
* @param {string} fileType - 'media' or 'layout'
|
|
@@ -425,6 +463,14 @@ export class DownloadManager {
|
|
|
425
463
|
return this.queue.getProgress();
|
|
426
464
|
}
|
|
427
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Prioritize layout files — reorder queue and hold other downloads
|
|
468
|
+
*/
|
|
469
|
+
prioritizeLayoutFiles(fileIds) {
|
|
470
|
+
this.queue.prioritizeLayoutFiles(fileIds);
|
|
471
|
+
this.queue.processQueue();
|
|
472
|
+
}
|
|
473
|
+
|
|
428
474
|
/**
|
|
429
475
|
* Clear all downloads
|
|
430
476
|
*/
|