@torrent-tv/proxy 2.98.10 → 2.98.11

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.10",
3
+ "version": "2.98.11",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -28,6 +28,8 @@ export class EncodeInputs {
28
28
  /** Whether an output's last resolved input was its original file. @type {WeakMap<object, boolean>} */
29
29
  #original = new WeakMap();
30
30
  #revision = 0;
31
+ /** Output → how long its last input took to be ready, ms. @type {WeakMap<object, number>} */
32
+ #preparedIn = new WeakMap();
31
33
  #memoryRevision = 0;
32
34
  #held = 0;
33
35
  #allowed = 0;
@@ -270,6 +272,18 @@ export class EncodeInputs {
270
272
  * @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> } | null} [arrived] -
271
273
  * The bytes that arrived, inclusive, when the event names them.
272
274
  */
275
+ /**
276
+ * How long this output's last input took from being asked for to being
277
+ * ready, in milliseconds, or null before one was: the time a run gives the
278
+ * input after it to be prepared in (`EncodeRuns`).
279
+ *
280
+ * @param {object} output
281
+ * @returns {number | null}
282
+ */
283
+ preparationMsOf(output) {
284
+ return this.#preparedIn.get(output) ?? null;
285
+ }
286
+
273
287
  bytesChanged(arrived = null) {
274
288
  this.#revision++;
275
289
  for (const request of this.#requests.values()) {
@@ -376,6 +390,7 @@ export class EncodeInputs {
376
390
  // is commanded over its prefix.
377
391
  if (result.kind === "result" && result.to > request.to) result.to = Math.max(request.from, request.to);
378
392
  request.result = result;
393
+ if (result.kind === "result") this.#preparedIn.set(request.output, this.#now() - request.timing.askedAt);
379
394
  this.#note(request, result.kind === "result" ? `ready #${result.from}..#${result.to}, ${result.bytes ?? "?"} bytes` +
380
395
  (Number.isFinite(result.copyMs) ? (result.deferredBytes > 0
381
396
  ? `, opening part copied in ${result.copyMs}ms, ${result.deferredBytes} bytes of the stretch copied while the run opens`
@@ -12,6 +12,7 @@ import { spawn } from "node:child_process";
12
12
  import { contentionPenalty } from "./contention.js";
13
13
  import { LOOKAHEAD_PAUSE_SECONDS } from "./CushionReport.js";
14
14
  import { SpeedReadings } from "./SpeedReadings.js";
15
+ import { nextInputAtFirstPiece } from "./next-input.js";
15
16
  import { ENCODE_RUN_STATE, liveRunsOf, runStateOf, wireState } from "./encode-run-state.js";
16
17
  import { ENCODE_EXIT } from "./encode-exit.js";
17
18
  import { EncodeRun, endOfRun } from "./EncodeRun.js";
@@ -1096,6 +1097,8 @@ export class EncodeRuns {
1096
1097
  // killed mid-piece leaving a partial file — is not a correctness problem
1097
1098
  // either, because the store serves a segment only once its closure is
1098
1099
  // proven, and it is cleared up when the run ends.
1100
+ // Told when this run closes its first piece; see the next stretch below.
1101
+ let atFirstPiece = null;
1099
1102
  const run = new EncodeRun({
1100
1103
  address: session.outputKey ?? session.id,
1101
1104
  encoder: this.#host.videoEncoder,
@@ -1129,7 +1132,12 @@ export class EncodeRuns {
1129
1132
  onProgress: (report) => this.#noteRunProgress(session, run, report),
1130
1133
  // A closed piece moves this encoder forward, perhaps out of the band that
1131
1134
  // led; who has the processor is decided again at once.
1132
- onProduced: () => this.#host.encodeOrchestrator?.shareTheMachine?.(),
1135
+ onProduced: () => {
1136
+ this.#host.encodeOrchestrator?.shareTheMachine?.();
1137
+ const told = atFirstPiece;
1138
+ atFirstPiece = null;
1139
+ told?.();
1140
+ },
1133
1141
  indexOfName: (name) => session.segmentFormat.segmentIndexFromName(
1134
1142
  session.segmentFormat.servedNameOf?.(name) ?? name),
1135
1143
  // Why this encoder exists, recorded with its argument list. It used to be
@@ -1231,12 +1239,24 @@ export class EncodeRuns {
1231
1239
  // It waits for this run's own stretch to be copied: that copy goes on
1232
1240
  // while the run opens and FFmpeg's reads wait for it, and a second copy
1233
1241
  // beside it on the same thread would slow it (torrent-tv/meta#178).
1242
+ //
1243
+ // And for this run's first piece when the rest of the run leaves the
1244
+ // time to prepare it in (`next-input.js`).
1234
1245
  if (inputEnd < mayReach) {
1235
1246
  const askNext = () => {
1236
1247
  if (this.isLive(session) && run.isAlive) this.#host.encodeInputs?.take(session, inputEnd + 1, mayReach);
1237
1248
  };
1238
- if (admittedInput.whole) void admittedInput.whole.then(whole => { if (whole) askNext(); });
1239
- else askNext();
1249
+ const preparedInMs = this.#host.encodeInputs?.preparationMsOf?.(session) ?? null;
1250
+ const speed = Number(this.#host.encodeCost?.speedForOutput?.(session.outputKey));
1251
+ const timeline = session.timeline;
1252
+ const restSeconds = Number(timeline?.publishedStartOf?.(inputEnd + 1)) - Number(timeline?.publishedStartOf?.(safeIndex + 1));
1253
+ const atOnce = !nextInputAtFirstPiece({ preparedInMs, speed, restSeconds });
1254
+ let copied = false, produced = atOnce;
1255
+ const askWhenBoth = () => { if (copied && produced) askNext(); };
1256
+ if (!atOnce) atFirstPiece = () => { produced = true; askWhenBoth(); };
1257
+ const whenCopied = () => { copied = true; askWhenBoth(); };
1258
+ if (admittedInput.whole) void admittedInput.whole.then(whole => { if (whole) whenCopied(); });
1259
+ else whenCopied();
1240
1260
  }
1241
1261
  } else if (admittedInput) {
1242
1262
  run.admittedInputKeys = new Map([[safeIndex, inputKey]]);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @file When a run asks for the input of the run after it (torrent-tv/meta#178).
3
+ *
4
+ * Asked as soon as the run's own stretch was copied, the next input was copied
5
+ * while the run opened: a background run on Home Assistant started with its
6
+ * 108.7 MB, the next 115.6 MB were copied at once, the proxy took 1.5-2 cores,
7
+ * and the share left to encoders fell from 65 % to 10-16 % before either had
8
+ * a piece (r37). Asked at the run's first piece, it still has the rest of the
9
+ * run to be ready in — unless preparing an input of this output was measured
10
+ * to take longer than that rest at this output's speed, or the run is one
11
+ * piece, whose first piece is its last.
12
+ */
13
+
14
+ /**
15
+ * Whether the next input waits for this run's first piece.
16
+ *
17
+ * @param {object} params
18
+ * @param {number | null} params.preparedInMs - How long this output's last input took from being asked for to being ready.
19
+ * @param {number} params.speed - This output's processing speed, film seconds per second; 0 when not known.
20
+ * @param {number} params.restSeconds - The film this run makes after its first piece.
21
+ * @returns {boolean}
22
+ */
23
+ export function nextInputAtFirstPiece({ preparedInMs, speed, restSeconds }) {
24
+ if (!Number.isFinite(preparedInMs) || !(speed > 0) || !(restSeconds > 0)) return false;
25
+ return preparedInMs < restSeconds / speed * 1000;
26
+ }