@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.28",
3
+ "version": "2.107.30",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
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 = this.#rankAt(request.output, serves);
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 resolved = await this.#resolveTimed(request.timing, output, from, end).catch(() => null);
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, so an answer can wait for every
151
- // read asked before it. Told to whoever asked, apart from the read itself,
152
- // because the two call for different remedies (torrent-tv/meta#166).
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 reading = previous.catch(() => undefined).then(async () => {
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
- if (!aside) this.#reads.set(queueKey, reading);
176
- try {
177
- const { result, revision } = await reading;
178
- if (!isCurrent()) return { kind: "terminal", reason: "request-obsolete", requestId: params.requestId };
179
- if (result.kind === "needs-ranges") params.onNeedsRanges?.(result);
180
- if (statement === "tracks" && result.kind === "result") params.onTracks?.(result.value);
181
- await params.onReadResult?.(statement, result, revision);
182
- return result;
183
- } finally {
184
- if (this.#reads.get(queueKey) === reading) this.#reads.delete(queueKey);
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
- await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
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 (statement === "chapters" && params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
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.#reads.delete(key);
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 this.readRange(0, 29);
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 this.readRange(this.#offset, this.#offset + 23);
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, this.readRange) : null;
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: this.readRange, fileSize: this.fileSize, tracks: this.#tracks,
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: this.readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom, isCurrent });
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: this.readRange, fileSize: this.fileSize, info: this.#info });
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: this.readRange, at, end: data.end, info: this.#info }));
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
- const bytes = await read(start, last);
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) {