@torrent-tv/proxy 2.97.20 → 2.97.21

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.97.20",
3
+ "version": "2.97.21",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -281,6 +281,21 @@ export class EncodeRuns {
281
281
  return this.#runsByInputToken.get(runToken)?.originalInput ?? null;
282
282
  }
283
283
 
284
+ /**
285
+ * What FFmpeg's caches may hold for the live runs that read an original
286
+ * file: at most what each was admitted, since a cache keeps what its run
287
+ * read of it (`source-command.js`).
288
+ *
289
+ * @returns {number}
290
+ */
291
+ inputCacheBytes() {
292
+ let bytes = 0;
293
+ for (const run of this.#runsByInputToken.values()) {
294
+ if (run.isAlive && run.originalInput) bytes += Math.max(0, Number(run.originalInput.bytes) || 0);
295
+ }
296
+ return bytes;
297
+ }
298
+
284
299
  /**
285
300
  * @param {object} output
286
301
  * @returns {number}
@@ -270,7 +270,15 @@ export function buildOriginalCommand(params) {
270
270
  const primary = sourceFor(audioOnly ? "audio" : "video");
271
271
  const audio = sourceFor("audio") ?? primary;
272
272
  if (!primary) throw new Error("The selected source track is absent from original input.");
273
- const url = source => new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href;
273
+ // READ THROUGH FFMPEG'S CACHE, so a return to bytes already read is not a
274
+ // new request. An OpenDML AVI's open seeks from its super index in the header
275
+ // to each standard index and back: on the field's two-hour AVI, 195 indexes
276
+ // made 390 requests and 636-1243 MB of answers, and the first piece came 4.8 s
277
+ // after the start against 2.8 s through the cache, which made 3 requests;
278
+ // the pieces were the same to the byte, on FFmpeg 6.1 and 8.1.2 (Home
279
+ // Assistant 2026-10-13, torrent-tv/meta#178). The cache's file holds what
280
+ // FFmpeg read and is counted by the budget (`EncodeRuns.inputCacheBytes`).
281
+ const url = source => `cache:${new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href}`;
274
282
  // The container named the bytes FFmpeg's stream search reads for this much
275
283
  // media time; the search is told the same, whatever its own default.
276
284
  const openArgs = source => Number.isFinite(source.input.streamSearchSeconds)
@@ -93,6 +93,7 @@ import { audioReadingFor } from "../encode/audio-work-rate.js";
93
93
  import { wireMachineBudget } from "../storage/wire.js";
94
94
  import { Returns } from "../storage/returns.js";
95
95
  import { freeBytesFor } from "../storage/free.js";
96
+ import { existsSync } from "node:fs";
96
97
  import path from "node:path";
97
98
  import { contentOf, LocalObservations, PROXY_ROOT } from "../encode/LocalObservations.js";
98
99
  import { configurationKeyOf } from "../encode/fingerprint.js";
@@ -345,7 +346,7 @@ export function wireOutputs({
345
346
  get encoders() { return parts.encoders; },
346
347
  invalidateWaits: (output) => parts.serving.invalidateWaits(output),
347
348
  productionFailed: (output) => parts.renditions.noteProductionFailed(output),
348
- encodersChanged: () => { parts.applyDownloadShare?.(); onEncodersChanged?.(); },
349
+ encodersChanged: () => { parts.applyDownloadShare?.(); reviseInputCaches(); onEncodersChanged?.(); },
349
350
  });
350
351
  // How much film is ready in front of the viewers, said; and the spare soundtracks fetched once it is full.
351
352
  parts.cushion = new CushionReport({
@@ -1044,10 +1045,25 @@ export function wireOutputs({
1044
1045
  memory: parts.memoryClaimant,
1045
1046
  encodeInputs: parts.encodeInputs,
1046
1047
  indexMemory,
1048
+ // FFmpeg puts its temporary files in /tmp, or where it runs when there is
1049
+ // none — an output directory of the segments.
1050
+ inputCaches: { held: () => parts.encodeRuns?.inputCacheBytes() ?? 0, root: existsSync("/tmp") ? "/tmp" : null },
1047
1051
  policy: parts.budgetPolicy ?? undefined,
1048
1052
  readFree: freeBytesFor,
1049
1053
  logger
1050
1054
  });
1055
+ /**
1056
+ * A run reading an original file started or ended, so the disk its cache is
1057
+ * on is divided again — only when what the caches may hold has changed.
1058
+ */
1059
+ let inputCachesDivided = 0;
1060
+ function reviseInputCaches() {
1061
+ const bytes = parts.encodeRuns?.inputCacheBytes() ?? 0;
1062
+ const on = parts.machineBudget?.inputCachesOn;
1063
+ if (!on || bytes === inputCachesDivided) return;
1064
+ inputCachesDivided = bytes;
1065
+ void parts.machineBudget.revise(on, "encoder input caches").catch(error => logger.warn(`input cache budget: ${error.message}`));
1066
+ }
1051
1067
  /**
1052
1068
  * What starting a run costs this output before its first piece is encoded:
1053
1069
  * its own measured open where it has one (`RunCosts.openOf`), else the
@@ -44,6 +44,9 @@ import { availableMemoryBytes } from "./machine-memory.js";
44
44
  * not fit in memory is spilled, so this share decides how much disk is needed.
45
45
  * @param {{ held: () => number, allow: (bytes: number) => unknown, root?: string }} [params.wholeFiles] -
46
46
  * Files downloaded whole and kept as files, on the torrent thread as well.
47
+ * @param {{ held: () => number, root?: string | null }} [params.inputCaches] -
48
+ * What FFmpeg's caches of original input hold, and the directory FFmpeg puts
49
+ * them in (the segments' disk when none).
47
50
  * @param {import("./Diagnostics.js").Diagnostics} [params.diagnostics] - The
48
51
  * evidence. Built where the collectors are, so one object is both registered
49
52
  * here and consulted by them.
@@ -64,6 +67,7 @@ export function wireMachineBudget({
64
67
  encodeInputs,
65
68
  indexMemory,
66
69
  wholeFiles,
70
+ inputCaches,
67
71
  diagnostics,
68
72
  diagnosticsRoot = "",
69
73
  readFree,
@@ -153,6 +157,25 @@ export function wireMachineBudget({
153
157
  });
154
158
  }
155
159
 
160
+ // FFMPEG'S CACHE OF WHAT A RUN READ from an original file (`cache:`,
161
+ // `encode/source-command.js`): a temporary file where FFmpeg puts its own,
162
+ // as large as what the run has read and at most what it was admitted. Held
163
+ // while the run lives and given back by nobody else, so it is required in
164
+ // full and the rest of its disk is divided after it.
165
+ let inputCachesOn = null;
166
+ if (typeof inputCaches?.held === "function") {
167
+ inputCachesOn = inputCaches.root ? deviceOf(inputCaches.root) : segmentsOn;
168
+ defineDisk(inputCachesOn, inputCaches.root || segmentStore.root);
169
+ budget.register({
170
+ name: "encoder input caches",
171
+ resource: inputCachesOn,
172
+ held: () => inputCaches.held(),
173
+ wanted: () => inputCaches.held(),
174
+ required: () => inputCaches.held(),
175
+ allow: () => {}
176
+ });
177
+ }
178
+
156
179
  let evidenceOn = null;
157
180
  if (diagnostics) {
158
181
  evidenceOn = diagnosticsRoot ? deviceOf(diagnosticsRoot) : segmentsOn;
@@ -188,6 +211,8 @@ export function wireMachineBudget({
188
211
  return budget.revise(resource, because);
189
212
  },
190
213
  segmentBytes: () => segmentBytes,
214
+ /** The disk FFmpeg's input caches are counted on, or null when none are. */
215
+ inputCachesOn,
191
216
  diagnostics: diagnostics ?? null,
192
217
  describe: () => budget.describe()
193
218
  };