@torrent-tv/proxy 2.98.18 → 2.98.19

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.98.18",
3
+ "version": "2.98.19",
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
@@ -65,6 +65,7 @@ import { DownloadMaps } from "./services/viewer/DownloadMaps.js";
65
65
  import { nativeSourceMap, nativeOriginalSourceMap } from "./services/viewer/NativeSourceMap.js";
66
66
  import { MediaReadRequests } from "./services/media/MediaReadRequests.js";
67
67
  import { SegmentInputs } from "./services/media/SegmentInputs.js";
68
+ import { directSegmentInput } from "./services/media/direct-segment-input.js";
68
69
  import { ContainerTrack } from "./services/media/tracks/ContainerTrack.js";
69
70
  import { pauseCoefficient, viewerStartsOn } from "./services/viewer/PriorityMap.js";
70
71
  import { coalescing } from "./utils/coalesce.js";
@@ -551,6 +552,17 @@ export async function startProxyServer({
551
552
  `${Math.round(passCosts.ms)}ms from start to result in all, longest ${Math.round(passCosts.maxMs)}ms`);
552
553
  Object.assign(passCosts, { since: now, passes: 0, whole: 0, reworked: 0, resolved: 0, windows: 0, ms: 0, maxMs: 0 });
553
554
  };
555
+ // A container whose packet table a pass did not wait for builds the map
556
+ // again once, when the table is built or refused (`directSegmentInput`).
557
+ const refreshAfterTable = new WeakSet();
558
+ const refreshWhenBuilt = (container, built, { sourceKey, fileIndex }) => {
559
+ if (refreshAfterTable.has(container)) return;
560
+ refreshAfterTable.add(container);
561
+ void built.then(() => {
562
+ refreshAfterTable.delete(container);
563
+ return downloadMaps.refresh(sourceKey, fileIndex);
564
+ }).catch(error => logger.warn(`download map after a packet table: ${error?.message ?? error}`));
565
+ };
554
566
  const downloadMaps = new DownloadMaps({
555
567
  // The ranges a picture reads at open are named again by every segment;
556
568
  // only the zone the swarm would rank first is sent (torrent-tv/meta#166).
@@ -693,13 +705,15 @@ export async function startProxyServer({
693
705
  // packets, which the same index states (torrent-tv/meta#166).
694
706
  let value = null;
695
707
  try {
696
- if (await direct.supportsOriginalSourceRanges(interval) === true) value = await direct.readSourceRanges(interval);
697
- else {
698
- const index = await direct.readPacketIndex(interval);
699
- await index.prepareAudioDependencies?.(interval, direct.readRange);
700
- value = new SegmentInputs({ index, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
701
- }
708
+ value = await directSegmentInput(direct, { interval, tracks: wanted, mode: track => modes.get(track) },
709
+ built => refreshWhenBuilt(direct, built, map));
702
710
  } catch { value = null; }
711
+ // A table still being built holds this output's segments, and only
712
+ // this output's: the map is built again when it is.
713
+ if (value?.kind === "pending") {
714
+ waiting.add(zone.outputKey);
715
+ continue;
716
+ }
703
717
  // The pass shares the thread in equal turns (`takeTurns`): in one
704
718
  // stretch the first pass over a film's 4000 zones held the main thread
705
719
  // for all of its work, and handed back after every zone it took
@@ -199,6 +199,20 @@ export class AviContainer extends Container {
199
199
  return false;
200
200
  }
201
201
 
202
+ /**
203
+ * Whether reading the sound's packet table now would wait for it to be
204
+ * built: the index is read, the table is not, and no refusal of memory for
205
+ * it stands. A reader with other work may go on with it instead. Field r42
206
+ * (Home Assistant, 2026-10-14): the download map waited 3.2 s for the
207
+ * table, between the soundtrack's first segment and the picture's next
208
+ * ones, and the swarm was asked for the picture's next piece only after it
209
+ * (torrent-tv/meta#178).
210
+ */
211
+ packetTableWouldWait() {
212
+ if (!this.#index || this.#packets || this.#scan) return false;
213
+ return this.#soundPacketsRefused === null || this.packetMemory?.fits?.(this.#soundPacketsRefused) !== false;
214
+ }
215
+
202
216
  /** The index, read from the file; see `#readIndex`. */
203
217
  async #readIndexOnce() {
204
218
  const tracks = await this.readTracks();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @file The bytes one segment reads, from an index its container already
3
+ * holds, for the download map's pass (torrent-tv/meta#178).
4
+ *
5
+ * The pass works out every segment of every output a viewer watches, one at a
6
+ * time. A segment read from the original file is answered by arithmetic over
7
+ * the index; a soundtrack segment of an AVI needs the sound's packet table,
8
+ * which is built once for the whole film. While that table was being built the
9
+ * pass waited for it, and the picture's segments after the soundtrack's first
10
+ * were not stated to the swarm: field r42 (Home Assistant, 2026-10-14), the
11
+ * picture's next piece was asked for 4.0 s after its input asked for it, 3.2 s
12
+ * of that behind the table.
13
+ */
14
+ import { SegmentInputs } from "./SegmentInputs.js";
15
+
16
+ /**
17
+ * @param {object} container - Has `supportsOriginalSourceRanges`, `readSourceRanges` and `readPacketIndex`.
18
+ * @param {{ interval: object, tracks: object[], mode: (track: object) => string }} segment
19
+ * @param {(built: Promise<void>) => void} whenBuilt - Given a promise that settles when a table
20
+ * still being built is built or refused, when the answer is "pending".
21
+ * @returns {Promise<{ kind: string, ranges?: Array<[number, number]> }>} `pending` when the
22
+ * answer waits for a table still being built.
23
+ */
24
+ export async function directSegmentInput(container, { interval, tracks, mode }, whenBuilt) {
25
+ if (await container.supportsOriginalSourceRanges(interval) === true) return container.readSourceRanges(interval);
26
+ // The table is begun, or joined, and not waited for.
27
+ if (container.packetTableWouldWait?.() === true) {
28
+ whenBuilt(container.readPacketIndex(interval).then(() => undefined, () => undefined));
29
+ return { kind: "pending" };
30
+ }
31
+ const index = await container.readPacketIndex(interval);
32
+ await index.prepareAudioDependencies?.(interval, container.readRange);
33
+ return new SegmentInputs({ index, tracks }).forInterval({ ...interval, mode });
34
+ }