@torrent-tv/proxy 2.107.28 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.28",
3
+ "version": "2.107.29",
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
@@ -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,6 +1043,8 @@ 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
1050
  params.onTimed = (statement, waitedMs, readMs) => {
@@ -1073,7 +1075,6 @@ export async function startProxyServer({
1073
1075
  if (media.kind !== "result") return media;
1074
1076
  const timeShiftSeconds = Number(media.value?.startTimeSeconds) || 0;
1075
1077
  const sourceInterval = output.timeline.sourceInterval(fromIndex, toIndex, timeShiftSeconds);
1076
- if (isCurrent) params.isCurrent = isCurrent;
1077
1078
  params.packetInterval = { ...sourceInterval,
1078
1079
  trackIds: wanted.map(track => track.trackNumber),
1079
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;
@@ -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;
@@ -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
- await value?.prepareAudioDependencies?.(params.packetInterval, container.readRange);
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 (statement === "chapters" && params.isCurrent?.() === false) return { kind: "terminal", reason: "request-obsolete", requestId };
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 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) {