@torrent-tv/proxy 2.16.0 → 2.17.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2.17.0
2
+
3
+ - **New**: The encoder is benchmarked on real footage instead of a generated test pattern, and measured by ffmpeg's own progress rather than by the clock around the process. The pattern has flat areas and no grain and encodes **1.23x** cheaper than film on the same machine and preset — an error that always points at offering a rung the host cannot hold. Timing whole runs was the second error: process startup is ~0.4 s, which put `fast` and `ultrafast` within 1.24x of each other when they differ by three times. The clip is decoded once to raw frames in a temp file (feeding them through a pipe measured the pipe: the fastest presets want hundreds of megabytes a second), each preset is read from the slope between two progress reports, and the run is stopped as soon as a second of it has been covered.
4
+ - **Fix**: A preset that ends before its window is covered is still measured, but never over a window of no width — two reports a millisecond apart would have called a host twenty times faster than it is, and one such reading is what every ladder decision is then taken from. A position ffmpeg reports as the smallest signed 64-bit integer (some builds print that instead of `N/A` before the first packet) is discarded, and a slope above a thousand times realtime is treated as a fault rather than as a fast machine.
5
+ - **Fix**: Which rungs may be offered is decided from the CHEAPEST preset's throughput, not from the largest reading in the array. Measurements scatter on a busy machine — `faster` read below `fast` twice on 2026-08-15 — and taking the maximum let one noisy reading of an expensive preset raise the bar that decides what is offered. Choosing a preset still scans every entry rather than stopping at the first miss, because there the direction of that error costs picture quality, not playback.
6
+ - **Fix**: A host with nothing measured says so in those words — `the quality ladder is UNFILTERED on this host` — because that is what an empty benchmark means, and the previous wording said only that presets were unmeasured. The benchmark also can no longer stop the proxy from starting: a missing or read-only temp directory, or a locked file after a kill, is a host left unmeasured, not a process that fails to listen.
7
+ - **Fix**: The host-load line counts CPU per PROCESS across readings, and only for processes present in both. A seek kills ffmpeg and starts another whose counter begins at zero, so subtracting one total from another printed shares like `-598%`; and on a host without `/proc` the sum of no readings was reported as a confident `0%` beside honest `n/a`s.
8
+
1
9
  ## 2.16.0
2
10
 
3
11
  - **New**: While an encoder runs, one line every five seconds says what the MACHINE is doing: the share of it ffmpeg is getting, the share everything else is taking, the share spent waiting on a disk, the CPU's current clock and its temperature. The budget predicts a rung from benchmarks taken at startup on an idle box, and on 2026-08-15 it predicted 1.83x for a rung that then ran at 0.90-0.999x with nothing else encoding — and no log anywhere could say which of the candidate reasons it was. Now the reading exists: an encoder starved of cores, a machine that has dropped its clock or grown hot, and work around the encode that nobody counted all look different in this line. Linux-only and best effort — a host without `/proc` writes nothing and nothing else changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
package/server.js CHANGED
@@ -124,21 +124,15 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
124
124
  // For software libx264, benchmark preset throughput once at startup so the
125
125
  // session manager can pick the highest-quality preset that still encodes each
126
126
  // stream faster than realtime. Hardware encoders use their own fixed preset.
127
- const softwarePresetBenchmark = videoEncoder?.kind === "software"
128
- ? await benchmarkSoftwarePresets({ ffmpegBin, logger })
129
- : null;
130
- // A re-encode pays for decoding as well, and the preset benchmark measures
131
- // only the encoder — which is how a rung this host runs at 0.39x came to be
132
- // offered as if it cleared realtime 2.5× over (measured 2026-08-14). Solve
133
- // the decode cost from the bundled calibration clips once at startup; every
134
- // source is then priced from figures the probe already has.
135
- // Only the software path can read it: the budget and the ladder both bail
136
- // on a missing preset benchmark, and that is only produced for libx264. A
137
- // host with a hardware encoder would pay three decodes at every start for a
138
- // figure nothing would ever ask for.
127
+ // The decode model first, and the preset benchmark second — they are
128
+ // independent now (the presets are timed on raw frames), but the order costs
129
+ // nothing and keeps the two figures side by side in the log.
139
130
  const decodeCostModel = videoEncoder?.kind === "software"
140
131
  ? await benchmarkDecodeCost({ ffmpegBin, logger })
141
132
  : null;
133
+ const softwarePresetBenchmark = videoEncoder?.kind === "software"
134
+ ? await benchmarkSoftwarePresets({ ffmpegBin, logger })
135
+ : null;
142
136
  // Whether this ffmpeg build can tone-map HDR→SDR (zscale + tonemap filters).
143
137
  // Detected once; the session manager applies the tonemap chain only for HDR
144
138
  // sources on the software path when available.
@@ -18,7 +18,7 @@ import { spawn } from "node:child_process";
18
18
  import { createRequire } from "node:module";
19
19
  import { logger } from "../utils/logger.js";
