@torrent-tv/proxy 2.97.31 → 2.98.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/bin/cli.js CHANGED
@@ -88,6 +88,7 @@ program
88
88
  .option("--no-transcode-audio", "Disable optional HLS AAC audio transcoding")
89
89
  .option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
90
90
  .option("--delivery-sink", "Serve /api/delivery-sink, a torrent-free byte stream for delivery testing")
91
+ .option("--pin-preset <name>", "Encode every software picture at this libx264 preset only, for runs that must be compared at one quality (off by default)")
91
92
  .option("--profile-start <seconds>", "Profile the main thread's CPU for this many seconds from the first source opened, written beside the state (off by default)")
92
93
  .option(
93
94
  "--send-chunk-bytes <bytes>",
@@ -394,6 +395,7 @@ try {
394
395
  logFile: options.logFile ?? "",
395
396
  deliverySink: options.deliverySink === true,
396
397
  profileStartSeconds: Number(options.profileStart) || 0,
398
+ pinnedPreset: typeof options.pinPreset === "string" && options.pinPreset.trim() ? options.pinPreset.trim() : null,
397
399
  // Late-bound the same way `webRtcManager` is below: the torrent pool is
398
400
  // built inside `startProxyServer`, before `dataChannelHandler` — the
399
401
  // thing that actually owns a channel to push down — exists.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.97.31",
3
+ "version": "2.98.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -127,6 +127,9 @@ export async function startProxyServer({
127
127
  deliverySink = false,
128
128
  // Seconds of main-thread CPU profile from the first source opened; 0 is off.
129
129
  profileStartSeconds = 0,
130
+ // The one software preset every picture is encoded at, for runs compared at
131
+ // one quality; null leaves the choice to the machine.
132
+ pinnedPreset = null,
130
133
  // What viewers' connections carry beyond the film, per byte of film; the
131
134
  // transport counts it and is built after this server, so it is late-bound.
132
135
  serviceShare = () => null,
@@ -798,6 +801,7 @@ export async function startProxyServer({
798
801
  });
799
802
  let sourcePreparation;
800
803
  const outputParts = wireOutputs({
804
+ pinnedPreset,
801
805
  indexMemory,
802
806
  serviceShare,
803
807
  readMetadataActivity: () => containerOrchestrator.activity(),
@@ -1358,8 +1358,23 @@ export class Renditions {
1358
1358
  if (!base) {
1359
1359
  return null;
1360
1360
  }
1361
- if (!this.#host.qualityOffer.offeredHeightsFor(base).includes(height)) {
1362
- return null;
1361
+ const offered = this.#host.qualityOffer.offeredHeightsFor(base);
1362
+ if (!offered.includes(height)) {
1363
+ if (!this.#host.outputs.splicableHeights(base).includes(height)) {
1364
+ return null;
1365
+ }
1366
+ // A STEP THE MASTER LISTS AND THE OFFER NO LONGER HOLDS. The offer moves
1367
+ // with what the machine measures, and a page preparing a step it was
1368
+ // asked for can arrive after the ask was withdrawn: field r28, 360p
1369
+ // asked at 21:40:44.308, the offer down to 400p at 21:40:46.143, the
1370
+ // warm-up at 46.170 answered "no such variant" (torrent-tv/meta#178).
1371
+ // The step exists; it is not available now, and the page keeps what it
1372
+ // plays and follows the next ask.
1373
+ this.#host.logger.info(`transcode ${base.id} warming ${height}p for ${consumerId}: not offered now (offering ${offered.map(one => `${one}p`).join(" ") || "nothing"})`);
1374
+ const error = new Error(`${height}p is not offered now.`);
1375
+ error.code = OUTPUT_UNAVAILABLE;
1376
+ error.details = { reason: `${height}p is not offered on this machine now`, figures: { height, offeredHeights: offered } };
1377
+ throw error;
1363
1378
  }
1364
1379
  const index = this.#host.outputTimes.segmentIndexForTime(base, positionSeconds);
1365
1380
  const variant = await this.resolveVariantSession(baseSessionId, height, index, consumerId);
@@ -1715,7 +1715,8 @@ export function buildResolutionLadder(ceilingWidth, ceilingHeight) {
1715
1715
  * @param {{ width: number, height: number }} ceiling
1716
1716
  * @param {number} outputFps
1717
1717
  * @param {{ decodeModel?: object | null, source?: { megapixelsPerSecond: number, megabitsPerSecond: number } | null, requiredSpeed?: number | null }} [cost]
1718
- * @returns {{ width: number, height: number, preset: string, ladder: Array<{ width: number, height: number }>, rungIndex: number } | null}
1718
+ * @returns {{ width: number, height: number, preset: string, ladder: Array<{ width: number, height: number }>, rungIndex: number, bar: number } | null}
1719
+ * `bar` is the speed the choice had to clear.
1719
1720
  */
1720
1721
  export function chooseSoftwareEncodeSettings(benchmark, ceiling, outputFps, cost = {}) {
1721
1722
  if (!Array.isArray(benchmark) || benchmark.length === 0) {
@@ -1750,7 +1751,7 @@ export function chooseSoftwareEncodeSettings(benchmark, ceiling, outputFps, cost
1750
1751
  }
1751
1752
  const chosen = ladder[chosenIndex];
1752
1753
  const preset = pickSoftwarePreset(benchmark, chosen.width * chosen.height * fps, cost, chosen);
1753
- return { width: chosen.width, height: chosen.height, preset, ladder, rungIndex: chosenIndex };
1754
+ return { width: chosen.width, height: chosen.height, preset, ladder, rungIndex: chosenIndex, bar };
1754
1755
  }
1755
1756
 
1756
1757
  /**
@@ -1103,7 +1103,8 @@ export class EncodeCost {
1103
1103
  sourceHeight,
1104
1104
  outputFps,
1105
1105
  source = null,
1106
- requiredSpeed = null
1106
+ requiredSpeed = null,
1107
+ observedDecodeCostSec = null
1107
1108
  }) {
1108
1109
  if (!transcodeVideo || this.#host().encoderKind !== "software" || !this.#host().benchmark) {
1109
1110
  return null;
@@ -1113,26 +1114,44 @@ export class EncodeCost {
1113
1114
  return null;
1114
1115
  }
1115
1116
  const concurrentCostSec = this.#committedMachineCostSec();
1117
+ // THE FILE'S OWN DECODE COST, where its encoder has already been read: the
1118
+ // opening had it and the choice priced decoding from the startup model
1119
+ // instead (review of r28, torrent-tv/meta#178).
1120
+ const decodeCostSec = Number.isFinite(observedDecodeCostSec) && observedDecodeCostSec > 0 ? observedDecodeCostSec : null;
1121
+ /** Every size and preset the choice priced, with the speed it was priced at. */
1122
+ const considered = [];
1116
1123
  const speedForPreset = (benchmark, frame, outputPixelsPerSec) => {
1117
- if (concurrentCostSec === null) {
1118
- return null;
1119
- }
1120
- return this.#speedOnThisMachine({
1124
+ const speed = concurrentCostSec === null ? null : this.#speedOnThisMachine({
1121
1125
  benchmark,
1122
1126
  source,
1123
1127
  width: frame.width,
1124
1128
  height: frame.height,
1125
1129
  fps: outputPixelsPerSec / (frame.width * frame.height),
1126
- observedDecodeCostSec: null,
1130
+ observedDecodeCostSec: decodeCostSec,
1127
1131
  concurrentCostSec
1128
1132
  });
1133
+ considered.push({ preset: benchmark[0]?.preset, width: frame.width, height: frame.height, speed });
1134
+ return speed;
1129
1135
  };
1130
- return chooseSoftwareEncodeSettings(
1136
+ const choice = chooseSoftwareEncodeSettings(
1131
1137
  this.#host().benchmark,
1132
1138
  { width: ceiling.w, height: ceiling.h },
1133
1139
  outputFps,
1134
- { decodeModel: this.#host().decodeModel, source, requiredSpeed, speedForPreset }
1140
+ { decodeModel: this.#host().decodeModel, source, requiredSpeed, speedForPreset, observedDecodeCostSec: decodeCostSec }
1135
1141
  );
1142
+ // WHAT THE CHOICE WAS MADE FROM, said once per choice: three runs of one
1143
+ // file on one host opened at three presets and nothing said why (r25, r26,
1144
+ // r28, torrent-tv/meta#178).
1145
+ if (choice) {
1146
+ const availability = this.#host().availability;
1147
+ const priced = considered.map(({ preset, width, height, speed }) =>
1148
+ `${width}x${height} ${preset}=${Number.isFinite(speed) ? `${speed.toFixed(2)}x` : "unpriced"}`).join(", ");
1149
+ logger.info(`encode choice: ${choice.width}x${choice.height}@${Number(outputFps).toFixed(3)} ${choice.preset} against ${choice.bar.toFixed(2)}x; ` +
1150
+ `priced ${priced}; decoding ${decodeCostSec === null ? "from the startup model" : `${decodeCostSec.toFixed(4)} s per second, measured on this file`}; ` +
1151
+ `machine ${availability?.known ? `${Math.round(availability.share * 100)}% to spare` : "share unknown"}; ` +
1152
+ `committed work ${concurrentCostSec === null ? "unmeasured" : `${concurrentCostSec.toFixed(3)} s per second`}`);
1153
+ }
1154
+ return choice;
1136
1155
  }
1137
1156
  }
1138
1157
 
@@ -144,7 +144,8 @@ export function decideOutputFormat({
144
144
  sourceHeight: source.height,
145
145
  outputFps: fps,
146
146
  source: source.decode,
147
- requiredSpeed: cost.requiredSpeed
147
+ requiredSpeed: cost.requiredSpeed,
148
+ observedDecodeCostSec: cost.observedDecodeCostSec ?? null
148
149
  });
149
150
  const width = budget?.width ?? ceiling?.w ?? 0;
150
151
  const height = budget?.height ?? ceiling?.h ?? 0;
@@ -189,6 +189,9 @@ export function wireOutputs({
189
189
  keyframeTables = new KeyframeTables(),
190
190
  videoEncoder = null,
191
191
  softwarePresetBenchmark = null,
192
+ // A software preset every picture is encoded at, for runs compared at one
193
+ // quality (`--pin-preset`); null leaves the choice to the machine.
194
+ pinnedPreset = null,
192
195
  // What every encoder this host may use was qualified and measured to do at
193
196
  // startup, by kind (`encode/calibration.js`). Takes the place of the one
194
197
  // array above, which a test may still hand in on its own.
@@ -771,8 +774,26 @@ export function wireOutputs({
771
774
  // Null where nothing was measured, and then nothing is re-encoded here.
772
775
  parts.calibration = calibration ?? null;
773
776
  const givenModes = Array.isArray(softwarePresetBenchmark) ? softwarePresetBenchmark : null;
777
+ // ONE PRESET FOR RUNS COMPARED AT ONE QUALITY. Three runs of one film on one
778
+ // host opened at `faster`, `veryfast` and `fast`, so no change could be
779
+ // compared at the same picture (r25, r26, r28, torrent-tv/meta#178). Pinned
780
+ // here, every decision that prices or chooses a software mode — the choice,
781
+ // the offer, the machine's admission and the command — sees the one mode,
782
+ // measured as calibrated. A preset calibration did not qualify is not
783
+ // pinned: the machine's own list stands, and the log says so.
784
+ let pinWarned = false;
785
+ const pinned = (modes) => {
786
+ if (!pinnedPreset || !Array.isArray(modes) || parts.encoders.current?.kind !== "software") return modes;
787
+ const kept = modes.filter(mode => mode.preset === pinnedPreset);
788
+ if (kept.length > 0) return kept;
789
+ if (!pinWarned) {
790
+ pinWarned = true;
791
+ logger.warn(`encode: --pin-preset ${pinnedPreset} names no calibrated mode (${modes.map(mode => mode.preset).join(", ")}); not pinned`);
792
+ }
793
+ return modes;
794
+ };
774
795
  Object.defineProperty(parts, "softwarePresetBenchmark", {
775
- get: () => (parts.calibration
796
+ get: () => pinned(parts.calibration
776
797
  ? parts.calibration.modesFor(parts.encoders.current?.kind)
777
798
  : givenModes),
778
799
  enumerable: true