@torrent-tv/proxy 2.96.11 → 2.96.12

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.96.11",
3
+ "version": "2.96.12",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -33,6 +33,24 @@ import { contentionPenalty } from "./contention.js";
33
33
  import { SegmentDemand } from "./SegmentDemand.js";
34
34
  import { runStateOf } from "./encode-run-state.js";
35
35
 
36
+ /**
37
+ * Which of two bands of the map is wanted sooner: what is needed before
38
+ * playback can continue first, then the higher priority.
39
+ *
40
+ * Everything needed before playback can continue is ONE band, whatever its
41
+ * priority: two viewers each short of their own minimum share the machine, and
42
+ * neither is stopped for the other. Priority orders only what can wait.
43
+ *
44
+ * @param {{ urgent: boolean, priority: number }} left
45
+ * @param {{ urgent: boolean, priority: number }} right
46
+ * @returns {number} Negative when `left` is more urgent, zero when equal.
47
+ */
48
+ export function compareUrgency(left, right) {
49
+ const urgent = Number(right.urgent === true) - Number(left.urgent === true);
50
+ if (urgent !== 0 || left.urgent === true) return urgent;
51
+ return right.priority - left.priority;
52
+ }
53
+
36
54
  /**
37
55
  * @typedef {Object} SegmentCoverage
38
56
  * @property {(address: string) => number[]} provenNumbers
@@ -105,6 +123,14 @@ export class EncodeOrchestrator {
105
123
  /** The last state said out loud, so an unchanged state is not repeated. */
106
124
  #lastDescribed = "";
107
125
 
126
+ /**
127
+ * The most urgent band any live encoder stands in, over every output, as the
128
+ * pass that is deciding now found it. Null when no encoder is alive.
129
+ *
130
+ * @type {{ urgent: boolean, priority: number, address: string, index: number } | null}
131
+ */
132
+ #leading = null;
133
+
108
134
  /** What a stop and a start have cost on this host. */
109
135
  #costs = new RunCosts();
110
136
 
@@ -575,6 +601,9 @@ export class EncodeOrchestrator {
575
601
  for (const run of this.runsOn(address)) {
576
602
  run.noteProduced(index);
577
603
  }
604
+ // A closed piece moves its encoder forward, perhaps out of the band that
605
+ // led: who has the processor is decided again now, not at the next pass.
606
+ this.shareTheMachine();
578
607
  }
579
608
 
580
609
  /**
@@ -586,9 +615,11 @@ export class EncodeOrchestrator {
586
615
  */
587
616
  reconcile() {
588
617
  const addresses = new Set([...this.#demand.addresses(), ...this.#runs.keys()]);
618
+ this.#leading = this.#leadingBand();
589
619
  for (const address of addresses) {
590
620
  this.#reconcileOne(address);
591
621
  }
622
+ this.shareTheMachine();
592
623
  // WHAT THIS CLASS BELIEVES, said by this class. `describe()` was written
593
624
  // and called from nowhere, so on 2026-09-05 the question "why did the plan
594
625
  // not see the gap the viewer was stopped at" had to be answered by
@@ -604,6 +635,82 @@ export class EncodeOrchestrator {
604
635
  }
605
636
  }
606
637
 
638
+ /**
639
+ * How urgently the map wants one segment of one output: whether it is needed
640
+ * before playback can continue, and the map's priority for it. A number in
641
+ * nobody's zone is wanted least of all.
642
+ *
643
+ * The priority is a step of distance from a viewer on one scale for every
644
+ * output, so the picture and the soundtrack a viewer plays are compared on it
645
+ * directly.
646
+ *
647
+ * @param {string} address
648
+ * @param {number} index
649
+ * @returns {{ urgent: boolean, priority: number }}
650
+ */
651
+ #bandAt(address, index) {
652
+ let band = { urgent: false, priority: 0 };
653
+ for (const zone of this.#demand.mapOn(address)) {
654
+ if (index < zone.from || index > zone.to) continue;
655
+ const candidate = { urgent: zone.urgent === true, priority: Number(zone.priority) || 0 };
656
+ if (compareUrgency(candidate, band) < 0) band = candidate;
657
+ }
658
+ return band;
659
+ }
660
+
661
+ /**
662
+ * The most urgent band a live encoder stands in, over every output.
663
+ *
664
+ * @returns {{ urgent: boolean, priority: number, address: string, index: number } | null}
665
+ */
666
+ #leadingBand() {
667
+ let leading = null;
668
+ for (const [address, runs] of this.#runs) {
669
+ for (const run of runs) {
670
+ if (!run.isAlive || run.isStopping) continue;
671
+ const band = this.#bandAt(address, run.head);
672
+ if (!leading || compareUrgency(band, leading) < 0) leading = { ...band, address, index: run.head };
673
+ }
674
+ }
675
+ return leading;
676
+ }
677
+
678
+ /**
679
+ * WHO GETS THE PROCESSOR FIRST, over every output at once.
680
+ *
681
+ * Each output is planned on its own, and a file is encoded whole in the order
682
+ * its map gives (the rule of 2026-09-05), so an output that is cheap to make
683
+ * runs far ahead of one that is dear: field 2026-10-10, a copied soundtrack
684
+ * at 38 minutes while its picture, re-encoded at 0.65–1.31x, was at five and
685
+ * the viewer stalled. The machine is one, so the order is one: an encoder
686
+ * making something less urgent than what another encoder is making somewhere
687
+ * stands still where it is — paused, not stopped, so it keeps its place, its
688
+ * input and its claim — and goes on the moment nothing more urgent is being
689
+ * made. The encoder in the most urgent band is never paused, so something is
690
+ * always being made.
691
+ *
692
+ * Paused rather than lowered in priority: a lowered priority cannot be raised
693
+ * again without `CAP_SYS_NICE`, which the add-on's container does not have
694
+ * (Home Assistant 2026-10-10: `renice 0` after `renice 19` left 19).
695
+ */
696
+ shareTheMachine() {
697
+ const leading = this.#leadingBand();
698
+ this.#leading = leading;
699
+ if (!leading) return;
700
+ for (const [address, runs] of this.#runs) {
701
+ for (const run of runs) {
702
+ if (!run.isAlive || run.isStopping || typeof run.pause !== "function") continue;
703
+ const behind = compareUrgency(this.#bandAt(address, run.head), leading) > 0;
704
+ if (behind && run.isSuspended !== true) {
705
+ run.pause(`#${run.head} of ${address} is wanted less urgently than #${leading.index} of ${leading.address}, ` +
706
+ `which is being made`);
707
+ } else if (!behind && run.isSuspended === true) {
708
+ run.resume(`nothing more urgent than #${run.head} of ${address} is being made`);
709
+ }
710
+ }
711
+ }
712
+ }
713
+
607
714
  /**
608
715
  * @param {string} address
609
716
  */
@@ -739,7 +846,16 @@ export class EncodeOrchestrator {
739
846
  `live=${JSON.stringify(live.map((run) => ({ from: run.from, to: run.to, head: run.head, speedX: run.speedX })))}`
740
847
  );
741
848
  }
