@torrent-tv/proxy 2.96.10 → 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.10",
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": {
package/server.js CHANGED
@@ -55,6 +55,7 @@ import { createPlaybackPlanner } from "./services/media/playback-planner.js";
55
55
  import { KeyframeTables } from "./services/media/KeyframeTables.js";
56
56
  import { contentsOf } from "./services/torrent/Contents.js";
57
57
  import { joinedWithinPieces } from "./services/torrent/demand/pieces.js";
58
+ import { withoutRepeatedZones } from "./services/torrent/map-zones.js";
58
59
  import { SubtitleOrchestrator } from "./services/media/SubtitleOrchestrator.js";
59
60
  import { containerOrchestrator, CONTAINER_HEAD_BYTES, describeWorkTags } from "./services/media/ContainerOrchestrator.js";
60
61
  import { readPlaybackDeclarations } from "./services/media/read-playback-declarations.js";
@@ -533,7 +534,9 @@ export async function startProxyServer({
533
534
  segmentSourceRanges.set(rangesKey, input.ranges);
534
535
  convert(input.ranges);
535
536
  }
536
- return converted;
537
+ // The ranges a picture reads at open are named again by every segment;
538
+ // only the zone the swarm would rank first is sent (torrent-tv/meta#166).
539
+ return withoutRepeatedZones(converted);
537
540
  }
538
541
  });
