@torrent-tv/proxy 2.96.11 → 2.96.13
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
package/server.js
CHANGED
|
@@ -423,7 +423,14 @@ export async function startProxyServer({
|
|
|
423
423
|
/** Source byte ranges of one segment interval, by file and interval; forgotten with the source. */
|
|
424
424
|
const segmentSourceRanges = new Map();
|
|
425
425
|
const downloadMaps = new DownloadMaps({
|
|
426
|
-
|
|
426
|
+
// The ranges a picture reads at open are named again by every segment;
|
|
427
|
+
// only the zone the swarm would rank first is sent (torrent-tv/meta#166).
|
|
428
|
+
// Left out of what is SENT and nowhere else: every segment's own zones are
|
|
429
|
+
// also what says which bytes that segment needs, and a segment whose zones
|
|
430
|
+
// all repeat another's — two of one group of pictures in an AVI read the
|
|
431
|
+
// same bytes — lost them with 2.96.11, and the start forecast answered
|
|
432
|
+
// "source-input-ranges-unavailable" for ever (Home Assistant 2026-10-11).
|
|
433
|
+
publish: (map) => torrentPool.setPriorityMap({ ...map, zones: withoutRepeatedZones(map.zones) }),
|
|
427
434
|
log: (line) => logger.info(line),
|
|
428
435
|
resolvePlayback: async (map) => {
|
|
429
436
|
if (map.zones.length === 0) return [];
|
|
@@ -534,9 +541,7 @@ export async function startProxyServer({
|
|
|
534
541
|
segmentSourceRanges.set(rangesKey, input.ranges);
|
|
535
542
|
convert(input.ranges);
|
|
536
543
|
}
|
|
537
|
-
|
|
538
|
-
// only the zone the swarm would rank first is sent (torrent-tv/meta#166).
|
|
539
|
-
return withoutRepeatedZones(converted);
|
|
544
|
+
return converted;
|
|
540
545
|
}
|
|
541
546
|
});
|
|
542
547
|
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
|
|
|
@@ -807,7 +812,7 @@ export class EncodeRun {
|
|
|
807
812
|
// is moved either way — but nothing was resumed, and saying so is what
|
|
808
813
|
// stops a dead run being reported as producing again.
|
|
809
814
|
const continued = this.#signal("SIGCONT");
|
|
810
|
-
this.#transition(ENCODE_RUN_EVENT.
|
|
815
|
+
this.#transition(ENCODE_RUN_EVENT.RESUME_ORDERED);
|
|
811
816
|
this.clock.continued();
|
|
812
817
|
return continued;
|
|
813
818
|
}
|
|
@@ -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
|