@torrent-tv/proxy 2.99.15 → 2.99.17

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.99.15",
3
+ "version": "2.99.17",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -21,6 +21,7 @@ import { computeOutputDimensions } from "./args.js";
21
21
  import { buildAdmittedCommand } from "./admitted-command.js";
22
22
  import { buildOriginalCommand } from "./source-command.js";
23
23
  import { writeAdmittedInput } from "./AdmittedInput.js";
24
+ import { nextPacketInputs } from "./next-packet-input.js";
24
25
  import { cutOf, cutShiftOf, judgeNeighbors, judgePiece, leadingSecondsOf, lacksOnlyItsLeadingPictures } from "./piece-completeness.js";
25
26
  import { leadingPictures, presentationSegment } from "./segment-formats/presentation-segment.js";
26
27
  import { InputFailures, failedAdmittedInput } from "./InputFailures.js";
@@ -1262,23 +1263,23 @@ export class EncodeRuns {
1262
1263
  }
1263
1264
  } else if (admittedInput) {
1264
1265
  run.admittedInputKeys = new Map([[safeIndex, inputKey]]);
1265
- let nextIndex = startIndex + 1;
1266
+ const nextIndex = startIndex + 1;
1266
1267
  const canAppend = index => this.isLive(session) && run.isAlive &&
1267
1268
  this.#host.encodeOrchestrator.wantedSegmentsOn(session.outputKey).some(window => index >= window.from && index <= window.to) &&
1268
1269
  !this.liveRunsOf(session).some(other => other !== run && index >= other.from && index <= other.to) &&
1269
1270
  !this.#host.producedNumbers(session).includes(index);
1271
+ // Only an input already prepared is appended, and the one after it is
1272
+ // begun at once (`next-packet-input.js`).
1273
+ const following = nextPacketInputs({ take: index => this.#host.encodeInputs.take(session, index, index),
1274
+ canAppend, first: nextIndex, last: ordered?.to ?? startIndex });
1270
1275
  void writeAdmittedInput(admittedInput, run.process.stdin, { next: async () => {
1271
- if (nextIndex > (ordered?.to ?? startIndex) || !canAppend(nextIndex)) return null;
1272
- const next = await this.#host.encodeInputs.acquire(session, nextIndex, nextIndex);
1273
- if (!next) return null;
1274
- if (!canAppend(nextIndex)) { next.release(); return null; }
1275
- run.admittedInputKeys.set(nextIndex, this.#inputFailures.key(next, inputParameters));
1276
- run.to = nextIndex++;
1276
+ const next = following.next(index => { run.to = index; });
1277
+ if (next) run.admittedInputKeys.set(run.to, this.#inputFailures.key(next, inputParameters));
1277
1278
  return next;
1278
- }, waiting: waits => { if (waits) run.inputWaitBegins(); else run.inputWaitEnds(); } }).catch(error => {
1279
+ } }).catch(error => {
1279
1280
  this.#host.logger.warn(`encode input output=${session.outputKey} segment=${safeIndex} write failed: ${error.message}`);
1280
1281
  run.process?.stdin?.destroy();
1281
- });
1282
+ }).finally(() => following.release());
1282
1283
  }
1283
1284
  this.#runsByInputToken.set(inputToken, run);