849
+ // A START BEHIND WHAT IS BEING MADE ELSEWHERE IS NOT MADE: the encoder would
850
+ // be paused the moment it exists (`shareTheMachine`), holding a process and
851
+ // its input for nothing. It is placed when nothing more urgent is in hand.
852
+ let heldBack = null;
742
853
  for (const action of actions) {
854
+ if (action.type === "start" && this.#leading &&
855
+ compareUrgency(this.#bandAt(address, action.from), this.#leading) > 0) {
856
+ heldBack = action;
857
+ continue;
858
+ }
743
859
  if (action.type === "stop") {
744
860
  this.#stop(action.run, action.because);
745
861
  continue;
@@ -795,6 +911,15 @@ export class EncodeOrchestrator {
795
911
  const stillRunning = this.runsOn(address).filter((run) => run.isAlive);
796
912
  if (wanting === null || stillRunning.length > 0) {
797
913
  this.#lastUnmet.delete(address);
914
+ } else if (heldBack) {
915
+ const said = `held:${heldBack.from}:${this.#leading?.address}:${this.#leading?.index}`;
916
+ if (this.#lastUnmet.get(address) !== said) {
917
+ this.#lastUnmet.set(address, said);
918
+ this.logger.info(
919
+ `encode: #${heldBack.from} of ${address} waits — #${this.#leading?.index} of ` +
920
+ `${this.#leading?.address} is wanted more urgently and is being made`
921
+ );
922
+ }
798
923
  } else if (this.#lastUnmet.get(address) !== wanting) {
799
924
  this.#lastUnmet.set(address, wanting);
800
925
  const held = this.#segmentCoverage ? this.#segmentCoverage.filesHeld(address) : -1;
@@ -216,6 +216,8 @@ export class EncodeRun {
216
216
  * encoder itself names it on its own channel, and answers with the name that
217
217
  * piece is served under — because making it servable is a rename, and only
218
218
  * whoever owns the disk can perform one.
219
+ * @param {(index: number) => void} [params.onProduced] - Called with every
220
+ * segment number this run has closed, as it closes it.
219
221
  * @param {(progress: object) => void} [params.onProgress] - Called with this
220
222
  * run's progress on the source timeline after every `-progress` report.
221
223
  * @param {() => number | null} [params.lastSegmentIndex] - The film's last
@@ -251,6 +253,7 @@ export class EncodeRun {
251
253
  onSpeedMeasured,
252
254
  onClosed,
253
255
  onProgress,
256
+ onProduced,
254
257
  lastSegmentIndex,
255
258
  inputUnavailable,
256
259
  argsDescribed = "",
@@ -274,6 +277,7 @@ export class EncodeRun {
274
277
  this.clock = new RunClock({ now: this.now });
275
278
  this.onEnded = typeof onEnded === "function" ? onEnded : () => {};
276
279
  this.onProgress = typeof onProgress === "function" ? onProgress : () => {};
280
+ this.onProduced = typeof onProduced === "function" ? onProduced : () => {};
277
281
  this.onSpeedMeasured = typeof onSpeedMeasured === "function" ? onSpeedMeasured : () => {};
278
282
  // Told the NAME of every piece the encoder has closed. The name is the
279
283
  // proof it is whole; nothing else here can prove that.
@@ -569,6 +573,7 @@ export class EncodeRun {
569
573
  if (this.#state === ENCODE_RUN_STATE.STARTING) {
570
574
  this.#transition(ENCODE_RUN_EVENT.FIRST_SEGMENT);
571
575
  }
576
+ this.onProduced(index);
572
577
  }
573
578
  }
574
579
 
@@ -999,6 +999,9 @@ export class EncodeRuns {
999
999
  run.to,
1000
1000
  inputUnavailable: (message) => isInputUnavailable(message),
1001
1001
  onProgress: (report) => this.#noteRunProgress(session, run, report),
1002
+ // A closed piece moves this encoder forward, perhaps out of the band that
1003
+ // led; who has the processor is decided again at once.
1004
+ onProduced: () => this.#host.encodeOrchestrator?.shareTheMachine?.(),
1002
1005
  indexOfName: (name) => session.segmentFormat.segmentIndexFromName(
1003
1006
  session.segmentFormat.servedNameOf?.(name) ?? name),
1004
1007
  // Why this encoder exists, recorded with its argument list. It used to be