20
20
  import { readKeyframeIndex } from "./container-index/index.js";
21
- import { readMachineState, sampleHost, shareOfMachine } from "./host-load.js";
21
+ import { readMachineState, readProcessCpuSeconds, readSystemCpu, shareOfMachine } from "./host-load.js";
22
22
 
23
23
  /** Own package version, stamped onto session-start log lines. */
24
24
  const PROXY_VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -2665,27 +2665,69 @@ export class HlsSessionManager {
2665
2665
  this.#hostLoadSample = null;
2666
2666
  return;
2667
2667
  }
2668
- // The busiest single encoder is the one worth naming; the system share
2669
- // below covers everything else the box is doing.
2670
- const watched = encoding[0];
2671
- const sample = await sampleHost(watched.ffmpeg?.pid ?? null);
2668
+ // EVERY encoder, added up. One of them is meaningless on a host that runs a
2669
+ // picture and an audio track at once, and picking the first would have
2670
+ // reported whichever the map happened to hold.
2671
+ // Kept per PROCESS, not as one total. The set changes between readings —
2672
+ // a seek kills ffmpeg and starts another with a new pid whose counter
2673
+ // begins at zero, a session ends, a rendition begins — and subtracting one
2674
+ // total from another across a changed set produces nonsense: a restart
2675
+ // alone would print something like `ffmpeg=-598%`. Only pids present in
2676
+ // BOTH readings are counted, so a process that came or went contributes
2677
+ // nothing rather than a lie.
2678
+ const pids = encoding.map((session) => session.ffmpeg?.pid ?? null).filter((pid) => pid !== null);
2679
+ const [system, ...cpuReadings] = await Promise.all([
2680
+ readSystemCpu(),
2681
+ ...pids.map((pid) => readProcessCpuSeconds(pid))
2682
+ ]);
2683
+ /** @type {Map<number, number>} */
2684
+ const byPid = new Map();
2685
+ pids.forEach((pid, index) => {
2686
+ const seconds = cpuReadings[index];
2687
+ if (seconds !== null) {
2688
+ byPid.set(pid, seconds);
2689
+ }
2690
+ });
2691
+ const sample = { takenAt: Date.now(), byPid, system };
2672
2692
  const previous = this.#hostLoadSample;
2673
2693
  this.#hostLoadSample = sample;
2674
2694
  if (previous === null) {
2675
2695
  return; // the first reading is only something to compare against
2676
2696
  }
2677
- const share = shareOfMachine(previous, sample);
2697
+ // Summed over the pids both readings hold, so nothing is measured against a
2698
+ // process that was not there before. Unknown stays unknown: on a host with
2699
+ // no `/proc` there are no readings at all, and the share is null rather
2700
+ // than a confident zero.
2701
+ let encoderDelta = null;
2702
+ for (const [pid, seconds] of sample.byPid) {
2703
+ const before = previous.byPid?.get(pid);
2704
+ if (before !== undefined && seconds >= before) {
2705
+ encoderDelta = (encoderDelta ?? 0) + (seconds - before);
2706
+ }
2707
+ }
2708
+ const share = shareOfMachine(
2709
+ { takenAt: previous.takenAt, processCpuSeconds: encoderDelta === null ? null : 0, system: previous.system },
2710
+ { takenAt: sample.takenAt, processCpuSeconds: encoderDelta, system: sample.system }
2711
+ );
2678
2712
  if (share === null) {
2679
2713
  return;
2680
2714
  }
2715
+ // How many of them are stopped by the look-ahead cap. Without this a zero
2716
+ // share reads as an encoder being starved of the machine, when it is an
2717
+ // encoder deliberately not running — which is what the first readings on
2718
+ // the addon host actually were (2026-08-15: `ffmpeg=0% system=24%`, both
2719
+ // encoders suspended, and the speed beside it a stale figure from before
2720
+ // they stopped).
2721
+ const suspended = encoding.filter((session) => session.encoderPaused === true).length;
2722
+ const running = encoding.length - suspended;
2681
2723
  const machine = await readMachineState();
2682
2724
  const asPercent = (value) => (value === null ? "n/a" : `${Math.round(value * 100)}%`);
2683
2725
  logger.info(
2684
2726
  `host-load: ffmpeg=${asPercent(share.processShare)} system=${asPercent(share.systemShare)} ` +
2685
2727
  `iowait=${asPercent(share.iowaitShare)} cpu=${machine.megahertz === null ? "n/a" : `${machine.megahertz}MHz`} ` +
2686
2728
  `temp=${machine.celsius === null ? "n/a" : `${machine.celsius}C`} ` +
2687
- `encoders=${encoding.length} speed=${watched.progress?.speed ?? "n/a"} ` +
2688
- `over=${share.elapsedSec.toFixed(1)}s`
2729
+ `encoders=${running} running` + (suspended > 0 ? ` +${suspended} suspended` : "") +
2730
+ ` over=${share.elapsedSec.toFixed(1)}s`
2689
2731
  );
2690
2732
  }
2691
2733