@torrent-tv/proxy 2.97.23 → 2.97.24
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
|
@@ -291,7 +291,7 @@ export class EncodeRuns {
|
|
|
291
291
|
inputCacheBytes() {
|
|
292
292
|
let bytes = 0;
|
|
293
293
|
for (const run of this.#runsByInputToken.values()) {
|
|
294
|
-
if (run.isAlive && run.originalInput) bytes += Math.max(0, Number(run.originalInput.bytes) || 0);
|
|
294
|
+
if (run.isAlive && run.originalInput?.readThroughCache) bytes += Math.max(0, Number(run.originalInput.bytes) || 0);
|
|
295
295
|
}
|
|
296
296
|
return bytes;
|
|
297
297
|
}
|
|
@@ -1024,6 +1024,9 @@ export class EncodeRuns {
|
|
|
1024
1024
|
this.#lastInputToken += 1;
|
|
1025
1025
|
const inputToken = this.#lastInputToken;
|
|
1026
1026
|
if (admittedInput) admittedInput.runTag = `${startIndex}r${inputToken}`;
|
|
1027
|
+
// Whether this run's FFmpeg keeps what it reads in its cache's file, which
|
|
1028
|
+
// is what the budget counts on that file's disk.
|
|
1029
|
+
if (admittedInput?.original) admittedInput.readThroughCache = this.#host.readsThroughCache?.() === true;
|
|
1027
1030
|
const buildCommand = admittedInput.original ? buildOriginalCommand : buildAdmittedCommand;
|
|
1028
1031
|
const built = buildCommand({
|
|
1029
1032
|
admittedInput,
|
|
@@ -1044,7 +1047,8 @@ export class EncodeRuns {
|
|
|
1044
1047
|
endIndex: admittedInput.original ? inputEnd
|
|
1045
1048
|
: Number.isInteger(ordered?.to) ? ordered.to : runEnd >= startIndex ? runEnd : session.timeline.segmentCount - 1,
|
|
1046
1049
|
videoEncoder: this.#host.videoEncoder,
|
|
1047
|
-
segmentDurationSec: this.#host.segmentDurationSec
|
|
1050
|
+
segmentDurationSec: this.#host.segmentDurationSec,
|
|
1051
|
+
readThroughCache: admittedInput.readThroughCache === true
|
|
1048
1052
|
});
|
|
1049
1053
|
const { args, cutTimes } = built;
|
|
1050
1054
|
// The piece before the start, made only to have its group in hand, is
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether this machine's FFmpeg can read an encoder's input through its
|
|
3
|
+
* `cache:` protocol, and on which directory's disk that cache lies.
|
|
4
|
+
*
|
|
5
|
+
* FFmpeg's cache (libavformat/cache.c) keeps what it has read in a temporary
|
|
6
|
+
* file opened by `avpriv_tempfile`, and a Linux build can only open that file
|
|
7
|
+
* in /tmp (libavutil/file_open.c n8.1, `mkstemp`); a Windows or Android build
|
|
8
|
+
* opens it where FFmpeg runs. Where no /tmp can be written, every run reading
|
|
9
|
+
* through the cache fails at its open — checked on Home Assistant with a
|
|
10
|
+
* read-only root: "ff_tempfile: Cannot open temporary file
|
|
11
|
+
* /tmp/ffcacheXXXXXX", while the same file read directly opened. So one small
|
|
12
|
+
* read through the cache is tried at startup with this proxy's own FFmpeg, and
|
|
13
|
+
* until it has succeeded an input is read directly (torrent-tv/meta#178).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where FFmpeg keeps its cache's file: /tmp on a build that uses `mkstemp`,
|
|
24
|
+
* else where FFmpeg runs, which for a run is a directory of the segments.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} [platform]
|
|
27
|
+
* @returns {string | null} null for the segments' own directory.
|
|
28
|
+
*/
|
|
29
|
+
export function inputCacheRoot(platform = process.platform) {
|
|
30
|
+
return platform === "win32" || platform === "android" ? null : "/tmp";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read a small file through FFmpeg's cache once.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} params
|
|
37
|
+
* @param {string} params.ffmpegBin
|
|
38
|
+
* @param {typeof nodeSpawn} [params.spawn]
|
|
39
|
+
* @param {string} [params.scratch] - Where the file read is written.
|
|
40
|
+
* @returns {Promise<{ available: boolean, reason: string }>}
|
|
41
|
+
*/
|
|
42
|
+
export async function probeInputCache({ ffmpegBin, spawn = nodeSpawn, scratch = os.tmpdir() }) {
|
|
43
|
+
let directory = null;
|
|
44
|
+
try {
|
|
45
|
+
directory = await mkdtemp(path.join(scratch, "ttv-input-cache-"));
|
|
46
|
+
const file = path.join(directory, "input.bin");
|
|
47
|
+
await writeFile(file, randomBytes(64 * 1024));
|
|
48
|
+
const { code, error } = await new Promise((resolve) => {
|
|
49
|
+
let stderr = "";
|
|
50
|
+
const child = spawn(ffmpegBin, ["-v", "error", "-nostdin", "-f", "data", "-i", `cache:file:${file}`,
|
|
51
|
+
"-map", "0", "-c", "copy", "-f", "null", "-"], { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
52
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
53
|
+
child.once("error", (failure) => resolve({ code: null, error: failure.message }));
|
|
54
|
+
child.once("close", (exitCode) => resolve({ code: exitCode, error: stderr.trim().split(/\r?\n/).pop() ?? "" }));
|
|
55
|
+
});
|
|
56
|
+
return code === 0 ? { available: true, reason: "a read through it completed" }
|
|
57
|
+
: { available: false, reason: `FFmpeg ${code === null ? "did not start" : `exited ${code}`}: ${error || "no message"}` };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return { available: false, reason: `the file to read could not be prepared: ${error instanceof Error ? error.message : String(error)}` };
|
|
60
|
+
} finally {
|
|
61
|
+
if (directory) await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -278,7 +278,10 @@ export function buildOriginalCommand(params) {
|
|
|
278
278
|
// the pieces were the same to the byte, on FFmpeg 6.1 and 8.1.2 (Home
|
|
279
279
|
// Assistant 2026-10-13, torrent-tv/meta#178). The cache's file holds what
|
|
280
280
|
// FFmpeg read and is counted by the budget (`EncodeRuns.inputCacheBytes`).
|
|
281
|
-
|
|
281
|
+
// Only where a read through it has worked on this machine
|
|
282
|
+
// (`encode/input-cache.js`); elsewhere the input is read directly.
|
|
283
|
+
const readThroughCache = params.readThroughCache !== false;
|
|
284
|
+
const url = source => `${readThroughCache ? "cache:" : ""}${new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href}`;
|
|
282
285
|
// The container named the bytes FFmpeg's stream search reads for this much
|
|
283
286
|
// media time; the search is told the same, whatever its own default.
|
|
284
287
|
const openArgs = source => Number.isFinite(source.input.streamSearchSeconds)
|
|
@@ -93,7 +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 {
|
|
96
|
+
import { inputCacheRoot, probeInputCache } from "../encode/input-cache.js";
|
|
97
97
|
import path from "node:path";
|
|
98
98
|
import { contentOf, LocalObservations, PROXY_ROOT } from "../encode/LocalObservations.js";
|
|
99
99
|
import { configurationKeyOf } from "../encode/fingerprint.js";
|
|
@@ -341,6 +341,7 @@ export function wireOutputs({
|
|
|
341
341
|
get outputs() { return parts.outputs; },
|
|
342
342
|
get priority() { return parts.priority; },
|
|
343
343
|
get segmentDurationSec() { return parts.segmentDurationSec; },
|
|
344
|
+
readsThroughCache: () => parts.inputCacheAvailable === true,
|
|
344
345
|
segmentFiles,
|
|
345
346
|
get videoEncoder() { return parts.encoders.current; },
|
|
346
347
|
get encoders() { return parts.encoders; },
|
|
@@ -627,6 +628,16 @@ export function wireOutputs({
|
|
|
627
628
|
parts.coldStarts = new ColdStarts();
|
|
628
629
|
parts.enabled = Boolean(enabled);
|
|
629
630
|
parts.ffmpegBin = ffmpegBin;
|
|
631
|
+
// Whether a run may read its input through FFmpeg's cache: once a read
|
|
632
|
+
// through it has worked with this FFmpeg, and directly until then
|
|
633
|
+
// (`encode/input-cache.js`).
|
|
634
|
+
parts.inputCacheAvailable = false;
|
|
635
|
+
if (parts.enabled && ffmpegBin) {
|
|
636
|
+
void probeInputCache({ ffmpegBin }).then(({ available, reason }) => {
|
|
637
|
+
parts.inputCacheAvailable = available;
|
|
638
|
+
logger.info(`encoder input: ${available ? "read through FFmpeg's cache" : "read directly"} — ${reason}`);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
630
641
|
parts.sourceInputsFor = typeof sourceInputsFor === "function" ? sourceInputsFor : null;
|
|
631
642
|
parts.keyframeTables = keyframeTables;
|
|
632
643
|
// Where measurements about this host are kept between runs. Empty means
|
|
@@ -1045,9 +1056,9 @@ export function wireOutputs({
|
|
|
1045
1056
|
memory: parts.memoryClaimant,
|
|
1046
1057
|
encodeInputs: parts.encodeInputs,
|
|
1047
1058
|
indexMemory,
|
|
1048
|
-
// FFmpeg
|
|
1049
|
-
//
|
|
1050
|
-
inputCaches: { held: () => parts.encodeRuns?.inputCacheBytes() ?? 0, root:
|
|
1059
|
+
// FFmpeg's cache keeps its file in /tmp, or where FFmpeg runs on a build
|
|
1060
|
+
// without `mkstemp` — an output directory of the segments.
|
|
1061
|
+
inputCaches: { held: () => parts.encodeRuns?.inputCacheBytes() ?? 0, root: inputCacheRoot() },
|
|
1051
1062
|
policy: parts.budgetPolicy ?? undefined,
|
|
1052
1063
|
readFree: freeBytesFor,
|
|
1053
1064
|
logger
|