1284
1285
  this.#host.encodersChanged?.();
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @file Which input a run fed packet by packet goes on with.
3
+ *
4
+ * Such a run keeps its input open while more is appended, and FFmpeg closes a
5
+ * piece only once it reads past the piece's end, so a piece is published when
6
+ * the input after it arrives or the input ends. Waited for, the next input
7
+ * held each piece back for as long as preparing it took: on Home Assistant a
8
+ * soundtrack's #7 waited 14 s for #8's attempt, which ended refused memory, and
9
+ * the picture stood at 61.8 s (r54, r55, torrent-tv/meta#178).
10
+ *
11
+ * So a run is given the next input only when it is ready, and the one after it
12
+ * is begun as soon as one is taken: the run goes on while preparation keeps
13
+ * ahead of it, and ends, publishing its last piece, when it does not. The plan
14
+ * starts the next run on the input being prepared.
15
+ */
16
+
17
+ /**
18
+ * A source of the inputs after a run's first.
19
+ *
20
+ * @param {object} params
21
+ * @param {(index: number) => object | null} params.take - The input of one
22
+ * piece if ready, beginning its preparation otherwise (`EncodeInputs.take`).
23
+ * @param {(index: number) => boolean} params.canAppend - Whether the piece may
24
+ * still be made by this run.
25
+ * @param {number} params.first - The first piece after the run's own.
26
+ * @param {number} params.last - The last piece the run may reach.
27
+ * @returns {{ next: (onTaken?: (index: number) => void) => object | null, release: () => void }}
28
+ */
29
+ export function nextPacketInputs({ take, canAppend, first, last }) {
30
+ let index = first;
31
+ let ahead = null;
32
+ const begin = at => {
33
+ if (at <= last && canAppend(at)) ahead = take(at);
34
+ };
35
+ begin(index);
36
+ return {
37
+ next(onTaken) {
38
+ if (index > last || !canAppend(index)) return null;
39
+ const input = ahead ?? take(index);
40
+ ahead = null;
41
+ if (!input) return null;
42
+ if (!canAppend(index)) {
43
+ input.release();
44
+ return null;
45
+ }
46
+ onTaken?.(index);
47
+ index += 1;
48
+ begin(index);
49
+ return input;
50
+ },
51
+ release() {
52
+ ahead?.release();
53
+ ahead = null;
54
+ }
55
+ };
56
+ }
@@ -19,7 +19,6 @@
19
19
  */
20
20
 
21
21
  import { readVint } from "./ebml-reader.js";
22
- import { isUnavailable } from "./unavailable.js";
23
22
 
24
23
  /** An element id is at most four bytes and a size at most eight (RFC 8794 §4). */
25
24
  export const MAX_HEADER_BYTES = 12;
@@ -65,10 +64,6 @@ export class ElementReader {
65
64
  /** @type {number} */
66
65
  #windowAt = 0;
67
66
 
68
- /** Below this position a portion was found missing: read exactly there. */
69
- #exactBefore = -1;
70
- #narrow;
71
-
72
67
  /**
73
68
  * @param {object} params
74
69
  * @param {(start: number, end: number) => Promise<Buffer>} params.read - Answers
@@ -84,13 +79,8 @@ export class ElementReader {
84
79
  * last byte that can be read from `start` in one go. A read of what is
85
80
  * downloaded must not run a prefetch past the end of the downloaded run,
86
81
  * or a cluster that is whole would fail for the bytes after it.
87
- * @param {boolean} [params.narrowWhenMissing] - When a prefetched portion is
88
- * not all downloaded, read only the bytes asked for, and read exactly up to
89
- * the end of that portion: what is not downloaded yet is then only what is
90
- * needed, and the bytes before it are read.
91
82
  */
92
- constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil, narrowWhenMissing = false }) {
93
- this.#narrow = narrowWhenMissing;
83
+ constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil }) {
94
84
  this.#read = read;
95
85
  this.#fileSize = fileSize;
96
86
  this.#readableUntil = typeof readableUntil === "function" ? readableUntil : () => fileSize;
@@ -120,15 +110,8 @@ export class ElementReader {
120
110
  return window.subarray(start - this.#windowAt, end - this.#windowAt);
121
111
  }
122
112
  const reach = Math.min(this.#fileSize, Math.max(end, this.#readableUntil(start)));
123
- const span = this.#prefetch && start >= this.#exactBefore ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
124
- let got;
125
- try {
126
- got = await this.#read(start, start + span - 1);
127
- } catch (error) {
128
- if (!this.#narrow || span === end - start || !isUnavailable(error)) throw error;
129
- this.#exactBefore = start + span;
130
- got = await this.#read(start, end - 1);
131
- }
113
+ const span = this.#prefetch ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
114
+ const got = await this.#read(start, start + span - 1);
132
115
  this.#window = got;
133
116
  this.#windowAt = start;
134
117
  return got.subarray(0, end - start);
@@ -48,10 +48,13 @@ export async function walkMatroskaPackets({ readRange, fileSize, portionBytes, l
48
48
  // header at a time, each a read of a few bytes from the torrent thread, the
49
49
  // soundtrack's second piece of a copied 1080p film took 29.6 s on Home
50
50
  // Assistant while that thread was downloading (r52, torrent-tv/meta#178).
51
- // Between clusters only their headers are read; where a portion is not all
52
- // downloaded, only the bytes a header needs are read and asked for.
51
+ // A portion not all downloaded is asked for whole and the walk goes on from
52
+ // the same block once it is: read on header by header up to the gap, a walk
53
+ // held the file's reads 50 s and the picture's input waited behind it
54
+ // (r54). The headers lie throughout the portion, so it is all needed.
55
+ // Between clusters only their headers are read.
53
56
  let readableUntil = null;
54
- const reader = new ElementReader({ read: readRange, fileSize, portionBytes, prefetch: true, narrowWhenMissing: true,
57
+ const reader = new ElementReader({ read: readRange, fileSize, portionBytes, prefetch: true,
55
58
  readableUntil: start => readableUntil ?? start });
56
59
  const declared = recordedTracks(tracks, trackIds);
57
60
  if (!state.packets) {
@@ -944,6 +944,25 @@ export class ViewerRequests {
944
944
  }
945
945
  // Totals that went back belong to a torrent added again: read on from them.
946
946
  if (limit) source.limit = limit;
947
- return source.free.snapshot() ?? source.delivered.snapshot();
947
+ return sourceService(source.free.snapshot(), source.delivered.snapshot());
948
948
  }
949
949
  }
950
+
951
+ /**
952
+ * The download service a source's forecast runs on: what the swarm carried
953
+ * while our selection held none of its connections back, but never less than
954
+ * what it carried in all. A stretch our selection held nothing back can be a
955
+ * moment with nothing to ask, and its rate then measures our asking: after a
956
+ * seek to 1000 s the forecast read 0 B/s from such a stretch while the torrent
957
+ * downloaded 5-18 MB/s, answered "service not advancing", and held the viewer
958
+ * (r54, torrent-tv/meta#178). What the swarm did carry, it can carry.
959
+ *
960
+ * @param {object | null} free - A rate summary (`RateTrend.snapshot`).
961
+ * @param {object | null} delivered
962
+ * @returns {object | null}
963
+ */
964
+ export function sourceService(free, delivered) {
965
+ if (!free) return delivered;
966
+ if (!delivered) return free;
967
+ return delivered.lastValue > free.lastValue ? delivered : free;
968
+ }