@torrent-tv/proxy 2.21.0 → 2.22.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,12 @@
1
+ ## 2.22.0
2
+
3
+ - **Fix**: What a rung is OFFERED on is the startup measurement again, not the figure learned from a live session. The startup one is taken on a quiet machine against known clips and does not move; the learned one moves with whatever else the box was doing that second, and three field sessions in a row show the price of that: decoding learned at 0.87x, then at 1.34-1.57x, against calibration's 2.6x — each reading refusing another rung until the offer held a single height and the quality menu vanished with it.
4
+ - **New**: A live reading keeps the one thing it is authority on — itself. A rung that has actually been seen running below realtime, with the machine to itself, is withdrawn on that evidence whatever any prediction says. A rung nobody has run is judged by the startup measurement like any other, because a measurement of one rung is not a prediction about the rest.
5
+
6
+ ## 2.21.1
7
+
8
+ - **Fix**: A cost is learned only from an encoder that had the machine to itself. Beside another encoder a reading already contains that other work, and the budget then ADDS the same work again when it predicts — so the price of a file grew with every reading. Measured in the field 2026-08-15: copying, whose truth is 7.9x, was learned as 2.03x; decoding, whose calibration clips say 2.6x, as 0.87x. Every re-encoded rung was then refused (`not offering 720p=0.56x … 240p=0.66x`), the offer collapsed to the one copied height, and the viewer lost the quality menu entirely.
9
+
1
10
  ## 2.21.0
2
11
 
3
12
  - **Fix**: The two costs added in 2.18.0 and 2.19.0 were never measured in production — both features were inert. The torrent's cost read `torrentPool.client`, a field that belongs to the pool implementation that no longer runs on this thread (the WebTorrent client lives on the worker), so the byte totals were always zero and the guard that needs two megabytes of movement never passed. The copy's cost sat in a branch its only caller had already filtered out, so it never ran. The totals now come from the worker over its own protocol, and the caller admits a copying session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.21.0",
