@torrent-tv/proxy 2.107.28 → 2.107.30
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 +1 -1
- package/server.js +6 -4
- package/services/encode/EncodeInputs.js +31 -5
- package/services/media/ContainerOrchestrator.js +98 -20
- package/services/media/container/AsfContainer.js +21 -20
- package/services/media/container/Container.js +1 -1
- package/services/media/container/unavailable.js +24 -4
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -917,7 +917,7 @@ export async function startProxyServer({
|
|
|
917
917
|
reviseBudget: () => { void outputParts.machineBudget.revise("memory", "media metadata").catch(error => logger.warn(`packet index budget: ${error.message}`)); },
|
|
918
918
|
changed: () => { mediaReads.memoryChanged(); probeReads.memoryChanged(); outputParts.encodeInputs?.memoryChanged(); }
|
|
919
919
|
});
|
|
920
|
-
const containerOver = async ({ sourceKey, fileIndex, requestId, packetInterval, demand }) => {
|
|
920
|
+
const containerOver = async ({ sourceKey, fileIndex, requestId, packetInterval, demand, readPriority }) => {
|
|
921
921
|
const record = sourceRegistry.get(sourceKey);
|
|
922
922
|
if (!record) {
|
|
923
923
|
return null;
|
|
@@ -932,6 +932,7 @@ export async function startProxyServer({
|
|
|
932
932
|
fileIndex,
|
|
933
933
|
requestId,
|
|
934
934
|
packetInterval,
|
|
935
|
+
readPriority,
|
|
935
936
|
packetMemory: indexMemory.forFile(sourceKey, fileIndex),
|
|
936
937
|
downloadInterval: demand ? { from: demand.from, to: demand.to } : null,
|
|
937
938
|
readRange: (start, end) =>
|
|
@@ -1023,7 +1024,7 @@ export async function startProxyServer({
|
|
|
1023
1024
|
},
|
|
1024
1025
|
// `timing`, when given, is told how long each read of a file waited behind
|
|
1025
1026
|
// the reads asked before it and how long it took (torrent-tv/meta#166).
|
|
1026
|
-
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null) => {
|
|
1027
|
+
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null, signal = null, readPriority = null) => {
|
|
1027
1028
|
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
1028
1029
|
const from = grid?.[fromIndex], to = grid?.[toIndex + 1];
|
|
1029
1030
|
if (!Number.isFinite(from) || !(to > from)) return { kind: "terminal", reason: "output-interval-not-declared" };
|
|
@@ -1041,8 +1042,10 @@ export async function startProxyServer({
|
|
|
1041
1042
|
// reassembled packets otherwise.
|
|
1042
1043
|
const files = [];
|
|
1043
1044
|
for (const [fileIndex, choices] of selected) {
|
|
1044
|
-
const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex });
|
|
1045
|
+
const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex, readPriority });
|
|
1045
1046
|
if (!params) return { kind: "needs-source" };
|
|
1047
|
+
if (isCurrent) params.isCurrent = isCurrent;
|
|
1048
|
+
if (signal) params.signal = signal;
|
|
1046
1049
|
if (timing) {
|
|
1047
1050
|
params.walkAccount = timing.walk ??= { lockMs: 0, reads: 0, bytes: 0 };
|
|
1048
1051
|
params.onTimed = (statement, waitedMs, readMs) => {
|
|
@@ -1073,7 +1076,6 @@ export async function startProxyServer({
|
|
|
1073
1076
|
if (media.kind !== "result") return media;
|
|
1074
1077
|
const timeShiftSeconds = Number(media.value?.startTimeSeconds) || 0;
|
|
1075
1078
|
const sourceInterval = output.timeline.sourceInterval(fromIndex, toIndex, timeShiftSeconds);
|
|
1076
|
-
if (isCurrent) params.isCurrent = isCurrent;
|
|
1077
1079
|
params.packetInterval = { ...sourceInterval,
|
|
1078
1080
|
trackIds: wanted.map(track => track.trackNumber),
|
|
1079
1081
|
modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
|
|
@@ -100,7 +100,8 @@ export class EncodeInputs {
|
|
|
100
100
|
if (request.noted === state) return;
|
|
101
101
|
request.noted = state;
|
|
102
102
|
const serves = request.serves ?? request.from;
|
|
103
|
-
const rank =
|
|
103
|
+
const rank = request.readPriority?.serves === serves
|
|
104
|
+
? request.readPriority : this.#rankAt(request.output, serves);
|
|
104
105
|
this.#log(`encode input #${request.from}..#${request.to} of ${request.output.outputKey ?? request.output.id}: ${state}` +
|
|
105
106
|
` (held ${this.held()} of ${this.#allowed} allowed; for #${serves}` +
|
|
106
107
|
`${rank ? `, rank ${rank.rank} of ${rank.topRank}` : ""})${detail}`);
|
|
@@ -115,10 +116,10 @@ export class EncodeInputs {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/** The container's statement of an interval, its time added to the request's account. */
|
|
118
|
-
async #resolveTimed(timing, output, from, to, isCurrent = null) {
|
|
119
|
+
async #resolveTimed(timing, output, from, to, isCurrent = null, signal = null, readPriority = null) {
|
|
119
120
|
const startedAt = this.#now();
|
|
120
121
|
try {
|
|
121
|
-
return await this.#resolve(output, from, to, timing, isCurrent);
|
|
122
|
+
return await this.#resolve(output, from, to, timing, isCurrent, signal, readPriority);
|
|
122
123
|
} finally {
|
|
123
124
|
timing.resolutions += 1;
|
|
124
125
|
timing.resolveMs += this.#now() - startedAt;
|
|
@@ -208,6 +209,7 @@ export class EncodeInputs {
|
|
|
208
209
|
: "withdrawn: no wanted window covers it");
|
|
209
210
|
this.#requests.delete(key);
|
|
210
211
|
this.#wanted.delete(key);
|
|
212
|
+
request.controller?.abort();
|
|
211
213
|
request.result?.release?.();
|
|
212
214
|
}
|
|
213
215
|
this.#retryMemory();
|
|
@@ -233,6 +235,7 @@ export class EncodeInputs {
|
|
|
233
235
|
this.#note(request, "withdrawn: the plan places no run there");
|
|
234
236
|
this.#requests.delete(key);
|
|
235
237
|
this.#wanted.delete(key);
|
|
238
|
+
request.controller?.abort();
|
|
236
239
|
// A copy already answered may still be copying its stretch; released,
|
|
237
240
|
// the stretch is dropped when its read returns.
|
|
238
241
|
request.result?.release?.();
|
|
@@ -335,6 +338,7 @@ export class EncodeInputs {
|
|
|
335
338
|
if (request.output !== output) continue;
|
|
336
339
|
this.#requests.delete(key);
|
|
337
340
|
this.#wanted.delete(key);
|
|
341
|
+
request.controller?.abort();
|
|
338
342
|
request.result?.release?.();
|
|
339
343
|
}
|
|
340
344
|
this.#shared.forget(output);
|
|
@@ -404,7 +408,9 @@ export class EncodeInputs {
|
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
#prepare(request) {
|
|
411
|
+
request.controller ??= new AbortController();
|
|
407
412
|
request.pending = true;
|
|
413
|
+
request.readPriority = this.#readPriority(request, request.from, request.from);
|
|
408
414
|
request.revision = this.#revision;
|
|
409
415
|
request.memoryRevision = this.#memoryRevision;
|
|
410
416
|
request.arrivedMeanwhile = null;
|
|
@@ -421,7 +427,8 @@ export class EncodeInputs {
|
|
|
421
427
|
// packet walk, left to run, held the file's reads and the piece the
|
|
422
428
|
// viewer had seeked to waited 16.9 s behind it (r59, torrent-tv/meta#178).
|
|
423
429
|
const resolved = await this.#resolveTimed(request.timing, request.output, request.from, request.from,
|
|
424
|
-
() => this.#requests.get(request.key) === request
|
|
430
|
+
() => this.#requests.get(request.key) === request && !request.controller.signal.aborted, request.controller.signal,
|
|
431
|
+
request.readPriority);
|
|
425
432
|
if (resolved.kind === "result") this.#original.set(request.output, resolved.sources?.every(source => source.input.original === true) === true);
|
|
426
433
|
if (this.#requests.get(request.key) !== request) return;
|
|
427
434
|
let result;
|
|
@@ -488,6 +495,23 @@ export class EncodeInputs {
|
|
|
488
495
|
});
|
|
489
496
|
}
|
|
490
497
|
|
|
498
|
+
/**
|
|
499
|
+
* The media reads that prepare one input keep the output map's urgency and
|
|
500
|
+
* the segment they serve, so a container can run an exact nearby interval
|
|
501
|
+
* before a speculative attempt to extend that input.
|
|
502
|
+
*
|
|
503
|
+
* @param {object} request
|
|
504
|
+
* @param {number} from
|
|
505
|
+
* @param {number} to
|
|
506
|
+
* @returns {{ rank: number, serves: number, from: number, to: number }}
|
|
507
|
+
*/
|
|
508
|
+
#readPriority(request, from, to) {
|
|
509
|
+
const serves = request.serves ?? request.from;
|
|
510
|
+
const rank = this.#rankAt(request.output, serves);
|
|
511
|
+
return { rank: Number.isFinite(rank?.rank) ? rank.rank : 0, topRank: Number.isFinite(rank?.topRank) ? rank.topRank : 0,
|
|
512
|
+
serves, from, to };
|
|
513
|
+
}
|
|
514
|
+
|
|
491
515
|
/**
|
|
492
516
|
* Whether bytes came during a try that the try may have missed. A try that
|
|
493
517
|
* waits for named bytes is repeated only when some of them came, or when an
|
|
@@ -559,7 +583,9 @@ export class EncodeInputs {
|
|
|
559
583
|
if (!stretches.has(end)) {
|
|
560
584
|
// A longer stretch that cannot be stated is not one to copy; the
|
|
561
585
|
// shorter one already stated still runs.
|
|
562
|
-
const
|
|
586
|
+
const current = () => this.#requests.get(request.key) === request && !request.controller.signal.aborted;
|
|
587
|
+
const resolved = await this.#resolveTimed(request.timing, output, from, end, current, request.controller.signal,
|
|
588
|
+
this.#readPriority(request, from, end)).catch(() => null);
|
|
563
589
|
stretches.set(end, resolved?.kind === "result" && resolved.sources?.every(source => source.input.original === true)
|
|
564
590
|
? { sources: resolved.sources, bytes: originalInputBytes(resolved.sources) } : null);
|
|
565
591
|
}
|
|
@@ -32,6 +32,28 @@ import { logger } from "../../utils/logger.js";
|
|
|
32
32
|
*/
|
|
33
33
|
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
34
34
|
|
|
35
|
+
/** Higher urgency, earlier served time, then the smaller source interval. */
|
|
36
|
+
function compareReadPriority(left, right) {
|
|
37
|
+
const leftRanked = Number.isFinite(left.readPriority?.rank) && left.readPriority.rank > 0;
|
|
38
|
+
const rightRanked = Number.isFinite(right.readPriority?.rank) && right.readPriority.rank > 0;
|
|
39
|
+
if (leftRanked !== rightRanked) return rightRanked - leftRanked;
|
|
40
|
+
if (!leftRanked) return left.sequence - right.sequence;
|
|
41
|
+
const leftRank = left.readPriority.rank;
|
|
42
|
+
const rightRank = right.readPriority.rank;
|
|
43
|
+
if (leftRank !== rightRank) return rightRank - leftRank;
|
|
44
|
+
const leftServes = Number.isFinite(left.readPriority?.serves) ? left.readPriority.serves : Number.POSITIVE_INFINITY;
|
|
45
|
+
const rightServes = Number.isFinite(right.readPriority?.serves) ? right.readPriority.serves : Number.POSITIVE_INFINITY;
|
|
46
|
+
if (leftServes !== rightServes) return leftServes - rightServes;
|
|
47
|
+
const leftSpan = Number.isFinite(left.readPriority?.from) && Number.isFinite(left.readPriority?.to)
|
|
48
|
+
? Math.max(0, left.readPriority.to - left.readPriority.from) : Number.POSITIVE_INFINITY;
|
|
49
|
+
const rightSpan = Number.isFinite(right.readPriority?.from) && Number.isFinite(right.readPriority?.to)
|
|
50
|
+
? Math.max(0, right.readPriority.to - right.readPriority.from) : Number.POSITIVE_INFINITY;
|
|
51
|
+
if (leftSpan !== rightSpan) return leftSpan - rightSpan;
|
|
52
|
+
const leftFrom = Number.isFinite(left.readPriority?.from) ? left.readPriority.from : Number.POSITIVE_INFINITY;
|
|
53
|
+
const rightFrom = Number.isFinite(right.readPriority?.from) ? right.readPriority.from : Number.POSITIVE_INFINITY;
|
|
54
|
+
return leftFrom - rightFrom || left.sequence - right.sequence;
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
/**
|
|
36
58
|
* The size of the file and its OpenSubtitles hash, from the two edges. Nothing
|
|
37
59
|
* partial is kept: an edge that has not arrived is named instead.
|
|
@@ -101,6 +123,7 @@ export class ContainerOrchestrator {
|
|
|
101
123
|
#lifetimes = new Map();
|
|
102
124
|
#activityEpoch = 0;
|
|
103
125
|
#activeReads = 0;
|
|
126
|
+
#readSequence = 0;
|
|
104
127
|
|
|
105
128
|
activity() { return { epoch: this.#activityEpoch, active: this.#activeReads > 0 }; }
|
|
106
129
|
constructor() {
|
|
@@ -146,10 +169,12 @@ export class ContainerOrchestrator {
|
|
|
146
169
|
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
147
170
|
let lifetime = this.#lifetimes.get(key);
|
|
148
171
|
if (!lifetime) this.#lifetimes.set(key, lifetime = {});
|
|
149
|
-
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false;
|
|
150
|
-
// Reads of one file run one after another,
|
|
151
|
-
//
|
|
152
|
-
//
|
|
172
|
+
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false && params.signal?.aborted !== true;
|
|
173
|
+
// Reads of one file run one after another, ordered by playback-map urgency
|
|
174
|
+
// and then by interval size, so speculative extensions cannot sit ahead of
|
|
175
|
+
// an exact interval the viewer needs. Told to whoever asked, apart from
|
|
176
|
+
// the read itself, because the two call for different remedies
|
|
177
|
+
// (torrent-tv/meta#166, #ttv-245).
|
|
153
178
|
// A statement answered from what was already read reads and parses
|
|
154
179
|
// nothing, so it is answered beside the queue and holds nobody up: the
|
|
155
180
|
// picture's first input of an AVI waited about 4 s behind the sound's
|
|
@@ -159,9 +184,8 @@ export class ContainerOrchestrator {
|
|
|
159
184
|
// index. Keep it serial with other chapter reads, but out of the queue used
|
|
160
185
|
// to prepare playback inputs for this file.
|
|
161
186
|
const queueKey = statement === "chapters" ? `${key}:chapters` : key;
|
|
162
|
-
const previous = aside ? Promise.resolve() : this.#reads.get(queueKey) ?? Promise.resolve();
|
|
163
187
|
const askedAt = performance.now();
|
|
164
|
-
const
|
|
188
|
+
const perform = async () => {
|
|
165
189
|
if (!isCurrent()) return { result: { kind: "terminal", reason: "request-obsolete", requestId: params.requestId } };
|
|
166
190
|
const startedAt = performance.now();
|
|
167
191
|
const revision = params.onReadStart?.(statement);
|
|
@@ -171,18 +195,70 @@ export class ContainerOrchestrator {
|
|
|
171
195
|
params.onObserved?.({ statement, askedAt, startedAt, endedAt, queuedMs: startedAt - askedAt,
|
|
172
196
|
readMs: endedAt - startedAt, cached: aside, result: result.kind, reason: result.reason, ranges: result.ranges });
|
|
173
197
|
return { result, revision };
|
|
198
|
+
};
|
|
199
|
+
const reading = aside ? Promise.resolve().then(perform) : this.#queueRead(queueKey, { params, isCurrent, perform });
|
|
200
|
+
const { result, revision } = await reading;
|
|
201
|
+
if (!isCurrent()) return { kind: "terminal", reason: "request-obsolete", requestId: params.requestId };
|
|
202
|
+
if (result.kind === "needs-ranges") params.onNeedsRanges?.(result);
|
|
203
|
+
if (statement === "tracks" && result.kind === "result") params.onTracks?.(result.value);
|
|
204
|
+
await params.onReadResult?.(statement, result, revision);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Run one media read at a time per file, in the map's order of urgency. */
|
|
209
|
+
#queueRead(key, { params, isCurrent, perform }) {
|
|
210
|
+
let queue = this.#reads.get(key);
|
|
211
|
+
if (!queue) this.#reads.set(key, queue = { active: false, pending: [] });
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const entry = { params, isCurrent, perform, resolve, reject, readPriority: params.readPriority,
|
|
214
|
+
sequence: this.#readSequence++, started: false, abort: null };
|
|
215
|
+
const cancel = () => {
|
|
216
|
+
if (entry.started) return;
|
|
217
|
+
const index = queue.pending.indexOf(entry);
|
|
218
|
+
if (index < 0) return;
|
|
219
|
+
queue.pending.splice(index, 1);
|
|
220
|
+
params.signal?.removeEventListener("abort", entry.abort);
|
|
221
|
+
resolve({ result: { kind: "terminal", reason: "request-obsolete", requestId: params.requestId } });
|
|
222
|
+
this.#drainReadQueue(key, queue);
|
|
223
|
+
};
|
|
224
|
+
entry.abort = cancel;
|
|
225
|
+
queue.pending.push(entry);
|
|
226
|
+
queue.pending.sort(compareReadPriority);
|
|
227
|
+
if (params.signal?.aborted) cancel();
|
|
228
|
+
else params.signal?.addEventListener("abort", cancel, { once: true });
|
|
229
|
+
this.#drainReadQueue(key, queue);
|
|
174
230
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#drainReadQueue(key, queue) {
|
|
234
|
+
if (queue.active) return;
|
|
235
|
+
while (queue.pending.length > 0) {
|
|
236
|
+
const entry = queue.pending.shift();
|
|
237
|
+
entry.params.signal?.removeEventListener("abort", entry.abort);
|
|
238
|
+
if (!entry.isCurrent()) {
|
|
239
|
+
entry.resolve({ result: { kind: "terminal", reason: "request-obsolete", requestId: entry.params.requestId } });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
entry.started = true;
|
|
243
|
+
queue.active = true;
|
|
244
|
+
Promise.resolve().then(entry.perform).then(entry.resolve, entry.reject).finally(() => {
|
|
245
|
+
queue.active = false;
|
|
246
|
+
if (queue.pending.length === 0 && this.#reads.get(key) === queue) this.#reads.delete(key);
|
|
247
|
+
else this.#drainReadQueue(key, queue);
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (this.#reads.get(key) === queue) this.#reads.delete(key);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#cancelQueuedReads(key) {
|
|
255
|
+
const queue = this.#reads.get(key);
|
|
256
|
+
if (!queue) return;
|
|
257
|
+
for (const entry of queue.pending.splice(0)) {
|
|
258
|
+
entry.params.signal?.removeEventListener("abort", entry.abort);
|
|
259
|
+
entry.resolve({ result: { kind: "terminal", reason: "request-obsolete", requestId: entry.params.requestId } });
|
|
185
260
|
}
|
|
261
|
+
if (this.#reads.get(key) === queue) this.#reads.delete(key);
|
|
186
262
|
}
|
|
187
263
|
|
|
188
264
|
/** Whether a statement of this file is answered from what was already read: its track table kept here, or the container's word. */
|
|
@@ -237,9 +313,10 @@ export class ContainerOrchestrator {
|
|
|
237
313
|
value = await container.readSourceRanges(params.packetInterval);
|
|
238
314
|
if (value.kind !== "result") return { ...value, requestId };
|
|
239
315
|
} else if (statement === "packets") {
|
|
240
|
-
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, account: params.walkAccount });
|
|
316
|
+
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, signal: params.signal, account: params.walkAccount });
|
|
241
317
|
if (value === null) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
242
|
-
|
|
318
|
+
const readRange = (start, end) => container.readRange(start, end, { signal: params.signal });
|
|
319
|
+
await value?.prepareAudioDependencies?.(params.packetInterval, readRange);
|
|
243
320
|
} else if (statement === "chapters") {
|
|
244
321
|
value = await container.readChapters({ read: params.readHeld ?? params.readRange,
|
|
245
322
|
isCurrent: params.isCurrent,
|
|
@@ -252,7 +329,7 @@ export class ContainerOrchestrator {
|
|
|
252
329
|
}
|
|
253
330
|
return { kind: "result", value, requestId };
|
|
254
331
|
} catch (error) {
|
|
255
|
-
if (
|
|
332
|
+
if (params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
256
333
|
if (error instanceof ChapterUnsupported) return { kind: "terminal", reason: "chapter-not-supported", chapterReason: error.message, requestId };
|
|
257
334
|
if (error instanceof IndexMemoryUnavailable) {
|
|
258
335
|
const result = { kind: "needs-memory", bytes: error.bytes, requestId };
|
|
@@ -379,7 +456,8 @@ export class ContainerOrchestrator {
|
|
|
379
456
|
for (const key of this.#lifetimes.keys()) {
|
|
380
457
|
if (fileIndex === undefined ? key.startsWith(prefix) : key === `${sourceKey}:${fileIndex}`) {
|
|
381
458
|
this.#lifetimes.delete(key);
|
|
382
|
-
this.#
|
|
459
|
+
this.#cancelQueuedReads(key);
|
|
460
|
+
this.#cancelQueuedReads(`${key}:chapters`);
|
|
383
461
|
}
|
|
384
462
|
}
|
|
385
463
|
if (fileIndex === undefined) {
|
|
@@ -42,11 +42,11 @@ export class AsfContainer extends Container {
|
|
|
42
42
|
}
|
|
43
43
|
static detect(head) { return head.length >= 16 && head.subarray(0, 16).toString("hex") === HEADER; }
|
|
44
44
|
|
|
45
|
-
async #readHeader() {
|
|
45
|
+
async #readHeader(readRange = this.readRange) {
|
|
46
46
|
if (this.#info) return;
|
|
47
47
|
this.#declarations ??= new RetainedReads(this.packetMemory);
|
|
48
48
|
if (!this.#header) {
|
|
49
|
-
const bytes = await
|
|
49
|
+
const bytes = await readRange(0, 29);
|
|
50
50
|
if (!AsfContainer.detect(bytes) || bytes[28] !== 1 || bytes[29] !== 2) throw new Error("ASF header is invalid.");
|
|
51
51
|
const size = Number(bytes.readBigUInt64LE(16));
|
|
52
52
|
const count = bytes.readUInt32LE(24);
|
|
@@ -56,7 +56,7 @@ export class AsfContainer extends Container {
|
|
|
56
56
|
this.#header = { size, count };
|
|
57
57
|
}
|
|
58
58
|
while (this.#objects.length < this.#header.count) {
|
|
59
|
-
const head = await
|
|
59
|
+
const head = await readRange(this.#offset, this.#offset + 23);
|
|
60
60
|
const size = Number(head.readBigUInt64LE(16));
|
|
61
61
|
if (!Number.isSafeInteger(size) || size < 24 || this.#offset + size > this.#header.size) {
|
|
62
62
|
throw new Error("ASF child object exceeds its header.");
|
|
@@ -64,7 +64,7 @@ export class AsfContainer extends Container {
|
|
|
64
64
|
const guid = head.subarray(0, 16).toString("hex");
|
|
65
65
|
if (ENCRYPTION.has(guid)) throw new Error("ASF media is encrypted.");
|
|
66
66
|
const bytes = [FILE, STREAM, EXTENSION, EXTENDED_STREAM, LANGUAGES].includes(guid)
|
|
67
|
-
? await this.#declarations.read(this.#offset + 24, this.#offset + size - 1,
|
|
67
|
+
? await this.#declarations.read(this.#offset + 24, this.#offset + size - 1, readRange) : null;
|
|
68
68
|
this.#objects.push({ guid, bytes });
|
|
69
69
|
this.#offset += size;
|
|
70
70
|
}
|
|
@@ -212,18 +212,19 @@ export class AsfContainer extends Container {
|
|
|
212
212
|
const index = await this.readPacketIndex();
|
|
213
213
|
return { times: index.keyframesOf(track.trackNumber), tolerance: 0 };
|
|
214
214
|
}
|
|
215
|
-
async readPacketIndex(interval, { isCurrent } = {}) {
|
|
215
|
+
async readPacketIndex(interval, { isCurrent, signal } = {}) {
|
|
216
|
+
const readRange = signal ? (start, end) => this.readRange(start, end, { signal }) : this.readRange;
|
|
216
217
|
if (isCurrent?.() === false) return null;
|
|
217
|
-
await this.#readHeader();
|
|
218
|
+
await this.#readHeader(readRange);
|
|
218
219
|
if (isCurrent?.() === false) return null;
|
|
219
220
|
if (!this.#packets && Number.isFinite(interval?.from) && Number.isFinite(interval?.to) && interval.to > interval.from) {
|
|
220
|
-
const window = await this.#readWindow(interval, isCurrent);
|
|
221
|
+
const window = await this.#readWindow(interval, isCurrent, readRange);
|
|
221
222
|
if (isCurrent?.() === false) return null;
|
|
222
223
|
if (window) return window;
|
|
223
224
|
}
|
|
224
225
|
if (isCurrent?.() === false) return null;
|
|
225
226
|
if (!this.#packets) {
|
|
226
|
-
const index = await readAsfPackets({ readRange
|
|
227
|
+
const index = await readAsfPackets({ readRange, fileSize: this.fileSize, tracks: this.#tracks,
|
|
227
228
|
info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory, isCurrent });
|
|
228
229
|
if (index === null) return null;
|
|
229
230
|
const timeline = this.#tracks.find(track => track.type === "video") ?? this.#tracks.find(track => track.type === "audio");
|
|
@@ -251,8 +252,8 @@ export class AsfContainer extends Container {
|
|
|
251
252
|
* interval, is read again from twice as far back, and from the first packet
|
|
252
253
|
* at the latest; which it reaches is checked on what it read, not assumed.
|
|
253
254
|
*/
|
|
254
|
-
async #readWindow(interval, isCurrent) {
|
|
255
|
-
const data = await this.#dataLayout();
|
|
255
|
+
async #readWindow(interval, isCurrent, readRange = this.readRange) {
|
|
256
|
+
const data = await this.#dataLayout(readRange);
|
|
256
257
|
if (isCurrent?.() === false) return null;
|
|
257
258
|
if (!data) return null;
|
|
258
259
|
const requested = this.#tracks.filter(track => ["video", "audio"].includes(track.type) &&
|
|
@@ -262,9 +263,9 @@ export class AsfContainer extends Container {
|
|
|
262
263
|
for (let lead = this.#info.prerollSeconds + WINDOW_LEAD_SECONDS; ; lead *= 2) {
|
|
263
264
|
if (isCurrent?.() === false) return null;
|
|
264
265
|
const sentFrom = interval.from - lead;
|
|
265
|
-
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent);
|
|
266
|
+
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent, readRange);
|
|
266
267
|
if (isCurrent?.() === false) return null;
|
|
267
|
-
const window = await readAsfWindow({ readRange
|
|
268
|
+
const window = await readAsfWindow({ readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom, isCurrent });
|
|
268
269
|
if (isCurrent?.() === false) return null;
|
|
269
270
|
if (window.startCovered || first === 0) return window.index;
|
|
270
271
|
}
|
|
@@ -275,9 +276,9 @@ export class AsfContainer extends Container {
|
|
|
275
276
|
* the format requires it (the File Properties state one length as both the
|
|
276
277
|
* least and the most), and a packet is then found by its number.
|
|
277
278
|
*/
|
|
278
|
-
async #dataLayout() {
|
|
279
|
+
async #dataLayout(readRange = this.readRange) {
|
|
279
280
|
if (this.#data === null) {
|
|
280
|
-
const data = await readAsfData({ readRange
|
|
281
|
+
const data = await readAsfData({ readRange, fileSize: this.fileSize, info: this.#info });
|
|
281
282
|
const length = this.#info.packetLength;
|
|
282
283
|
const fixed = length > 0 && this.#info.minimumPacketLength === length;
|
|
283
284
|
const whole = fixed ? Math.floor((data.end - data.start) / length) : 0;
|
|
@@ -300,16 +301,16 @@ export class AsfContainer extends Container {
|
|
|
300
301
|
* bytes the window from it to `until` is estimated to take, so the swarm is
|
|
301
302
|
* asked for the packet and the window it begins at once.
|
|
302
303
|
*/
|
|
303
|
-
async #packetSentBefore(seconds, data, until, isCurrent) {
|
|
304
|
+
async #packetSentBefore(seconds, data, until, isCurrent, readRange = this.readRange) {
|
|
304
305
|
if (isCurrent?.() === false) return null;
|
|
305
|
-
let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent), highSent = this.#duration;
|
|
306
|
+
let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent, readRange), highSent = this.#duration;
|
|
306
307
|
if (isCurrent?.() === false) return null;
|
|
307
308
|
if (lowSent > seconds) return 0;
|
|
308
309
|
// Past the last packet nothing is sent before the film ends; the last
|
|
309
310
|
// packet itself is read only when the search needs it.
|
|
310
311
|
if (!(highSent > seconds)) {
|
|
311
312
|
high = data.count - 1;
|
|
312
|
-
highSent = await this.#sentAt(high, data, 1, isCurrent);
|
|
313
|
+
highSent = await this.#sentAt(high, data, 1, isCurrent, readRange);
|
|
313
314
|
if (highSent <= seconds) return high;
|
|
314
315
|
}
|
|
315
316
|
for (const [number, at] of this.#sent) {
|
|
@@ -324,7 +325,7 @@ export class AsfContainer extends Container {
|
|
|
324
325
|
const guess = guesses >= 3 || !(perSecond > 0) ? Math.floor((low + high) / 2) : low + Math.floor((aim - lowSent) * perSecond);
|
|
325
326
|
const probe = Math.min(high - 1, Math.max(low + 1, guess));
|
|
326
327
|
const window = perSecond > 0 ? Math.ceil((until - aim) * perSecond) : 1;
|
|
327
|
-
const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent);
|
|
328
|
+
const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent, readRange);
|
|
328
329
|
if (isCurrent?.() === false) return null;
|
|
329
330
|
if (at <= seconds) { low = probe; lowSent = at; }
|
|
330
331
|
else { high = probe; highSent = at; }
|
|
@@ -336,12 +337,12 @@ export class AsfContainer extends Container {
|
|
|
336
337
|
* When packet `number` is sent. A packet that is not held is said as
|
|
337
338
|
* missing with the `packets` packets from it.
|
|
338
339
|
*/
|
|
339
|
-
async #sentAt(number, data, packets = 1, isCurrent) {
|
|
340
|
+
async #sentAt(number, data, packets = 1, isCurrent, readRange = this.readRange) {
|
|
340
341
|
if (isCurrent?.() === false) return null;
|
|
341
342
|
if (!this.#sent.has(number)) {
|
|
342
343
|
const at = data.start + number * data.length;
|
|
343
344
|
try {
|
|
344
|
-
this.#sent.set(number, await readAsfPacketTime({ readRange
|
|
345
|
+
this.#sent.set(number, await readAsfPacketTime({ readRange, at, end: data.end, info: this.#info }));
|
|
345
346
|
} catch (error) {
|
|
346
347
|
if (!isUnavailable(error) || packets <= 1) throw error;
|
|
347
348
|
throw new BytesUnavailable(at, Math.min(data.start + data.count * data.length, at + packets * data.length) - 1, 0);
|
|
@@ -54,7 +54,7 @@ export class Container {
|
|
|
54
54
|
* the medium — a torrent's piece length. Absent, an element is read in one go.
|
|
55
55
|
*
|
|
56
56
|
* @param {object} params
|
|
57
|
-
* @param {(start:number,end:number)=>Promise<Buffer|null>} [params.readRange]
|
|
57
|
+
* @param {(start:number,end:number,options?:{signal?:AbortSignal})=>Promise<Buffer|null>} [params.readRange]
|
|
58
58
|
* @param {number} [params.fileSize]
|
|
59
59
|
* @param {string} [params.label]
|
|
60
60
|
* @param {number} [params.portionBytes]
|
|
@@ -56,17 +56,37 @@ export function isUnavailable(error) {
|
|
|
56
56
|
* the clamped range is a read that did not finish: a stream that ended early,
|
|
57
57
|
* a piece that is not downloaded, a store that could not serve it.
|
|
58
58
|
*
|
|
59
|
-
* @param {(start: number, end: number) => Promise<Buffer | null>} read
|
|
59
|
+
* @param {(start: number, end: number, options?: { signal?: AbortSignal }) => Promise<Buffer | null>} read
|
|
60
60
|
* @param {number} fileSize
|
|
61
|
-
* @returns {(start: number, end: number) => Promise<Buffer>}
|
|
61
|
+
* @returns {(start: number, end: number, options?: { signal?: AbortSignal }) => Promise<Buffer>}
|
|
62
62
|
*/
|
|
63
63
|
export function strictReader(read, fileSize) {
|
|
64
|
-
return async (start, end) => {
|
|
64
|
+
return async (start, end, { signal } = {}) => {
|
|
65
65
|
const last = Math.min(end, fileSize - 1);
|
|
66
66
|
if (!(start >= 0) || last < start) {
|
|
67
67
|
return Buffer.alloc(0);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
let bytes;
|
|
70
|
+
if (!signal) {
|
|
71
|
+
bytes = await read(start, last);
|
|
72
|
+
} else {
|
|
73
|
+
const abort = () => {
|
|
74
|
+
const error = new Error("Read cancelled.");
|
|
75
|
+
error.name = "AbortError";
|
|
76
|
+
return error;
|
|
77
|
+
};
|
|
78
|
+
if (signal.aborted) throw abort();
|
|
79
|
+
let onAbort;
|
|
80
|
+
const cancelled = new Promise((_, reject) => {
|
|
81
|
+
onAbort = () => reject(abort());
|
|
82
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
bytes = await Promise.race([read(start, last, { signal }), cancelled]);
|
|
86
|
+
} finally {
|
|
87
|
+
signal.removeEventListener("abort", onAbort);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
70
90
|
const wanted = last - start + 1;
|
|
71
91
|
const received = bytes ? bytes.length : 0;
|
|
72
92
|
if (received < wanted) {
|