@torrent-tv/proxy 2.101.10 → 2.101.11

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.101.10",
3
+ "version": "2.101.11",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -156,10 +156,19 @@ export class EncodeOrchestrator {
156
156
  * Whether the run a viewer needs first is short of the processor, from the
157
157
  * last time who has the processor was decided (`processorShortfall`).
158
158
  *
159
- * @type {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number } | null}
159
+ * @type {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number,
160
+ * waitedFor?: number } | null}
160
161
  */
161
162
  #shortfall = null;
162
163
 
164
+ /**
165
+ * The piece each waiting viewer's start is held by the production of, as
166
+ * the start forecast last said it (`noteViewerWaiting`), by viewer.
167
+ *
168
+ * @type {Map<string, { address: string, index: number }>}
169
+ */
170
+ #viewersWaiting = new Map();
171
+
163
172
  /**
164
173
  * What starting and stopping an encoder was measured to cost on this host,
165
174
  * before any viewer existed. Told once; real runs replace it as they end.
@@ -455,6 +464,7 @@ export class EncodeOrchestrator {
455
464
  this.#lastError.delete(address);
456
465
  this.#lastProgress.delete(address);
457
466
  this.#failedStarts.delete(address);
467
+ this.#forgetWaitsOn(address);
458
468
  }
459
469
 
460
470
  /** @param {string} address @returns {{ at: number, count: number, blocked: boolean, limit: number }} */
@@ -610,6 +620,37 @@ export class EncodeOrchestrator {
610
620
  // right whatever the last one met.
611
621
  if (!Array.isArray(zones) || zones.length === 0) {
612
622
  this.#forgetInputState(address);
623
+ this.#forgetWaitsOn(address);
624
+ }
625
+ }
626
+
627
+ /**
628
+ * What a viewer's start waits for, as the start forecast says each time it
629
+ * is asked: the production of piece `index` of `address`, or nothing an
630
+ * encoder makes (null). The map's deadlines can be met while the forecast
631
+ * still holds the viewer for the stock it wants after the piece they stand
632
+ * in: on Home Assistant (r87, torrent-tv/meta#180) the download was let go
633
+ * 5.3 s before a seek's viewer started, while the forecast held them for
634
+ * the production of the next piece. Who has the processor is decided again
635
+ * when it changes.
636
+ *
637
+ * @param {string} viewer
638
+ * @param {{ address: string, index: number } | null} wait
639
+ */
640
+ noteViewerWaiting(viewer, wait) {
641
+ const said = this.#viewersWaiting.get(viewer) ?? null;
642
+ const next = typeof wait?.address === "string" && Number.isInteger(wait.index)
643
+ ? { address: wait.address, index: wait.index } : null;
644
+ if (said?.address === next?.address && said?.index === next?.index) return;
645
+ if (next) this.#viewersWaiting.set(viewer, next);
646
+ else this.#viewersWaiting.delete(viewer);
647
+ this.shareTheMachine();
648
+ }
649
+
650
+ /** @param {string} address */
651
+ #forgetWaitsOn(address) {
652
+ for (const [viewer, wait] of this.#viewersWaiting) {
653
+ if (wait.address === address) this.#viewersWaiting.delete(viewer);
613
654
  }
614
655
  }
615
656
 
