@epic-web/workshop-utils 6.56.0 → 6.57.0
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
1
2
|
import { Readable } from 'node:stream';
|
|
2
3
|
type OfflineVideoEntryStatus = 'ready' | 'downloading' | 'error';
|
|
3
4
|
export type OfflineVideoDownloadState = {
|
|
@@ -58,6 +59,18 @@ export type OfflineVideoAsset = {
|
|
|
58
59
|
end: number;
|
|
59
60
|
}) => Readable;
|
|
60
61
|
};
|
|
62
|
+
export type VideoDownloadProgress = {
|
|
63
|
+
playbackId: string;
|
|
64
|
+
bytesDownloaded: number;
|
|
65
|
+
totalBytes: number | null;
|
|
66
|
+
status: 'downloading' | 'complete' | 'error';
|
|
67
|
+
};
|
|
68
|
+
export declare const DOWNLOAD_PROGRESS_EVENTS: {
|
|
69
|
+
readonly PROGRESS: "progress";
|
|
70
|
+
};
|
|
71
|
+
declare class DownloadProgressEmitter extends EventEmitter {
|
|
72
|
+
}
|
|
73
|
+
export declare const downloadProgressEmitter: DownloadProgressEmitter;
|
|
61
74
|
export declare function getOfflineVideoDownloadState(): OfflineVideoDownloadState;
|
|
62
75
|
export declare function getOfflineVideoSummary({ request, }?: {
|
|
63
76
|
request?: Request;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
2
3
|
import { createReadStream, createWriteStream, promises as fs } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { Readable, Transform } from 'node:stream';
|
|
@@ -25,6 +26,15 @@ const log = logger('epic:offline-videos');
|
|
|
25
26
|
const offlineVideoDirectoryName = 'offline-videos';
|
|
26
27
|
const offlineVideoIndexFileName = 'index.json';
|
|
27
28
|
const offlineVideoConfigFileName = 'offline-video-config.json';
|
|
29
|
+
export const DOWNLOAD_PROGRESS_EVENTS = {
|
|
30
|
+
PROGRESS: 'progress',
|
|
31
|
+
};
|
|
32
|
+
class DownloadProgressEmitter extends EventEmitter {
|
|
33
|
+
}
|
|
34
|
+
export const downloadProgressEmitter = new DownloadProgressEmitter();
|
|
35
|
+
function emitDownloadProgress(progress) {
|
|
36
|
+
downloadProgressEmitter.emit(DOWNLOAD_PROGRESS_EVENTS.PROGRESS, progress);
|
|
37
|
+
}
|
|
28
38
|
let downloadState = {
|
|
29
39
|
status: 'idle',
|
|
30
40
|
startedAt: null,
|
|
@@ -213,6 +223,16 @@ function createSliceTransform({ skipBytes, takeBytes, }) {
|
|
|
213
223
|
},
|
|
214
224
|
});
|
|
215
225
|
}
|
|
226
|
+
function createProgressTrackingTransform({ onProgress, }) {
|
|
227
|
+
let totalBytes = 0;
|
|
228
|
+
return new Transform({
|
|
229
|
+
transform(chunk, _encoding, callback) {
|
|
230
|
+
totalBytes += chunk.length;
|
|
231
|
+
onProgress(totalBytes);
|
|
232
|
+
callback(null, chunk);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
216
236
|
function createOfflineVideoReadStream({ filePath, size, key, iv, range, }) {
|
|
217
237
|
if (!range) {
|
|
218
238
|
const decipher = createOfflineVideoDecipher({ key, iv });
|
|
@@ -331,6 +351,12 @@ async function isOfflineVideoReady(index, playbackId, keyId, cryptoVersion, work
|
|
|
331
351
|
async function downloadMuxVideo({ playbackId, filePath, key, iv, resolution, }) {
|
|
332
352
|
const urls = getMuxMp4Urls(playbackId, resolution);
|
|
333
353
|
let lastError = null;
|
|
354
|
+
emitDownloadProgress({
|
|
355
|
+
playbackId,
|
|
356
|
+
bytesDownloaded: 0,
|
|
357
|
+
totalBytes: null,
|
|
358
|
+
status: 'downloading',
|
|
359
|
+
});
|
|
334
360
|
for (const url of urls) {
|
|
335
361
|
const response = await fetch(url).catch((error) => {
|
|
336
362
|
lastError = error;
|
|
@@ -342,16 +368,49 @@ async function downloadMuxVideo({ playbackId, filePath, key, iv, resolution, })
|
|
|
342
368
|
lastError = new Error(`Failed to download ${playbackId} from ${url} (${response.status})`);
|
|
343
369
|
continue;
|
|
344
370
|
}
|
|
371
|
+
// Get content-length if available for progress tracking
|
|
372
|
+
const contentLengthHeader = response.headers.get('content-length');
|
|
373
|
+
const totalBytes = contentLengthHeader
|
|
374
|
+
? parseInt(contentLengthHeader, 10)
|
|
375
|
+
: null;
|
|
376
|
+
emitDownloadProgress({
|
|
377
|
+
playbackId,
|
|
378
|
+
bytesDownloaded: 0,
|
|
379
|
+
totalBytes,
|
|
380
|
+
status: 'downloading',
|
|
381
|
+
});
|
|
345
382
|
await ensureOfflineVideoDir();
|
|
346
383
|
const tmpPath = `${filePath}.tmp-${randomUUID()}`;
|
|
347
384
|
const stream = createWriteStream(tmpPath, { mode: 0o600 });
|
|
348
385
|
const cipher = createOfflineVideoCipher({ key, iv });
|
|
386
|
+
const progressTracker = createProgressTrackingTransform({
|
|
387
|
+
onProgress: (bytesDownloaded) => {
|
|
388
|
+
emitDownloadProgress({
|
|
389
|
+
playbackId,
|
|
390
|
+
bytesDownloaded,
|
|
391
|
+
totalBytes,
|
|
392
|
+
status: 'downloading',
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
});
|
|
349
396
|
const webStream = response.body;
|
|
350
|
-
await pipeline(Readable.from(webStream), cipher, stream);
|
|
397
|
+
await pipeline(Readable.from(webStream), progressTracker, cipher, stream);
|
|
351
398
|
await fs.rename(tmpPath, filePath);
|
|
352
399
|
const stat = await fs.stat(filePath);
|
|
400
|
+
emitDownloadProgress({
|
|
401
|
+
playbackId,
|
|
402
|
+
bytesDownloaded: stat.size,
|
|
403
|
+
totalBytes: stat.size,
|
|
404
|
+
status: 'complete',
|
|
405
|
+
});
|
|
353
406
|
return { size: stat.size };
|
|
354
407
|
}
|
|
408
|
+
emitDownloadProgress({
|
|
409
|
+
playbackId,
|
|
410
|
+
bytesDownloaded: 0,
|
|
411
|
+
totalBytes: null,
|
|
412
|
+
status: 'error',
|
|
413
|
+
});
|
|
355
414
|
throw lastError ?? new Error(`Unable to download video ${playbackId}`);
|
|
356
415
|
}
|
|
357
416
|
async function runOfflineVideoDownloads({ videos, index, keyInfo, workshop, resolution, }) {
|