@torrent-tv/proxy 2.89.6 → 2.89.8
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/AGENTS.md +32 -12
- package/CHANGELOG.md +2340 -2330
- package/assets/calibration/audio/NOTICE.md +19 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/biome.json +412 -412
- package/docs/browser-proxy-contract.md +155 -155
- package/docs/container-architecture.md +273 -273
- package/docs/encode-run-state.md +1 -1
- package/docs/playback-start-forecast.md +113 -98
- package/package.json +51 -50
- package/routes/api/sources/files/get.js +106 -106
- package/scripts/build-audio-samples.mjs +31 -31
- package/services/encode/EncodeRun.js +67 -15
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/OutputOpening.js +784 -784
- package/services/encode/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- package/services/encode/encode-exit.js +2 -0
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/output/index.js +24 -24
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/run-command.js +757 -755
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/media/ContainerOrchestrator.js +202 -202
- package/services/media/KeyframeTables.js +356 -356
- package/services/media/SubtitleCues.js +592 -592
- package/services/media/audio-inventory.js +299 -299
- package/services/media/container/AviContainer.js +268 -268
- package/services/media/container/Container.js +632 -632
- package/services/media/container/ContainerFactory.js +38 -38
- package/services/media/container/MatroskaContainer.js +1031 -1031
- package/services/media/container/Mp4Container.js +1354 -1354
- package/services/media/container/SubtitleFileContainer.js +339 -339
- package/services/media/container/ebml-stream.js +174 -174
- package/services/media/container/matroska-clusters.js +1165 -1165
- package/services/media/container/unavailable.js +77 -77
- package/services/media/tracks/ContainerTrack.js +84 -84
- package/services/media/tracks/TextSubtitleTrack.js +295 -295
- package/services/storage/piece-store/shared-piece-store.js +1937 -1937
- package/services/storage/segment-store/SegmentStore.js +1294 -1294
- package/services/torrent/Contents.js +382 -382
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/torrent-pool.js +2688 -2688
- package/services/torrent/worker/client.js +836 -836
- package/services/torrent/worker/held-bytes.js +231 -231
- package/services/torrent/worker/worker.js +1137 -1137
- package/services/transport/data-channel-handler.js +1373 -1373
- package/services/viewer/PriorityMap.js +421 -421
- package/test/audio-calibration.test.js +23 -23
- package/test/auto-quality-step.test.js +762 -762
- package/test/coalesce.test.js +39 -39
- package/test/coverage-follows-the-disk.test.js +5 -0
- package/test/decode-cost.test.js +475 -475
- package/test/encode-run.test.js +58 -0
- package/test/episode-naming.test.js +131 -131
- package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
- package/test/fixtures/audio-frame-boundary.json +1 -1
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +11 -13
- package/test/held-read-ownership.test.js +77 -77
- package/test/helpers/encode-run.js +14 -6
- package/test/helpers/held-file.js +77 -77
- package/test/helpers/manager.js +62 -62
- package/test/helpers/matroska-file.js +296 -296
- package/test/keyframe-table-budget.test.js +123 -123
- package/test/matroska-cluster-discovery.test.js +323 -323
- package/test/one-output-one-session.test.js +273 -273
- package/test/output-speed.test.js +6 -1
- package/test/piece-completeness.test.js +37 -1
- package/test/quality-variants.test.js +1274 -1270
- package/test/rate-control-in-the-output.test.js +4 -3
- package/test/reads-not-yet-arrived.test.js +218 -218
- package/test/seek-frees-the-viewers-requests.test.js +1 -1
- package/test/segment-closed-on-both-branches.test.js +114 -114
- package/test/segment-judged-by-its-own-header.test.js +222 -222
- package/test/segment-serve-wiring.test.js +481 -481
- package/test/serving-existing-output.test.js +111 -111
- package/test/source-files-route.test.js +113 -113
- package/test/subtitle-cue-walk.test.js +385 -385
- package/test/subtitle-metadata.test.js +15 -15
- package/test/subtitle-plan-waits.test.js +125 -125
- package/test/subtitle-track-numbering.test.js +391 -391
- package/test/two-viewers-one-picture.test.js +508 -508
- package/test/viewer-requests-admission.test.js +92 -92
- package/test/viewer-starts-on-map.test.js +27 -27
- package/test/viewer-states.test.js +5 -2
- package/test/viewing-generations.test.js +390 -390
- package/test/whole-file-held-read.test.js +30 -30
- package/utils/coalesce.js +41 -41
|
@@ -68,6 +68,7 @@ import { mpegtsFormat } from "./mpegts.js";
|
|
|
68
68
|
* data; whether a browser joins a gap is decided by the viewer component.
|
|
69
69
|
*
|
|
70
70
|
* @typedef {{ tracks: Array<{ id: number, kind?: string, timescale: bigint,
|
|
71
|
+
* firstSampleStart?: bigint, positionErrorTicks?: bigint,
|
|
71
72
|
* ranges: Array<{ start: bigint, end: bigint, frame: bigint }> }> }} MediaCoverage
|
|
72
73
|
*/
|
|
73
74
|
|
|
@@ -154,6 +154,11 @@ export function readTrackEdits(initSegment) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
edit.offset = roundedQuotient(emptyTicks * edit.timescale, movieTimescale) - mediaTime;
|
|
157
|
+
// movenc writes a positive empty edit with AV_ROUND_DOWN. Keep the
|
|
158
|
+
// uncertainty of that position separate from the served timestamps.
|
|
159
|
+
if (emptyTicks > 0n) {
|
|
160
|
+
edit.positionErrorTicks = (edit.timescale + movieTimescale - 1n) / movieTimescale;
|
|
161
|
+
}
|
|
157
162
|
}
|
|
158
163
|
});
|
|
159
164
|
return edits;
|
|
@@ -329,7 +334,9 @@ export function readPresentationRanges(raw) {
|
|
|
329
334
|
ranges.push({ start: sample.start, end, frame });
|
|
330
335
|
}
|
|
331
336
|
}
|
|
332
|
-
|
|
337
|
+
const edit = edits.get(id);
|
|
338
|
+
tracks.push({ id, kind: kinds.get(id), timescale: edit.timescale, ranges,
|
|
339
|
+
firstSampleStart: samples[0].start, positionErrorTicks: edit.positionErrorTicks ?? 0n });
|
|
333
340
|
}
|
|
334
341
|
return { tracks };
|
|
335
342
|
}
|
|
@@ -1,202 +1,202 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Container orchestrator — application layer over Container domain.
|
|
3
|
-
*
|
|
4
|
-
* Holds ONE Container per file (key sourceKey:fileIndex), and every reader of a
|
|
5
|
-
* file's statements — the track table, the media info, the keyframe table, the
|
|
6
|
-
* subtitle plan and the cue walk — goes through that one. The subtitle walk used
|
|
7
|
-
* to build a second container over the same file with readers of its own, which
|
|
8
|
-
* read the head and the Cues table a second time under a different rule for a
|
|
9
|
-
* read that had not arrived (`research/subtitles-never-appear-2026-10-01.md`).
|
|
10
|
-
*
|
|
11
|
-
* What is kept, and what is not: a container is kept once the bytes have said
|
|
12
|
-
* what format they are (including "none this proxy knows"); a head that has not
|
|
13
|
-
* arrived throws `BytesUnavailable` and leaves nothing behind, so the next ask
|
|
14
|
-
* reads again. Delegates format detection to ContainerFactory. Transport-
|
|
15
|
-
* agnostic — takes readRange, knows nothing about torrents or HTTP.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
19
|
-
import { isUnavailable } from "./container/unavailable.js";
|
|
20
|
-
import { logger } from "../../utils/logger.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* How much of a file's head its track table lives in.
|
|
24
|
-
*
|
|
25
|
-
* A fact of the FORMATS, not of the torrent: Matroska puts its Tracks element
|
|
26
|
-
* in the head and MP4's `moov` is there or pointed to from there. Whoever
|
|
27
|
-
* fetches bytes before asking is told this figure rather than choosing one.
|
|
28
|
-
*/
|
|
29
|
-
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
30
|
-
|
|
31
|
-
export class ContainerOrchestrator {
|
|
32
|
-
constructor() {
|
|
33
|
-
/** @type {Map<string, import("./container/Container.js").Container|null>} */
|
|
34
|
-
this.cache = new Map();
|
|
35
|
-
/** @type {Map<string, Promise<import("./container/Container.js").Container|null>>} */
|
|
36
|
-
this.pending = new Map();
|
|
37
|
-
/**
|
|
38
|
-
* What each file's container declares, once it has said it.
|
|
39
|
-
*
|
|
40
|
-
* The track table is asked for the audio menu, the subtitle defaults and
|
|
41
|
-
* the video facts of one file; the container keeps its own reading, and
|
|
42
|
-
* this keeps the answer so the question costs nothing the second time.
|
|
43
|
-
*
|
|
44
|
-
* @type {Map<string, import("./tracks/index.js").ContainerTrack[]>}
|
|
45
|
-
*/
|
|
46
|
-
this.tracks = new Map();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The container already built for a file, if any — without reading anything.
|
|
51
|
-
*
|
|
52
|
-
* `undefined` means none has been built yet; `null` means the bytes were read
|
|
53
|
-
* and are no format this proxy knows.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} sourceKey
|
|
56
|
-
* @param {number} fileIndex
|
|
57
|
-
* @returns {import("./container/Container.js").Container | null | undefined}
|
|
58
|
-
*/
|
|
59
|
-
known(sourceKey, fileIndex) {
|
|
60
|
-
return this.cache.get(`${sourceKey}:${fileIndex}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The file's one container, built on first ask.
|
|
65
|
-
*
|
|
66
|
-
* @param {object} params
|
|
67
|
-
* @param {string} params.sourceKey
|
|
68
|
-
* @param {number} params.fileIndex
|
|
69
|
-
* @param {(start:number,end:number)=>Promise<Buffer|null>} params.readRange
|
|
70
|
-
* @param {number} params.fileSize
|
|
71
|
-
* @param {string} [params.label]
|
|
72
|
-
* @param {number} [params.portionBytes]
|
|
73
|
-
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
74
|
-
* @throws {import("./container/unavailable.js").BytesUnavailable} While the
|
|
75
|
-
* head has not arrived. Nothing is kept for it.
|
|
76
|
-
*/
|
|
77
|
-
async containerFor({ sourceKey, fileIndex, readRange, fileSize, label = "", portionBytes }) {
|
|
78
|
-
const key = `${sourceKey}:${fileIndex}`;
|
|
79
|
-
if (this.cache.has(key)) return this.cache.get(key);
|
|
80
|
-
if (this.pending.has(key)) return this.pending.get(key);
|
|
81
|
-
const p = ContainerFactory.create({ readRange, fileSize, label, portionBytes })
|
|
82
|
-
.then((c) => {
|
|
83
|
-
this.cache.set(key, c);
|
|
84
|
-
if (c) logger.info(`container: ${c.formatName} for "${label}"`);
|
|
85
|
-
else logger.info(`container: unknown for "${label}"`);
|
|
86
|
-
return c;
|
|
87
|
-
})
|
|
88
|
-
.catch((e) => {
|
|
89
|
-
if (!isUnavailable(e)) {
|
|
90
|
-
logger.warn(`container: failed for "${label}": ${e?.message ?? e}`);
|
|
91
|
-
}
|
|
92
|
-
throw e;
|
|
93
|
-
})
|
|
94
|
-
.finally(() => {
|
|
95
|
-
this.pending.delete(key);
|
|
96
|
-
});
|
|
97
|
-
this.pending.set(key, p);
|
|
98
|
-
return p;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The same, answering null rather than throwing while the head is not here —
|
|
103
|
-
* for the callers whose own answer to "not yet" is "nothing yet".
|
|
104
|
-
*
|
|
105
|
-
* @param {object} params - same as containerFor
|
|
106
|
-
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
107
|
-
*/
|
|
108
|
-
async getContainer(params) {
|
|
109
|
-
try {
|
|
110
|
-
return await this.containerFor(params);
|
|
111
|
-
} catch {
|
|
112
|
-
// `containerFor` has already said what failed, where it was not a
|
|
113
|
-
// shortage of bytes; either way nothing was kept.
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* @param {object} params - same as containerFor
|
|
120
|
-
* @returns {Promise<import("./tracks/index.js").ContainerTrack[]>} Empty while
|
|
121
|
-
* the bytes have not arrived; that empty answer is not kept.
|
|
122
|
-
*/
|
|
123
|
-
async getTracks(params) {
|
|
124
|
-
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
125
|
-
const known = this.tracks.get(key);
|
|
126
|
-
if (known) {
|
|
127
|
-
return known;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
const container = await this.containerFor(params);
|
|
131
|
-
if (!container) return [];
|
|
132
|
-
const tracks = await container.readTracks();
|
|
133
|
-
if (Array.isArray(tracks)) {
|
|
134
|
-
this.tracks.set(key, tracks);
|
|
135
|
-
return tracks;
|
|
136
|
-
}
|
|
137
|
-
return [];
|
|
138
|
-
} catch (e) {
|
|
139
|
-
if (!isUnavailable(e)) {
|
|
140
|
-
logger.warn(`container: readTracks failed for "${params.label}": ${e?.message ?? e}`);
|
|
141
|
-
}
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* What the file declares about itself as a whole — format, duration, and
|
|
148
|
-
* where its own timeline begins.
|
|
149
|
-
*
|
|
150
|
-
* A `null` field means the container does not declare it, which is a final
|
|
151
|
-
* answer about the container. A null RESULT means the bytes have not arrived,
|
|
152
|
-
* or the format is unknown; neither is kept here.
|
|
153
|
-
*
|
|
154
|
-
* @param {object} params - same as containerFor
|
|
155
|
-
* @returns {Promise<import("./container/Container.js").ContainerMediaInfo|null>}
|
|
156
|
-
*/
|
|
157
|
-
async getMediaInfo(params) {
|
|
158
|
-
try {
|
|
159
|
-
const container = await this.containerFor(params);
|
|
160
|
-
if (!container) return null;
|
|
161
|
-
return await container.readMediaInfo();
|
|
162
|
-
} catch (e) {
|
|
163
|
-
if (!isUnavailable(e)) {
|
|
164
|
-
logger.warn(`container: readMediaInfo failed for "${params.label}": ${e?.message ?? e}`);
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* @param {object} params - same as containerFor
|
|
172
|
-
* @returns {Promise<{times:number[],tolerance:number}|null>} Null where the
|
|
173
|
-
* file has no usable index, or is of no known format.
|
|
174
|
-
* @throws {import("./container/unavailable.js").BytesUnavailable} While the
|
|
175
|
-
* bytes the index needs have not arrived — so that the keyframe table does
|
|
176
|
-
* not record "no keyframes" for a file whose index is still downloading.
|
|
177
|
-
*/
|
|
178
|
-
async getKeyframeIndex(params) {
|
|
179
|
-
const container = await this.containerFor(params);
|
|
180
|
-
if (!container) return null;
|
|
181
|
-
try {
|
|
182
|
-
return await container.readKeyframeIndex();
|
|
183
|
-
} catch (e) {
|
|
184
|
-
if (isUnavailable(e)) throw e;
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
forget(sourceKey, fileIndex) {
|
|
190
|
-
if (fileIndex === undefined) {
|
|
191
|
-
for (const k of [...this.cache.keys()]) if (k.startsWith(`${sourceKey}:`)) this.cache.delete(k);
|
|
192
|
-
for (const k of [...this.pending.keys()]) if (k.startsWith(`${sourceKey}:`)) this.pending.delete(k);
|
|
193
|
-
for (const k of [...this.tracks.keys()]) if (k.startsWith(`${sourceKey}:`)) this.tracks.delete(k);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
this.cache.delete(`${sourceKey}:${fileIndex}`);
|
|
197
|
-
this.pending.delete(`${sourceKey}:${fileIndex}`);
|
|
198
|
-
this.tracks.delete(`${sourceKey}:${fileIndex}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export const containerOrchestrator = new ContainerOrchestrator();
|
|
1
|
+
/**
|
|
2
|
+
* @file Container orchestrator — application layer over Container domain.
|
|
3
|
+
*
|
|
4
|
+
* Holds ONE Container per file (key sourceKey:fileIndex), and every reader of a
|
|
5
|
+
* file's statements — the track table, the media info, the keyframe table, the
|
|
6
|
+
* subtitle plan and the cue walk — goes through that one. The subtitle walk used
|
|
7
|
+
* to build a second container over the same file with readers of its own, which
|
|
8
|
+
* read the head and the Cues table a second time under a different rule for a
|
|
9
|
+
* read that had not arrived (`research/subtitles-never-appear-2026-10-01.md`).
|
|
10
|
+
*
|
|
11
|
+
* What is kept, and what is not: a container is kept once the bytes have said
|
|
12
|
+
* what format they are (including "none this proxy knows"); a head that has not
|
|
13
|
+
* arrived throws `BytesUnavailable` and leaves nothing behind, so the next ask
|
|
14
|
+
* reads again. Delegates format detection to ContainerFactory. Transport-
|
|
15
|
+
* agnostic — takes readRange, knows nothing about torrents or HTTP.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
19
|
+
import { isUnavailable } from "./container/unavailable.js";
|
|
20
|
+
import { logger } from "../../utils/logger.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How much of a file's head its track table lives in.
|
|
24
|
+
*
|
|
25
|
+
* A fact of the FORMATS, not of the torrent: Matroska puts its Tracks element
|
|
26
|
+
* in the head and MP4's `moov` is there or pointed to from there. Whoever
|
|
27
|
+
* fetches bytes before asking is told this figure rather than choosing one.
|
|
28
|
+
*/
|
|
29
|
+
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
30
|
+
|
|
31
|
+
export class ContainerOrchestrator {
|
|
32
|
+
constructor() {
|
|
33
|
+
/** @type {Map<string, import("./container/Container.js").Container|null>} */
|
|
34
|
+
this.cache = new Map();
|
|
35
|
+
/** @type {Map<string, Promise<import("./container/Container.js").Container|null>>} */
|
|
36
|
+
this.pending = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* What each file's container declares, once it has said it.
|
|
39
|
+
*
|
|
40
|
+
* The track table is asked for the audio menu, the subtitle defaults and
|
|
41
|
+
* the video facts of one file; the container keeps its own reading, and
|
|
42
|
+
* this keeps the answer so the question costs nothing the second time.
|
|
43
|
+
*
|
|
44
|
+
* @type {Map<string, import("./tracks/index.js").ContainerTrack[]>}
|
|
45
|
+
*/
|
|
46
|
+
this.tracks = new Map();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The container already built for a file, if any — without reading anything.
|
|
51
|
+
*
|
|
52
|
+
* `undefined` means none has been built yet; `null` means the bytes were read
|
|
53
|
+
* and are no format this proxy knows.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} sourceKey
|
|
56
|
+
* @param {number} fileIndex
|
|
57
|
+
* @returns {import("./container/Container.js").Container | null | undefined}
|
|
58
|
+
*/
|
|
59
|
+
known(sourceKey, fileIndex) {
|
|
60
|
+
return this.cache.get(`${sourceKey}:${fileIndex}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The file's one container, built on first ask.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} params
|
|
67
|
+
* @param {string} params.sourceKey
|
|
68
|
+
* @param {number} params.fileIndex
|
|
69
|
+
* @param {(start:number,end:number)=>Promise<Buffer|null>} params.readRange
|
|
70
|
+
* @param {number} params.fileSize
|
|
71
|
+
* @param {string} [params.label]
|
|
72
|
+
* @param {number} [params.portionBytes]
|
|
73
|
+
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
74
|
+
* @throws {import("./container/unavailable.js").BytesUnavailable} While the
|
|
75
|
+
* head has not arrived. Nothing is kept for it.
|
|
76
|
+
*/
|
|
77
|
+
async containerFor({ sourceKey, fileIndex, readRange, fileSize, label = "", portionBytes }) {
|
|
78
|
+
const key = `${sourceKey}:${fileIndex}`;
|
|
79
|
+
if (this.cache.has(key)) return this.cache.get(key);
|
|
80
|
+
if (this.pending.has(key)) return this.pending.get(key);
|
|
81
|
+
const p = ContainerFactory.create({ readRange, fileSize, label, portionBytes })
|
|
82
|
+
.then((c) => {
|
|
83
|
+
this.cache.set(key, c);
|
|
84
|
+
if (c) logger.info(`container: ${c.formatName} for "${label}"`);
|
|
85
|
+
else logger.info(`container: unknown for "${label}"`);
|
|
86
|
+
return c;
|
|
87
|
+
})
|
|
88
|
+
.catch((e) => {
|
|
89
|
+
if (!isUnavailable(e)) {
|
|
90
|
+
logger.warn(`container: failed for "${label}": ${e?.message ?? e}`);
|
|
91
|
+
}
|
|
92
|
+
throw e;
|
|
93
|
+
})
|
|
94
|
+
.finally(() => {
|
|
95
|
+
this.pending.delete(key);
|
|
96
|
+
});
|
|
97
|
+
this.pending.set(key, p);
|
|
98
|
+
return p;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The same, answering null rather than throwing while the head is not here —
|
|
103
|
+
* for the callers whose own answer to "not yet" is "nothing yet".
|
|
104
|
+
*
|
|
105
|
+
* @param {object} params - same as containerFor
|
|
106
|
+
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
107
|
+
*/
|
|
108
|
+
async getContainer(params) {
|
|
109
|
+
try {
|
|
110
|
+
return await this.containerFor(params);
|
|
111
|
+
} catch {
|
|
112
|
+
// `containerFor` has already said what failed, where it was not a
|
|
113
|
+
// shortage of bytes; either way nothing was kept.
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {object} params - same as containerFor
|
|
120
|
+
* @returns {Promise<import("./tracks/index.js").ContainerTrack[]>} Empty while
|
|
121
|
+
* the bytes have not arrived; that empty answer is not kept.
|
|
122
|
+
*/
|
|
123
|
+
async getTracks(params) {
|
|
124
|
+
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
125
|
+
const known = this.tracks.get(key);
|
|
126
|
+
if (known) {
|
|
127
|
+
return known;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const container = await this.containerFor(params);
|
|
131
|
+
if (!container) return [];
|
|
132
|
+
const tracks = await container.readTracks();
|
|
133
|
+
if (Array.isArray(tracks)) {
|
|
134
|
+
this.tracks.set(key, tracks);
|
|
135
|
+
return tracks;
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (!isUnavailable(e)) {
|
|
140
|
+
logger.warn(`container: readTracks failed for "${params.label}": ${e?.message ?? e}`);
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* What the file declares about itself as a whole — format, duration, and
|
|
148
|
+
* where its own timeline begins.
|
|
149
|
+
*
|
|
150
|
+
* A `null` field means the container does not declare it, which is a final
|
|
151
|
+
* answer about the container. A null RESULT means the bytes have not arrived,
|
|
152
|
+
* or the format is unknown; neither is kept here.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} params - same as containerFor
|
|
155
|
+
* @returns {Promise<import("./container/Container.js").ContainerMediaInfo|null>}
|
|
156
|
+
*/
|
|
157
|
+
async getMediaInfo(params) {
|
|
158
|
+
try {
|
|
159
|
+
const container = await this.containerFor(params);
|
|
160
|
+
if (!container) return null;
|
|
161
|
+
return await container.readMediaInfo();
|
|
162
|
+
} catch (e) {
|
|
163
|
+
if (!isUnavailable(e)) {
|
|
164
|
+
logger.warn(`container: readMediaInfo failed for "${params.label}": ${e?.message ?? e}`);
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {object} params - same as containerFor
|
|
172
|
+
* @returns {Promise<{times:number[],tolerance:number}|null>} Null where the
|
|
173
|
+
* file has no usable index, or is of no known format.
|
|
174
|
+
* @throws {import("./container/unavailable.js").BytesUnavailable} While the
|
|
175
|
+
* bytes the index needs have not arrived — so that the keyframe table does
|
|
176
|
+
* not record "no keyframes" for a file whose index is still downloading.
|
|
177
|
+
*/
|
|
178
|
+
async getKeyframeIndex(params) {
|
|
179
|
+
const container = await this.containerFor(params);
|
|
180
|
+
if (!container) return null;
|
|
181
|
+
try {
|
|
182
|
+
return await container.readKeyframeIndex();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (isUnavailable(e)) throw e;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
forget(sourceKey, fileIndex) {
|
|
190
|
+
if (fileIndex === undefined) {
|
|
191
|
+
for (const k of [...this.cache.keys()]) if (k.startsWith(`${sourceKey}:`)) this.cache.delete(k);
|
|
192
|
+
for (const k of [...this.pending.keys()]) if (k.startsWith(`${sourceKey}:`)) this.pending.delete(k);
|
|
193
|
+
for (const k of [...this.tracks.keys()]) if (k.startsWith(`${sourceKey}:`)) this.tracks.delete(k);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.cache.delete(`${sourceKey}:${fileIndex}`);
|
|
197
|
+
this.pending.delete(`${sourceKey}:${fileIndex}`);
|
|
198
|
+
this.tracks.delete(`${sourceKey}:${fileIndex}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const containerOrchestrator = new ContainerOrchestrator();
|