@@ -739,9 +780,14 @@ export class EncodeOrchestrator {
739
780
  const now = this.now();
740
781
  const running = this.#runningNow();
741
782
  const need = this.#needOf(leading);
742
- // Only a run in a band a viewer is in can be short for a viewer.
743
- const short = leading.urgent === true ? this.#pauseRule.shortfall({ leader: leading.run, need }) : null;
744
- this.#noteShortfall(short ? { address: leading.address, ...short } : null);
783
+ // Only a run in a band a viewer is in can be short for a viewer, and a run
784
+ // making a piece the start forecast holds a viewer for needs all it can
785
+ // make, whatever the map's deadlines say.
786
+ const waited = this.#runAViewerWaitsFor();
787
+ const judged = waited ?? (leading.urgent === true ? leading : null);
788
+ const short = judged
789
+ ? this.#pauseRule.shortfall({ leader: judged.run, need: waited ? Number.POSITIVE_INFINITY : need }) : null;
790
+ this.#noteShortfall(short ? { address: judged.address, ...short, ...(waited ? { waitedFor: waited.index } : {}) } : null);
745
791
  const free = this.#processorReading("freeCores");
746
792
  const others = this.#processorReading("otherCores");
747
793
  for (const [address, runs] of this.#runs) {
@@ -768,20 +814,41 @@ export class EncodeOrchestrator {
768
814
  this.#placeHeldBack(leading);
769
815
  }
770
816
 
817
+ /**
818
+ * The live run making a piece the start forecast holds a viewer for, the
819
+ * most urgent if there are several; null where none is.
820
+ *
821
+ * @returns {{ address: string, index: number, run: object, band: { urgent: boolean, priority: number } } | null}
822
+ */
823
+ #runAViewerWaitsFor() {
824
+ let found = null;
825
+ for (const { address, index } of this.#viewersWaiting.values()) {
826
+ for (const run of this.#runs.get(address) ?? []) {
827
+ if (!run.isAlive || run.isStopping || run.isSuspended === true || index < run.head || index > run.to) continue;
828
+ const band = this.#bandAt(address, run.head);
829
+ if (!found || compareUrgency(band, found.band) < 0) found = { address, index, run, band };
830
+ }
831
+ }
832
+ return found;
833
+ }
834
+
771
835
  /**
772
836
  * Whether the run a viewer needs first is short of the processor, and by
773
837
  * what: its output, the speed it made over its seconds kept (median and
774
838
  * scatter) and the speed its deadlines need, infinite where a viewer waits
775
- * for it (`PauseRule.shortfall`, torrent-tv/meta#180). Null where it is not
776
- * short, or nothing measured it.
839
+ * for it (`PauseRule.shortfall`, torrent-tv/meta#180). The run the start
840
+ * forecast holds a viewer for is judged before the map's leader, and names
841
+ * the piece it is waited for by (`waitedFor`). Null where it is not short,
842
+ * or nothing measured it.
777
843
  *
778
- * @returns {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number } | null}
844
+ * @returns {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number,
845
+ * waitedFor?: number } | null}
779
846
  */
780
847
  processorShortfall() {
781
848
  return this.#shortfall;
782
849
  }
783
850
 
784
- /** @param {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number } | null} short */
851
+ /** @param {{ address: string, speed: { middle: number, scatter: number }, need: number, seconds: number, waitedFor?: number } | null} short */
785
852
  #noteShortfall(short) {
786
853
  this.#shortfall = short;
787
854
  this.shortfallNoted();
@@ -790,6 +790,11 @@ export class ViewerRequests {
790
790
  })}`);
791
791
  }
792
792
  this.#sayHolding(session, consumerId, measurement, forecast, input, now);
793
+ // The encoder this viewer waits for, told to whoever shares the processor
794
+ // (torrent-tv/meta#180): the forecast knows it before the map's deadlines do.
795
+ const holding = forecast.holdingBy;
796
+ this.#host.noteViewerWaiting?.(consumerId || session.id,
797
+ holding?.kind === "production" ? { address: holding.track, index: holding.segment } : null);
793
798
  return forecast;
794
799
  }
795
800
 
@@ -631,6 +631,7 @@ export function wireOutputs({
631
631
  disposeSession: (...args) => parts.lifecycle.disposeSession(...args),
632
632
  planEncodersSoon: (...args) => parts.encodeRuns.planEncodersSoon(...args),
633
633
  waitUntilReady: (...args) => parts.serving.waitUntilReady(...args),
634
+ noteViewerWaiting: (viewer, wait) => parts.encodeOrchestrator?.noteViewerWaiting(viewer, wait),
634
635
  get encodeRuns() { return parts.encodeRuns; },
635
636
  // When this viewer's playback can start and run to the end: the viewer
636
637
  // component's forecast, which models the viewer's browser.
@@ -781,7 +782,8 @@ export function wireOutputs({
781
782
  const swarm = parts.hostLoad.swarmRate();
782
783
  const made = `the encoder a viewer needs first made ${short.speed.middle.toFixed(2)}x ` +
783
784
  `(scatter ${short.speed.scatter.toFixed(2)}) over ${short.seconds}s` +
784
- (Number.isFinite(short.need) ? `, needing ${short.need.toFixed(2)}x` : " with a viewer waiting");
785
+ (Number.isFinite(short.need) ? `, needing ${short.need.toFixed(2)}x` : " with a viewer waiting") +
786
+ (Number.isInteger(short.waitedFor) ? ` for its #${short.waitedFor} by the start forecast` : "");
785
787
  if (!Number.isFinite(needs) || needs <= 0) {
786
788
  because = `${made}; its film's bytes per second are unknown`;
787
789
  } else if (swarm === null) {