@torrent-tv/proxy 2.107.27 → 2.107.29
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 +7 -3
- package/services/encode/EncodeInputs.js +11 -4
- package/services/media/ContainerOrchestrator.js +5 -4
- package/services/media/container/AsfContainer.js +37 -22
- package/services/media/container/Container.js +1 -1
- package/services/media/container/asf-packets.js +12 -3
- package/services/media/container/unavailable.js +24 -4
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1023,7 +1023,7 @@ export async function startProxyServer({
|
|
|
1023
1023
|
},
|
|
1024
1024
|
// `timing`, when given, is told how long each read of a file waited behind
|
|
1025
1025
|
// the reads asked before it and how long it took (torrent-tv/meta#166).
|
|
1026
|
-
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null) => {
|
|
1026
|
+
resolveEncodeInput: async (output, fromIndex, toIndex, timing = null, isCurrent = null, signal = null) => {
|
|
1027
1027
|
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
1028
1028
|
const from = grid?.[fromIndex], to = grid?.[toIndex + 1];
|
|
1029
1029
|
if (!Number.isFinite(from) || !(to > from)) return { kind: "terminal", reason: "output-interval-not-declared" };
|
|
@@ -1043,12 +1043,17 @@ export async function startProxyServer({
|
|
|
1043
1043
|
for (const [fileIndex, choices] of selected) {
|
|
1044
1044
|
const params = await containerOver({ sourceKey: output.file.sourceKey, fileIndex });
|
|
1045
1045
|
if (!params) return { kind: "needs-source" };
|
|
1046
|
+
if (isCurrent) params.isCurrent = isCurrent;
|
|
1047
|
+
if (signal) params.signal = signal;
|
|
1046
1048
|
if (timing) {
|
|
1047
1049
|
params.walkAccount = timing.walk ??= { lockMs: 0, reads: 0, bytes: 0 };
|
|
1048
|
-
params.onTimed = (
|
|
1050
|
+
params.onTimed = (statement, waitedMs, readMs) => {
|
|
1049
1051
|
timing.reads += 1;
|
|
1050
1052
|
timing.queuedMs += waitedMs;
|
|
1051
1053
|
timing.readMs += readMs;
|
|
1054
|
+
const measured = timing.readsByStatement[statement] ??= { queuedMs: 0, readMs: 0 };
|
|
1055
|
+
measured.queuedMs += waitedMs;
|
|
1056
|
+
measured.readMs += readMs;
|
|
1052
1057
|
};
|
|
1053
1058
|
}
|
|
1054
1059
|
const tracks = await containerOrchestrator.inspect(params, "tracks");
|
|
@@ -1070,7 +1075,6 @@ export async function startProxyServer({
|
|
|
1070
1075
|
if (media.kind !== "result") return media;
|
|
1071
1076
|
const timeShiftSeconds = Number(media.value?.startTimeSeconds) || 0;
|
|
1072
1077
|
const sourceInterval = output.timeline.sourceInterval(fromIndex, toIndex, timeShiftSeconds);
|
|
1073
|
-
if (isCurrent) params.isCurrent = isCurrent;
|
|
1074
1078
|
params.packetInterval = { ...sourceInterval,
|
|
1075
1079
|
trackIds: wanted.map(track => track.trackNumber),
|
|
1076
1080
|
modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
|
|
@@ -115,10 +115,10 @@ export class EncodeInputs {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/** The container's statement of an interval, its time added to the request's account. */
|
|
118
|
-
async #resolveTimed(timing, output, from, to, isCurrent = null) {
|
|
118
|
+
async #resolveTimed(timing, output, from, to, isCurrent = null, signal = null) {
|
|
119
119
|
const startedAt = this.#now();
|
|
120
120
|
try {
|
|
121
|
-
return await this.#resolve(output, from, to, timing, isCurrent);
|
|
121
|
+
return await this.#resolve(output, from, to, timing, isCurrent, signal);
|
|
122
122
|
} finally {
|
|
123
123
|
timing.resolutions += 1;
|
|
124
124
|
timing.resolveMs += this.#now() - startedAt;
|
|
@@ -208,6 +208,7 @@ export class EncodeInputs {
|
|
|
208
208
|
: "withdrawn: no wanted window covers it");
|
|
209
209
|
this.#requests.delete(key);
|
|
210
210
|
this.#wanted.delete(key);
|
|
211
|
+
request.controller?.abort();
|
|
211
212
|
request.result?.release?.();
|
|
212
213
|
}
|
|
213
214
|
this.#retryMemory();
|
|
@@ -233,6 +234,7 @@ export class EncodeInputs {
|
|
|
233
234
|
this.#note(request, "withdrawn: the plan places no run there");
|
|
234
235
|
this.#requests.delete(key);
|
|
235
236
|
this.#wanted.delete(key);
|
|
237
|
+
request.controller?.abort();
|
|
236
238
|
// A copy already answered may still be copying its stretch; released,
|
|
237
239
|
// the stretch is dropped when its read returns.
|
|
238
240
|
request.result?.release?.();
|
|
@@ -335,6 +337,7 @@ export class EncodeInputs {
|
|
|
335
337
|
if (request.output !== output) continue;
|
|
336
338
|
this.#requests.delete(key);
|
|
337
339
|
this.#wanted.delete(key);
|
|
340
|
+
request.controller?.abort();
|
|
338
341
|
request.result?.release?.();
|
|
339
342
|
}
|
|
340
343
|
this.#shared.forget(output);
|
|
@@ -404,6 +407,7 @@ export class EncodeInputs {
|
|
|
404
407
|
}
|
|
405
408
|
|
|
406
409
|
#prepare(request) {
|
|
410
|
+
request.controller ??= new AbortController();
|
|
407
411
|
request.pending = true;
|
|
408
412
|
request.revision = this.#revision;
|
|
409
413
|
request.memoryRevision = this.#memoryRevision;
|
|
@@ -413,7 +417,7 @@ export class EncodeInputs {
|
|
|
413
417
|
// field 2026-10-10, a picture's request took 38 s before its copy began and
|
|
414
418
|
// nothing said on what (torrent-tv/meta#166).
|
|
415
419
|
request.timing ??= { askedAt: this.#now(), attempts: 0, resolutions: 0, resolveMs: 0,
|
|
416
|
-
reads: 0, queuedMs: 0, readMs: 0, heldMs: 0, budgetMs: 0 };
|
|
420
|
+
reads: 0, queuedMs: 0, readMs: 0, readsByStatement: {}, heldMs: 0, budgetMs: 0 };
|
|
417
421
|
request.timing.attempts += 1;
|
|
418
422
|
this.#observe(request, "attempt", { request: request.key, attempt: request.timing.attempts });
|
|
419
423
|
request.promise = (async () => {
|
|
@@ -421,7 +425,7 @@ export class EncodeInputs {
|
|
|
421
425
|
// packet walk, left to run, held the file's reads and the piece the
|
|
422
426
|
// viewer had seeked to waited 16.9 s behind it (r59, torrent-tv/meta#178).
|
|
423
427
|
const resolved = await this.#resolveTimed(request.timing, request.output, request.from, request.from,
|
|
424
|
-
() => this.#requests.get(request.key) === request);
|
|
428
|
+
() => this.#requests.get(request.key) === request && !request.controller.signal.aborted, request.controller.signal);
|
|
425
429
|
if (resolved.kind === "result") this.#original.set(request.output, resolved.sources?.every(source => source.input.original === true) === true);
|
|
426
430
|
if (this.#requests.get(request.key) !== request) return;
|
|
427
431
|
let result;
|
|
@@ -742,9 +746,12 @@ function awaitedIn(waiting, arrived) {
|
|
|
742
746
|
*/
|
|
743
747
|
function describeTiming(timing, now) {
|
|
744
748
|
if (!timing) return "";
|
|
749
|
+
const statements = Object.entries(timing.readsByStatement ?? {}).map(([statement, measured]) =>
|
|
750
|
+
`${statement} queued ${measured.queuedMs}ms/read ${measured.readMs}ms`).join(", ");
|
|
745
751
|
return `; asked ${Math.max(0, now - timing.askedAt)}ms ago over ${timing.attempts} attempt(s): ` +
|
|
746
752
|
`${timing.resolutions} resolution(s) ${timing.resolveMs}ms ` +
|
|
747
753
|
`(${timing.reads} file read(s): queued ${timing.queuedMs}ms, reading ${timing.readMs}ms), ` +
|
|
754
|
+
(statements ? `by statement: ${statements}, ` : "") +
|
|
748
755
|
`held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms` +
|
|
749
756
|
// Where a packet input's walk spent its read: waiting for the same
|
|
750
757
|
// track's earlier walks, or reading the file (r62, torrent-tv/meta#178).
|
|
@@ -146,7 +146,7 @@ export class ContainerOrchestrator {
|
|
|
146
146
|
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
147
147
|
let lifetime = this.#lifetimes.get(key);
|
|
148
148
|
if (!lifetime) this.#lifetimes.set(key, lifetime = {});
|
|
149
|
-
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false;
|
|
149
|
+
const isCurrent = () => this.#lifetimes.get(key) === lifetime && params.isCurrent?.() !== false && params.signal?.aborted !== true;
|
|
150
150
|
// Reads of one file run one after another, so an answer can wait for every
|
|
151
151
|
// read asked before it. Told to whoever asked, apart from the read itself,
|
|
152
152
|
// because the two call for different remedies (torrent-tv/meta#166).
|
|
@@ -237,9 +237,10 @@ export class ContainerOrchestrator {
|
|
|
237
237
|
value = await container.readSourceRanges(params.packetInterval);
|
|
238
238
|
if (value.kind !== "result") return { ...value, requestId };
|
|
239
239
|
} else if (statement === "packets") {
|
|
240
|
-
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, account: params.walkAccount });
|
|
240
|
+
value = await container.readPacketIndex(params.packetInterval, { isCurrent: params.isCurrent, signal: params.signal, account: params.walkAccount });
|
|
241
241
|
if (value === null) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
242
|
-
|
|
242
|
+
const readRange = (start, end) => container.readRange(start, end, { signal: params.signal });
|
|
243
|
+
await value?.prepareAudioDependencies?.(params.packetInterval, readRange);
|
|
243
244
|
} else if (statement === "chapters") {
|
|
244
245
|
value = await container.readChapters({ read: params.readHeld ?? params.readRange,
|
|
245
246
|
isCurrent: params.isCurrent,
|
|
@@ -252,7 +253,7 @@ export class ContainerOrchestrator {
|
|
|
252
253
|
}
|
|
253
254
|
return { kind: "result", value, requestId };
|
|
254
255
|
} catch (error) {
|
|
255
|
-
if (
|
|
256
|
+
if (params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
|
|
256
257
|
if (error instanceof ChapterUnsupported) return { kind: "terminal", reason: "chapter-not-supported", chapterReason: error.message, requestId };
|
|
257
258
|
if (error instanceof IndexMemoryUnavailable) {
|
|
258
259
|
const result = { kind: "needs-memory", bytes: error.bytes, requestId };
|
|
@@ -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,15 +212,21 @@ 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) {
|
|
216
|
-
|
|
215
|
+
async readPacketIndex(interval, { isCurrent, signal } = {}) {
|
|
216
|
+
const readRange = signal ? (start, end) => this.readRange(start, end, { signal }) : this.readRange;
|
|
217
|
+
if (isCurrent?.() === false) return null;
|
|
218
|
+
await this.#readHeader(readRange);
|
|
219
|
+
if (isCurrent?.() === false) return null;
|
|
217
220
|
if (!this.#packets && Number.isFinite(interval?.from) && Number.isFinite(interval?.to) && interval.to > interval.from) {
|
|
218
|
-
const window = await this.#readWindow(interval);
|
|
221
|
+
const window = await this.#readWindow(interval, isCurrent, readRange);
|
|
222
|
+
if (isCurrent?.() === false) return null;
|
|
219
223
|
if (window) return window;
|
|
220
224
|
}
|
|
225
|
+
if (isCurrent?.() === false) return null;
|
|
221
226
|
if (!this.#packets) {
|
|
222
|
-
const index = await readAsfPackets({ readRange
|
|
223
|
-
info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory });
|
|
227
|
+
const index = await readAsfPackets({ readRange, fileSize: this.fileSize, tracks: this.#tracks,
|
|
228
|
+
info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory, isCurrent });
|
|
229
|
+
if (index === null) return null;
|
|
224
230
|
const timeline = this.#tracks.find(track => track.type === "video") ?? this.#tracks.find(track => track.type === "audio");
|
|
225
231
|
const bounds = timeline ? index.boundsOf(timeline.trackNumber) : null;
|
|
226
232
|
if (bounds) {
|
|
@@ -246,17 +252,21 @@ export class AsfContainer extends Container {
|
|
|
246
252
|
* interval, is read again from twice as far back, and from the first packet
|
|
247
253
|
* at the latest; which it reaches is checked on what it read, not assumed.
|
|
248
254
|
*/
|
|
249
|
-
async #readWindow(interval) {
|
|
250
|
-
const data = await this.#dataLayout();
|
|
255
|
+
async #readWindow(interval, isCurrent, readRange = this.readRange) {
|
|
256
|
+
const data = await this.#dataLayout(readRange);
|
|
257
|
+
if (isCurrent?.() === false) return null;
|
|
251
258
|
if (!data) return null;
|
|
252
259
|
const requested = this.#tracks.filter(track => ["video", "audio"].includes(track.type) &&
|
|
253
260
|
(!interval.trackIds?.length || interval.trackIds.includes(track.trackNumber)));
|
|
254
261
|
if (!requested.length) return null;
|
|
255
262
|
const info = { ...this.#info, durationSeconds: this.#duration };
|
|
256
263
|
for (let lead = this.#info.prerollSeconds + WINDOW_LEAD_SECONDS; ; lead *= 2) {
|
|
264
|
+
if (isCurrent?.() === false) return null;
|
|
257
265
|
const sentFrom = interval.from - lead;
|
|
258
|
-
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS);
|
|
259
|
-
|
|
266
|
+
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent, readRange);
|
|
267
|
+
if (isCurrent?.() === false) return null;
|
|
268
|
+
const window = await readAsfWindow({ readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom, isCurrent });
|
|
269
|
+
if (isCurrent?.() === false) return null;
|
|
260
270
|
if (window.startCovered || first === 0) return window.index;
|
|
261
271
|
}
|
|
262
272
|
}
|
|
@@ -266,9 +276,9 @@ export class AsfContainer extends Container {
|
|
|
266
276
|
* the format requires it (the File Properties state one length as both the
|
|
267
277
|
* least and the most), and a packet is then found by its number.
|
|
268
278
|
*/
|
|
269
|
-
async #dataLayout() {
|
|
279
|
+
async #dataLayout(readRange = this.readRange) {
|
|
270
280
|
if (this.#data === null) {
|
|
271
|
-
const data = await readAsfData({ readRange
|
|
281
|
+
const data = await readAsfData({ readRange, fileSize: this.fileSize, info: this.#info });
|
|
272
282
|
const length = this.#info.packetLength;
|
|
273
283
|
const fixed = length > 0 && this.#info.minimumPacketLength === length;
|
|
274
284
|
const whole = fixed ? Math.floor((data.end - data.start) / length) : 0;
|
|
@@ -291,14 +301,16 @@ export class AsfContainer extends Container {
|
|
|
291
301
|
* bytes the window from it to `until` is estimated to take, so the swarm is
|
|
292
302
|
* asked for the packet and the window it begins at once.
|
|
293
303
|
*/
|
|
294
|
-
async #packetSentBefore(seconds, data, until) {
|
|
295
|
-
|
|
304
|
+
async #packetSentBefore(seconds, data, until, isCurrent, readRange = this.readRange) {
|
|
305
|
+
if (isCurrent?.() === false) return null;
|
|
306
|
+
let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent, readRange), highSent = this.#duration;
|
|
307
|
+
if (isCurrent?.() === false) return null;
|
|
296
308
|
if (lowSent > seconds) return 0;
|
|
297
309
|
// Past the last packet nothing is sent before the film ends; the last
|
|
298
310
|
// packet itself is read only when the search needs it.
|
|
299
311
|
if (!(highSent > seconds)) {
|
|
300
312
|
high = data.count - 1;
|
|
301
|
-
highSent = await this.#sentAt(high, data);
|
|
313
|
+
highSent = await this.#sentAt(high, data, 1, isCurrent, readRange);
|
|
302
314
|
if (highSent <= seconds) return high;
|
|
303
315
|
}
|
|
304
316
|
for (const [number, at] of this.#sent) {
|
|
@@ -307,12 +319,14 @@ export class AsfContainer extends Container {
|
|
|
307
319
|
else { high = number; highSent = at; }
|
|
308
320
|
}
|
|
309
321
|
for (let guesses = 0; high - low > 1 && seconds - lowSent > WINDOW_SEARCH_SECONDS; guesses++) {
|
|
322
|
+
if (isCurrent?.() === false) return null;
|
|
310
323
|
const perSecond = (high - low) / (highSent - lowSent);
|
|
311
324
|
const aim = seconds - WINDOW_SEARCH_SECONDS / 2;
|
|
312
325
|
const guess = guesses >= 3 || !(perSecond > 0) ? Math.floor((low + high) / 2) : low + Math.floor((aim - lowSent) * perSecond);
|
|
313
326
|
const probe = Math.min(high - 1, Math.max(low + 1, guess));
|
|
314
327
|
const window = perSecond > 0 ? Math.ceil((until - aim) * perSecond) : 1;
|
|
315
|
-
const at = await this.#sentAt(probe, data, Math.max(1, window));
|
|
328
|
+
const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent, readRange);
|
|
329
|
+
if (isCurrent?.() === false) return null;
|
|
316
330
|
if (at <= seconds) { low = probe; lowSent = at; }
|
|
317
331
|
else { high = probe; highSent = at; }
|
|
318
332
|
}
|
|
@@ -323,16 +337,17 @@ export class AsfContainer extends Container {
|
|
|
323
337
|
* When packet `number` is sent. A packet that is not held is said as
|
|
324
338
|
* missing with the `packets` packets from it.
|
|
325
339
|
*/
|
|
326
|
-
async #sentAt(number, data, packets = 1) {
|
|
340
|
+
async #sentAt(number, data, packets = 1, isCurrent, readRange = this.readRange) {
|
|
341
|
+
if (isCurrent?.() === false) return null;
|
|
327
342
|
if (!this.#sent.has(number)) {
|
|
328
343
|
const at = data.start + number * data.length;
|
|
329
344
|
try {
|
|
330
|
-
this.#sent.set(number, await readAsfPacketTime({ readRange
|
|
345
|
+
this.#sent.set(number, await readAsfPacketTime({ readRange, at, end: data.end, info: this.#info }));
|
|
331
346
|
} catch (error) {
|
|
332
347
|
if (!isUnavailable(error) || packets <= 1) throw error;
|
|
333
348
|
throw new BytesUnavailable(at, Math.min(data.start + data.count * data.length, at + packets * data.length) - 1, 0);
|
|
334
349
|
}
|
|
335
350
|
}
|
|
336
|
-
return this.#sent.get(number);
|
|
351
|
+
return isCurrent?.() === false ? null : this.#sent.get(number);
|
|
337
352
|
}
|
|
338
353
|
}
|
|
@@ -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]
|
|
@@ -20,7 +20,7 @@ export async function readAsfData({ readRange, fileSize, info }) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** ASF payload addresses, including fragmented and compressed media objects. */
|
|
23
|
-
export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {} }) {
|
|
23
|
+
export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {}, isCurrent }) {
|
|
24
24
|
if (!state.initialized) {
|
|
25
25
|
const data = await readAsfData({ readRange, fileSize, info });
|
|
26
26
|
Object.assign(state, { initialized: true, at: data.start, end: data.end, count: data.count, packets: 0,
|
|
@@ -30,6 +30,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
|
|
|
30
30
|
const { end, count, frames, assembling } = state;
|
|
31
31
|
const declared = new Map(tracks.map(track => [track.trackNumber, track]));
|
|
32
32
|
while (at < end && (!count || packets < count)) {
|
|
33
|
+
if (isCurrent?.() === false) return null;
|
|
33
34
|
const previousObjects = new Map([...assembling].map(([id, object]) => [id, { ...object, ranges: [...object.ranges] }]));
|
|
34
35
|
const previousLengths = new Map([...frames].map(([id, stream]) => [id, stream.length]));
|
|
35
36
|
try {
|
|
@@ -37,6 +38,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
|
|
|
37
38
|
packets++;
|
|
38
39
|
state.at = at;
|
|
39
40
|
state.packets = packets;
|
|
41
|
+
if (isCurrent?.() === false) return null;
|
|
40
42
|
if (firstPicture) {
|
|
41
43
|
const video = tracks.find(track => track.type === "video");
|
|
42
44
|
const stream = video && frames.get(video.trackNumber);
|
|
@@ -105,7 +107,7 @@ export async function readAsfPacketTime({ readRange, at, end, info }) {
|
|
|
105
107
|
* @param {number} params.sentFrom - When packet `first` is sent, as far as the caller knows.
|
|
106
108
|
* @returns {Promise<{ index: PacketIndex, startCovered: boolean }>}
|
|
107
109
|
*/
|
|
108
|
-
export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom }) {
|
|
110
|
+
export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom, isCurrent }) {
|
|
109
111
|
const declared = new Map(tracks.map(track => [track.trackNumber, track]));
|
|
110
112
|
// Not admitted to IndexMemory: one segment's records, a few hundred, held
|
|
111
113
|
// by the request that asked and dropped with it. Nothing owns a window to
|
|
@@ -125,23 +127,30 @@ export async function readAsfWindow({ readRange, data, tracks, requested, info,
|
|
|
125
127
|
const held = (start, end) => buffer !== null && start >= bufferAt && end < bufferAt + buffer.length;
|
|
126
128
|
const read = (start, end) => held(start, end) ? buffer.subarray(start - bufferAt, end - bufferAt + 1) : readRange(start, end);
|
|
127
129
|
const load = async () => {
|
|
130
|
+
if (isCurrent?.() === false) return false;
|
|
128
131
|
try {
|
|
129
132
|
buffer = await readRange(at, Math.min(dataEnd - 1, at + WINDOW_PORTION_PACKETS * data.length - 1));
|
|
130
133
|
} catch (error) {
|
|
134
|
+
if (isCurrent?.() === false) return false;
|
|
131
135
|
if (!isUnavailable(error)) throw error;
|
|
132
136
|
try {
|
|
133
137
|
buffer = await readRange(at, Math.min(dataEnd - 1, at + data.length - 1));
|
|
134
138
|
} catch (missing) {
|
|
139
|
+
if (isCurrent?.() === false) return false;
|
|
135
140
|
if (!isUnavailable(missing)) throw missing;
|
|
136
141
|
const ahead = Math.ceil(Math.max(0, interval.to + WINDOW_LEAD_SECONDS - sent) * rate);
|
|
137
142
|
throw new BytesUnavailable(at, Math.min(dataEnd - 1, Math.max(at + data.length, at + ahead) - 1), 0);
|
|
138
143
|
}
|
|
139
144
|
}
|
|
145
|
+
if (isCurrent?.() === false) return false;
|
|
140
146
|
bufferAt = at;
|
|
147
|
+
return true;
|
|
141
148
|
};
|
|
142
149
|
for (;;) {
|
|
150
|
+
if (isCurrent?.() === false) return null;
|
|
143
151
|
if (at >= dataEnd) { reachedEnd = true; break; }
|
|
144
|
-
if (!held(at, Math.min(dataEnd, at + data.length) - 1)
|
|
152
|
+
if (!held(at, Math.min(dataEnd, at + data.length) - 1) && !(await load())) return null;
|
|
153
|
+
if (isCurrent?.() === false) return null;
|
|
145
154
|
({ next: at, sent } = await readPacket({ readRange: read, at, end: data.end, info, declared, frames, assembling, started }));
|
|
146
155
|
for (const id of ids) {
|
|
147
156
|
const stream = frames.get(id), times = after.get(id);
|
|
@@ -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) {
|