@torrent-tv/proxy 2.105.0 → 2.105.2

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.105.0",
3
+ "version": "2.105.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -155,7 +155,11 @@ export class ContainerOrchestrator {
155
155
  // picture's first input of an AVI waited about 4 s behind the sound's
156
156
  // table of the whole film it does not use (r38, torrent-tv/meta#178).
157
157
  const aside = this.#answersWithoutReading(key, statement);
158
- const previous = aside ? Promise.resolve() : this.#reads.get(key) ?? Promise.resolve();
158
+ // Chapter discovery can walk a whole file when the format has no chapter
159
+ // index. Keep it serial with other chapter reads, but out of the queue used
160
+ // to prepare playback inputs for this file.
161
+ const queueKey = statement === "chapters" ? `${key}:chapters` : key;
162
+ const previous = aside ? Promise.resolve() : this.#reads.get(queueKey) ?? Promise.resolve();
159
163
  const askedAt = performance.now();
160
164
  const reading = previous.catch(() => undefined).then(async () => {
161
165
  if (!isCurrent()) return { result: { kind: "terminal", reason: "request-obsolete", requestId: params.requestId } };
@@ -168,7 +172,7 @@ export class ContainerOrchestrator {
168
172
  readMs: endedAt - startedAt, cached: aside, result: result.kind, reason: result.reason, ranges: result.ranges });
169
173
  return { result, revision };
170
174
  });
171
- if (!aside) this.#reads.set(key, reading);
175
+ if (!aside) this.#reads.set(queueKey, reading);
172
176
  try {
173
177
  const { result, revision } = await reading;
174
178
  if (!isCurrent()) return { kind: "terminal", reason: "request-obsolete", requestId: params.requestId };
@@ -177,7 +181,7 @@ export class ContainerOrchestrator {
177
181
  await params.onReadResult?.(statement, result, revision);
178
182
  return result;
179
183
  } finally {
180
- if (this.#reads.get(key) === reading) this.#reads.delete(key);
184
+ if (this.#reads.get(queueKey) === reading) this.#reads.delete(queueKey);
181
185
  }
182
186
  }
183
187
 
@@ -11,11 +11,12 @@ const TOP_LEVEL = new Set([SEEK_HEAD, CHAPTERS, CLUSTER, 0x1549a966, 0x1654ae6b,
11
11
  async function header(reader, at, limit) {
12
12
  reader.step();
13
13
  const vint = async (position, marker) => {
14
- const first = (await reader.bytes(position, 1))[0];
14
+ const firstBytes = await reader.bytes(position, 1);
15
+ const first = firstBytes[0];
15
16
  let n = 1;
16
17
  while (n <= 8 && !(first & (0x80 >> (n - 1)))) n++;
17
18
  if (n > (marker ? 4 : 8) || position + n > limit) throw new Error("Invalid chapter EBML header");
18
- const bytes = await reader.bytes(position, n);
19
+ const bytes = n === 1 ? firstBytes : Buffer.concat([firstBytes, await reader.bytes(position + 1, n - 1)]);
19
20
  const value = readVint(bytes, 0, marker);
20
21
  const mask = 2 ** (8 - n) - 1;
21
22
  return { ...value, unknown: !marker && (bytes[0] & mask) === mask && bytes.subarray(1).every((byte) => byte === 255) };
@@ -575,6 +575,7 @@ export class ViewerRequests {
575
575
  // still answered by the output it was made against — see the two deadlines
576
576
  // in `Assignments.js`.
577
577
  viewer.assignments.statedGeneration(generation);
578
+ this.#beginSeekWait(named, consumerId, generation);
578
579
  this.#host.outputs.touch(named);
579
580
  // EVERY REQUEST THIS VIEWER HAS OUTSTANDING WAS MADE FOR WHERE THEY WERE,
580
581
  // and hls.js keeps one fragment load outstanding per track: a request held
@@ -601,6 +602,40 @@ export class ViewerRequests {
601
602
  return true;
602
603
  }
603
604
 
605
+ #beginSeekWait(output, consumerId, generation) {
606
+ const file = output?.file;
607
+ if (!Number.isSafeInteger(generation) || !file || typeof file.sourceKey !== "string" ||
608
+ !Number.isSafeInteger(file.fileIndex) || file.fileIndex < 0) return;
609
+ const waitId = `${file.sourceKey}:${file.fileIndex}:${generation}`;
610
+ const key = `${consumerId}:${waitId}`;
611
+ const fileName = file.relativePath ?? file.name ?? "";
612
+ const extension = typeof fileName === "string" ? fileName.split(".").at(-1)?.toLowerCase() ?? "" : "";
613
+ const viewer = this.#host.viewers.get(consumerId);
614
+ const link = linkMeasurementOf(viewer);
615
+ const features = {
616
+ extension,
617
+ fileLength: Number.isFinite(file.length) && file.length > 0 ? file.length : null,
618
+ downloadSpeed: null,
619
+ linkMbps: link.mbps,
620
+ linkSampleAgeMs: link.ageMs,
621
+ preparedOutputBytes: this.#preparedOutputBytes(file.sourceKey, file.fileIndex),
622
+ fileAvailable: null,
623
+ headerRemaining: null,
624
+ peers: null,
625
+ videoOperation: null,
626
+ audioOperation: null,
627
+ videoSpeedX: null,
628
+ audioSpeedX: null
629
+ };
630
+ // A seek is a new waiting action even when a caller repeats the same
631
+ // generation. Start its clock at the explicit seek request, before the
632
+ // next readiness poll can return the first current-work estimate.
633
+ this.#earlyWaits.delete(key);
634
+ const context = this.#setEarlyWait({ consumerId, generation, sourceKey: file.sourceKey,
635
+ fileIndex: file.fileIndex, waitId, features, revision: 0, current: null, waitElapsedMs: 0 });
636
+ this.#recordEarlyWaitSnapshot(context, features);
637
+ }
638
+
604
639
  /**
605
640
  * Whether this viewer still takes a request made in the generation it states.
606
641
  *