539
542
  const mediaReads = new MediaReadRequests({
@@ -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
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file What one zone of the priority map tells the torrent: the level of
3
+ * urgency it is stated at, and the band the swarm ranks it by.
4
+ *
5
+ * Pure arithmetic over the zone's own fields, so the main thread can ask the
6
+ * same question before a map is sent and the torrent thread when it is applied.
7
+ */
8
+ import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "../viewer/PriorityMap.js";
9
+ import { Urgency } from "./demand/Urgency.js";
10
+ import { compareBands } from "./download/bands.js";
11
+
12
+ /**
13
+ * Which level of urgency one zone is stated at, from the map's own number
14
+ * and nothing else. The map's numbers are a scale as long as the film needs
15
+ * — ten bands on a fifty-minute film — while the register has five levels,
16
+ * and the fit is by meaning:
17
+ *
18
+ * - the top of the scale is where a viewer is standing, so it is the
19
+ * cushion being built: {@link Urgency.NEAR}. Never BLOCKED — that level
20
+ * means a reader is stopped on those bytes right now, which only a read
21
+ * can say;
22
+ * - the bottom is what nobody is approaching: behind a viewer moving
23
+ * forward, and the whole film of a viewer who has stopped the picture.
24
+ * Wanted only if somebody seeks back, which is {@link Urgency.BEHIND};
25
+ * - one above the bottom is the far tail — wanted for certain if the
26
+ * viewer watches on, and not before: {@link Urgency.TAIL};
27
+ * - everything between is the lead being built: {@link Urgency.AHEAD}.
28
+ *
29
+ * Read from the scale's own ends rather than from the highest and lowest
30
+ * number in THIS file's map. The two speculative levels are withheld across
31
+ * every torrent at once while anything urgent is missing anywhere, so a
32
+ * paused viewer's film has to compare as wanted-last against another film
33
+ * somebody is watching — and relative to itself alone it would compare as
34
+ * the most urgent thing there is.
35
+ *
36
+ * @param {{ priority?: number, behind?: boolean, deferred?: boolean, urgent?: boolean }} zone
37
+ * @returns {number}
38
+ */
39
+ export function levelOfMapZone(zone) {
40
+ // Read through the map's own words rather than by comparing its numbers.
41
+ // The scale is that layer's, and the numbers inside a band mean nothing
42
+ // but their order.
43
+ const priority = zone.priority ?? 0;
44
+ if (zone.behind === true || (zone.behind === undefined && isBehindEverybody(priority))) {
45
+ return Urgency.BEHIND;
46
+ }
47
+ if (zone.deferred === true || (zone.deferred === undefined && isNobodyComingNow(priority))) {
48
+ // In front of somebody who has stopped the picture, and of nobody who is
49
+ // watching. Wanted, and wanted after everyone who is on their way.
50
+ return Urgency.TAIL;
51
+ }
52
+ return (zone.urgent === true || (zone.urgent === undefined && isAtAWatchingViewer(priority))) ? Urgency.NEAR : Urgency.AHEAD;
53
+ }
54
+
55
+ /** The priority a zone is stated at, within the register's scale. */
56
+ export function priorityOfMapZone(zone) {
57
+ return Number.isFinite(zone.priority) ? Math.max(1, Math.min(100, zone.priority)) : 1;
58
+ }
59
+
60
+ /** When a zone's bytes are needed, as an instant; `now` reads `withinSeconds`. */
61
+ export function deadlineOfMapZone(zone, now = Date.now()) {
62
+ return Number.isFinite(zone.deadlineAt) ? zone.deadlineAt :
63
+ Number.isFinite(zone.withinSeconds) ? now + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY;
64
+ }
65
+
66
+ /**
67
+ * The zones with every repeat of the same bytes left out but the one the swarm
68
+ * would rank first.
69
+ *
70
+ * Each piece is fetched in the band of the best zone that holds it
71
+ * ({@link compareBands}), so a zone naming exactly the bytes of a better one
72
+ * changes nothing the swarm is told. The ranges an AVI's picture reads at open
73
+ * — its declarations and its indexes — are named again in the zone of every
74
+ * segment: field 2026-10-10 (torrent-tv/meta#166), maps of 9700 zones, for a
75
+ * film of 2035 segments, cost half a second each to deliver.
76
+ *
77
+ * @template {{ byteStart: number, byteEnd: number }} Zone
78
+ * @param {Zone[]} zones
79
+ * @param {number} [now]
80
+ * @returns {Zone[]} In their order, without the repeats.
81
+ */
82
+ export function withoutRepeatedZones(zones, now = Date.now()) {
83
+ const bandOf = zone => ({ urgency: levelOfMapZone(zone), priority: priorityOfMapZone(zone), deadlineAt: deadlineOfMapZone(zone, now) });
84
+ const best = new Map();
85
+ zones.forEach((zone, index) => {
86
+ const key = `${zone.byteStart}:${zone.byteEnd}`;
87
+ const kept = best.get(key);
88
+ if (!kept || compareBands(bandOf(zone), kept.band) < 0) best.set(key, { index, band: bandOf(zone) });
89
+ });
90
+ return zones.filter((zone, index) => best.get(`${zone.byteStart}:${zone.byteEnd}`).index === index);
91
+ }
@@ -20,7 +20,7 @@ import { demandProgress } from "./demand-progress.js";
20
20
  import { Urgency, urgencyName } from "./demand/index.js";
21
21
  import { demandFor, firstClassStatement, forgetTorrent, reconcileAll, hasUnmetDemand } from "./download/registry.js";
22
22
  import { withdrawClaim } from "./download/withdraw-claim.js";
23
- import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "../viewer/PriorityMap.js";
23
+ import { deadlineOfMapZone, levelOfMapZone, priorityOfMapZone } from "./map-zones.js";
24
24
  import { deriveSourceKey } from "../../utils/torrent-source-key.js";
25
25
  import { noteTorrentDestroyed } from "./destroyed-torrents.js";
26
26
 
@@ -1290,48 +1290,9 @@ export class TorrentPool {
1290
1290
  .filter((zone) => Number.isSafeInteger(zone?.byteStart) && Number.isSafeInteger(zone?.byteEnd) &&
1291
1291
  zone.byteStart >= 0 && zone.byteEnd >= zone.byteStart && zone.byteStart < length)
1292
1292
  .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
1293
- /**
1294
- * Which level of urgency one zone is stated at, from the map's own number
1295
- * and nothing else. The map's numbers are a scale as long as the film needs
1296
- * — ten bands on a fifty-minute film — while the register has five levels,
1297
- * and the fit is by meaning:
1298
- *
1299
- * - the top of the scale is where a viewer is standing, so it is the
1300
- * cushion being built: {@link Urgency.NEAR}. Never BLOCKED — that level
1301
- * means a reader is stopped on those bytes right now, which only a read
1302
- * can say;
1303
- * - the bottom is what nobody is approaching: behind a viewer moving
1304
- * forward, and the whole film of a viewer who has stopped the picture.
1305
- * Wanted only if somebody seeks back, which is {@link Urgency.BEHIND};
1306
- * - one above the bottom is the far tail — wanted for certain if the
1307
- * viewer watches on, and not before: {@link Urgency.TAIL};
1308
- * - everything between is the lead being built: {@link Urgency.AHEAD}.
1309
- *
1310
- * Read from the scale's own ends rather than from the highest and lowest
1311
- * number in THIS file's map. The two speculative levels are withheld across
1312
- * every torrent at once while anything urgent is missing anywhere, so a
1313
- * paused viewer's film has to compare as wanted-last against another film
1314
- * somebody is watching — and relative to itself alone it would compare as
1315
- * the most urgent thing there is.
1316
- *
1317
- * @param {{ priority?: number }} zone
1318
- * @returns {number}
1319
- */
1320
- const levelOf = (zone) => {
1321
- // Read through the map's own words rather than by comparing its numbers.
1322
- // The scale is that layer's, and the numbers inside a band mean nothing
1323
- // but their order.
1324
- const priority = zone.priority ?? 0;
1325
- if (zone.behind === true || (zone.behind === undefined && isBehindEverybody(priority))) {
1326
- return Urgency.BEHIND;
1327
- }
1328
- if (zone.deferred === true || (zone.deferred === undefined && isNobodyComingNow(priority))) {
1329
- // In front of somebody who has stopped the picture, and of nobody who is
1330
- // watching. Wanted, and wanted after everyone who is on their way.
1331
- return Urgency.TAIL;
1332
- }
1333
- return (zone.urgent === true || (zone.urgent === undefined && isAtAWatchingViewer(priority))) ? Urgency.NEAR : Urgency.AHEAD;
1334
- };
1293
+ // Which level each zone is stated at: `map-zones.js`, where the main thread
1294
+ // reads the same rule before it sends a map.
1295
+ const levelOf = levelOfMapZone;
1335
1296
  ordered.forEach((zone, index) => {
1336
1297
  // Only ranges declared by the container index or metadata read are wanted.
1337
1298
  const byteStart = zone.byteStart;
@@ -1345,10 +1306,9 @@ export class TorrentPool {
1345
1306
  byteStart,
1346
1307
  byteEnd,
1347
1308
  urgency: levelOf(zone),
1348
- priority: Number.isFinite(zone.priority) ? Math.max(1, Math.min(100, zone.priority)) : 1,
1309
+ priority: priorityOfMapZone(zone),
1349
1310
  order: Number.isSafeInteger(zone.order) && zone.order >= 0 ? zone.order : 0,
1350
- deadlineAt: Number.isFinite(zone.deadlineAt) ? zone.deadlineAt :
1351
- Number.isFinite(zone.withinSeconds) ? Date.now() + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY,
1311
+ deadlineAt: deadlineOfMapZone(zone),
1352
1312
  requestId: zone.requestId ?? `${torrent.infoHash}:${fileIndex}:map`
1353
1313
  });
1354
1314
  });