3
+ "version": "2.22.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": {
@@ -4750,6 +4750,9 @@ export class HlsSessionManager {
4750
4750
  heights: ordered,
4751
4751
  ownHeight: own,
4752
4752
  playingHeight: playing,
4753
+ // What each rung was actually seen doing in this session, which is the
4754
+ // only thing a live reading may speak for.
4755
+ measuredHeights: this.#measuredRungSpeeds(owner),
4753
4756
  // What the family is already spending while a rung is considered. The
4754
4757
  // picture being COPIED is the common case and used to be priced at
4755
4758
  // nothing; measured, it is about an eighth of the machine.
@@ -4759,7 +4762,18 @@ export class HlsSessionManager {
4759
4762
  fps: Number(owner.outputFps) || TRANSCODE_FPS,
4760
4763
  source: owner.sourceDecode ?? null,
4761
4764
  transcodeVideo: owner.transcodeVideo === true,
4762
- observedDecodeCostSec: observed?.costSec ?? null
4765
+ // NOT the learned cost. What a rung is OFFERED on is the startup
4766
+ // measurement, which is taken on a quiet machine against known clips and
4767
+ // does not move; the figure learned from a live session moves with
4768
+ // whatever else the box was doing at that second, and three field
4769
+ // sessions in a row (2026-08-15) show what that costs: 0.87x, then
4770
+ // 1.34-1.57x against calibration's 2.6x, each reading refusing another
4771
+ // rung until the offer held one height and the menu disappeared with it.
4772
+ //
4773
+ // The learned figure keeps its job — but only over the rung it was
4774
+ // measured ON, and only to take that one away (below). A measurement of
4775
+ // one rung is not a prediction about the others.
4776
+ observedDecodeCostSec: null
4763
4777
  });
4764
4778
  if (owner !== session) {
4765
4779
  // An orphan: its base is gone, so this is the family's last word and
@@ -4831,6 +4845,26 @@ export class HlsSessionManager {
4831
4845
  *
4832
4846
  * @param {HlsSession} session
4833
4847
  */
4848
+ /**
4849
+ * How many encoders are running and not suspended right now.
4850
+ *
4851
+ * A cost measured while two of them share the machine belongs to neither.
4852
+ *
4853
+ * @returns {number}
4854
+ */
4855
+ #runningEncoders() {
4856
+ let running = 0;
4857
+ for (const session of this.sessionsById.values()) {
4858
+ if (session?.ffmpeg != null &&
4859
+ !hasChildExited(session.ffmpeg) &&
4860
+ session.encoderPaused !== true &&
4861
+ session.state !== "disposed") {
4862
+ running += 1;
4863
+ }
4864
+ }
4865
+ return running;
4866
+ }
4867
+
4834
4868
  async #learnFromEncoder(session) {
4835
4869
  if (
4836
4870
  !session ||
@@ -4866,6 +4900,20 @@ export class HlsSessionManager {
4866
4900
  if (speed === null) {
4867
4901
  return;
4868
4902
  }
4903
+ // Only while this encoder had the machine to itself. A reading taken beside
4904
+ // another encoder already contains that other work — and the budget then
4905
+ // ADDS the same work again when it predicts, so the cost is counted twice
4906
+ // and grows with every reading. Measured 2026-08-15 in the field: copying,
4907
+ // whose truth is 7.9x, was learned as 2.03x, and decoding, whose clips say
4908
+ // 2.6x, as 0.87x. Every rung was then refused, the offer collapsed to the
4909
+ // one copied height, and the viewer lost the quality menu altogether.
4910
+ if (this.#runningEncoders() > 1) {
4911
+ return;
4912
+ }
4913
+ // What this rung did with the machine to itself — the one figure a live
4914
+ // reading is authority on, and what withdraws a rung that has been seen
4915
+ // failing without letting it speak for rungs nobody has run.
4916
+ session.lastAloneSpeed = speed;
4869
4917
  if (speed < BUDGET_SPEED_OK && await this.#classifyTranscodeBound(session) === "download") {
4870
4918
  return; // the torrent is what is short; this says nothing about the host
4871
4919
  }
@@ -5128,6 +5176,32 @@ export class HlsSessionManager {
5128
5176
  return cost;
5129
5177
  }
5130
5178
 
5179
+ /**
5180
+ * The speed each rung of this family was last seen running at, when it was
5181
+ * running alone.
5182
+ *
5183
+ * A rung that has been watched failing is refused on that evidence; a rung
5184
+ * nobody has run says nothing about itself and is judged by the startup
5185
+ * measurement like any other.
5186
+ *
5187
+ * @param {HlsSession} base
5188
+ * @returns {Map<number, number>}
5189
+ */
5190
+ #measuredRungSpeeds(base) {
5191
+ /** @type {Map<number, number>} */
5192
+ const speeds = new Map();
5193
+ for (const session of this.#familyOf(base)) {
5194
+ if (session.transcodeVideo !== true || !Number.isFinite(session.lastAloneSpeed)) {
5195
+ continue;
5196
+ }
5197
+ const height = this.variantHeightOf(session);
5198
+ if (height > 0) {
5199
+ speeds.set(height, session.lastAloneSpeed);
5200
+ }
5201
+ }
5202
+ return speeds;
5203
+ }
5204
+
5131
5205
  #sustainableHeights({
5132
5206
  heights,
5133
5207
  ownHeight,
@@ -5138,7 +5212,8 @@ export class HlsSessionManager {
5138
5212
  source,
5139
5213
  transcodeVideo,
5140
5214
  observedDecodeCostSec = null,
5141
- concurrentCostSec = 0
5215
+ concurrentCostSec = 0,
5216
+ measuredHeights = null
5142
5217
  }) {
5143
5218
  const benchmark = this.softwarePresetBenchmark;
5144
5219
  if (!Array.isArray(benchmark) || benchmark.length === 0 || sourceHeight <= 0 || sourceWidth <= 0) {
@@ -5170,6 +5245,14 @@ export class HlsSessionManager {
5170
5245
  kept.push(height);
5171
5246
  continue;
5172
5247
  }
5248
+ // A rung this session has actually been seen running below realtime is
5249
+ // withdrawn on that evidence, whatever the prediction says. This is the
5250
+ // one thing a live reading is authority on: itself.
5251
+ const measured = measuredHeights?.get(height) ?? null;
5252
+ if (measured !== null && measured < 1) {
5253
+ dropped.push(`${height}p=${measured.toFixed(2)}x measured`);
5254
+ continue;
5255
+ }
5173
5256
  const width = Math.round(((sourceWidth / sourceHeight) * height) / 2) * 2;
5174
5257
  const { speed, sustainable } = canSustainOutput({
5175
5258
  benchmark,