@torrent-tv/proxy 2.76.3 → 2.76.5
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 +11 -0
- package/biome.json +32 -0
- package/package.json +1 -1
- package/services/encode/EncodePlan.js +3 -1
- package/services/encode/EncodeRun.js +21 -1
- package/services/encode/encode-run-state.js +67 -0
- package/services/hls-session-manager.js +80 -540
- package/services/orchestrators/EncodeOrchestrator.js +42 -3
- package/services/quality/EncodeCost.js +500 -0
- package/test/encode-cost.test.js +94 -0
- package/test/encode-orchestrator.test.js +31 -0
- package/test/encode-plan.test.js +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 2.76.5
|
|
2
|
+
|
|
3
|
+
- **Chore**: What encoding costs this machine is a class of its own (`services/quality/EncodeCost.js`), the third part taken out of the session manager. One subject — seconds of work per second of video — asked about a picture being re-encoded, a soundtrack, a copy, everything running beside the rung being judged, and turned into the list of heights the machine can hold. It holds what an encoder taught it, since it is the only thing that reads those figures, and it is given what it cannot work out for itself: which sessions belong to one file, the host's own readings, how many encoders are running, and what the file costs merely by being fetched. The host is asked at the moment of the question rather than copied when the object is made — the share of the machine that is free is re-read every few seconds, and a copy would price every later rung against a machine that has gone. The manager keeps the three methods that LEARN those costs; moving them is the next step.
|
|
4
|
+
- **Fix**: A rung measured running below realtime is withdrawn even on a host whose startup benchmark is missing. That measurement is the strongest evidence there is about a rung — it ran, and it could not keep up — and it was being discarded along with the prediction it does not depend on. Found by a check written while the arithmetic was being moved.
|
|
5
|
+
- **Chore**: What a session's encoding is doing, as one state, is answered where the run states are defined (`encode/encode-run-state.js`) instead of by three functions in the manager. The boundary of the new directory is a lint rule, like every layer before it, and the rule was checked to fire rather than assumed to.
|
|
6
|
+
|
|
7
|
+
## 2.76.4
|
|
8
|
+
|
|
9
|
+
- **Fix**: An encoder was started and killed every five seconds, each one producing 0-2 segments, for as long as anybody watched. Measured on the addon host: a run given no end carries a `to` below its `from` — which is how "no end" is written everywhere here — and two places read that as a number instead. The plan's test for "is anybody waiting for what this run was given" said no, so it was stopped as unwanted; and the stretch it claimed in the coverage map collapsed to a single segment, so the plan saw the rest of the film as free and started another encoder one number along. The two together are the loop. The rule is stated once now (`endOfRun`) and read from that one place, including where a run's own ending is judged.
|
|
10
|
+
- **Fix**: A run with no end holds the look-ahead in front of it, not the rest of the film. It is what actually bounds one — a run is suspended once it is that far in front of the segment its viewer asked for and produces nothing until somebody asks — and claiming further would leave a viewer who opens the same film further in with no encoder at all, waiting for that run to encode its way there. The rule was already applied where a session plans its own interval and not on the path the plan uses.
|
|
11
|
+
|
|
1
12
|
## 2.76.3
|
|
2
13
|
|
|
3
14
|
- **Fix**: The proxy stopped answering anything — playback, health, its own log — a few seconds after a viewer opened a film, and burned a whole processor doing it. Measured on the addon host with the stack read out of the live process: the look-ahead timer asked the plan where a new encoder could start, and the walk that answers that walked one segment number at a time towards nine quadrillion, scanning every claim at each step. It did that because the map had no length, and the map had no length because the field naming it moved onto the timeline in 2.76.0 while three readers were left on the old name, where every session answers `undefined`. Those three are the whole defect: with them wrong, no run was ever given an end either, so the feature that lets two encoders share one output was inert as well.
|
package/biome.json
CHANGED
|
@@ -49,6 +49,38 @@
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
"includes": ["services/quality/**"],
|
|
54
|
+
"linter": {
|
|
55
|
+
"rules": {
|
|
56
|
+
"style": {
|
|
57
|
+
"noRestrictedImports": {
|
|
58
|
+
"level": "error",
|
|
59
|
+
"options": {
|
|
60
|
+
"patterns": [
|
|
61
|
+
{
|
|
62
|
+
"group": [
|
|
63
|
+
"../hls-session-manager.js",
|
|
64
|
+
"../controllers/**",
|
|
65
|
+
"../orchestrators/**",
|
|
66
|
+
"../container/**",
|
|
67
|
+
"../demand/**",
|
|
68
|
+
"../download/**",
|
|
69
|
+
"../piece-store/**",
|
|
70
|
+
"../segment-formats/**",
|
|
71
|
+
"../torrent-worker/**",
|
|
72
|
+
"../viewer/**",
|
|
73
|
+
"../output/**"
|
|
74
|
+
],
|
|
75
|
+
"message": "What encoding costs is arithmetic over readings. Which sessions belong to one file, how many encoders run, what a file costs to fetch — all of it is passed in, so this can be asked without a session manager, a torrent or a disk."
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
52
84
|
{
|
|
53
85
|
"includes": ["services/download/**"],
|
|
54
86
|
"linter": {
|
package/package.json
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
* own answer — the look-ahead, which suspends a run rather than bounding it.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
import { endOfRun } from "./EncodeRun.js";
|
|
42
|
+
|
|
41
43
|
/**
|
|
42
44
|
* One encoder that is running now.
|
|
43
45
|
*
|
|
@@ -138,7 +140,7 @@ export function planEncoders({
|
|
|
138
140
|
for (const run of live) {
|
|
139
141
|
// 1. Is anybody waiting for what this run was given? A run whose stretch
|
|
140
142
|
// touches no window is making material nobody has asked for.
|
|
141
|
-
const stillWanted = wanted.some((span) => overlaps(run.from, run
|
|
143
|
+
const stillWanted = wanted.some((span) => overlaps(run.from, endOfRun(run), span.from, span.to));
|
|
142
144
|
if (!stillWanted) {
|
|
143
145
|
stops.push({ type: "stop", run, because: "nothing it was given is wanted" });
|
|
144
146
|
continue;
|
|
@@ -63,6 +63,26 @@ const MICROSECONDS_PER_SECOND = 1_000_000;
|
|
|
63
63
|
* @property {string} lastError - The last thing ffmpeg said on stderr.
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The last number this run was given, or Infinity when it was given no end.
|
|
68
|
+
*
|
|
69
|
+
* ONE reading of one convention. "No end" is written as a `to` below `from` —
|
|
70
|
+
* the session layer returns -1 for it and the log prints `#-1` — and every place
|
|
71
|
+
* that has to compare against it read that for itself. One place did not: the
|
|
72
|
+
* coverage map was handed the raw -1, `Math.max(from, -1)` made the claim one
|
|
73
|
+
* segment long, and the plan then saw the rest of the film as free. Field,
|
|
74
|
+
* 2026-09-05: an encoder was started and killed every five seconds, each one
|
|
75
|
+
* producing 0-2 segments, for as long as anybody watched.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ from: number, to: number }} run
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
80
|
+
export function endOfRun(run) {
|
|
81
|
+
const from = Number(run?.from);
|
|
82
|
+
const to = Number(run?.to);
|
|
83
|
+
return Number.isInteger(to) && to >= from ? to : Number.POSITIVE_INFINITY;
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
export class EncodeRun {
|
|
67
87
|
/** @type {import("node:child_process").ChildProcess | null} */
|
|
68
88
|
#process = null;
|
|
@@ -466,7 +486,7 @@ export class EncodeRun {
|
|
|
466
486
|
// one, and the end of the film where it was not. A run told to make #10..#14
|
|
467
487
|
// that exits cleanly at #11 has not finished, whatever the film's length;
|
|
468
488
|
// and a run with no end has nothing but the film to be measured against.
|
|
469
|
-
const endOfWork =
|
|
489
|
+
const endOfWork = Number.isFinite(endOfRun(this)) ? this.to : this.lastSegmentIndex();
|
|
470
490
|
const outcome = classifyEncodeExit({
|
|
471
491
|
code,
|
|
472
492
|
producedThrough: this.#produced.size > 0 ? this.reached : null,
|
|
@@ -486,3 +486,70 @@ export function declaredEdges() {
|
|
|
486
486
|
export function declaredContainment() {
|
|
487
487
|
return Object.entries(PARENT).map(([state, parent]) => ({ state, parent }));
|
|
488
488
|
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Every run this session has going, earliest first.
|
|
492
|
+
*
|
|
493
|
+
* A session holds as many as the machine affords, because one output can be
|
|
494
|
+
* watched from more than one place: a viewer who jumps back gets a run of their
|
|
495
|
+
* own rather than dragging the picture away from a viewer watching ahead.
|
|
496
|
+
*
|
|
497
|
+
* @param {{ runs?: Set<object> }} session
|
|
498
|
+
* @returns {object[]}
|
|
499
|
+
*/
|
|
500
|
+
export function runsOf(session) {
|
|
501
|
+
const runs = session?.runs instanceof Set ? [...session.runs] : [];
|
|
502
|
+
return runs.sort((left, right) => left.from - right.from);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The runs of this session that still have a process.
|
|
507
|
+
*
|
|
508
|
+
* @param {{ runs?: Set<object> }} session
|
|
509
|
+
* @returns {object[]}
|
|
510
|
+
*/
|
|
511
|
+
export function liveRunsOf(session) {
|
|
512
|
+
return runsOf(session).filter((run) => run.isAlive);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* What this session's encoding is doing, as one state.
|
|
517
|
+
*
|
|
518
|
+
* The most advanced state among its live runs: a session with anything
|
|
519
|
+
* producing IS producing, whatever else it has going. A session that has never
|
|
520
|
+
* started a run, or whose runs have all ended, is at the table's own initial
|
|
521
|
+
* state — which is what "nothing is encoding" means.
|
|
522
|
+
*
|
|
523
|
+
* @param {{ runs?: Set<object> }} session
|
|
524
|
+
* @returns {string}
|
|
525
|
+
*/
|
|
526
|
+
export function runStateOf(session) {
|
|
527
|
+
let answer = INITIAL_RUN_STATE;
|
|
528
|
+
let rank = -1;
|
|
529
|
+
for (const run of runsOf(session)) {
|
|
530
|
+
const at = RUN_STATE_ORDER.indexOf(run.state);
|
|
531
|
+
if (at > rank) {
|
|
532
|
+
rank = at;
|
|
533
|
+
answer = run.state;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return answer;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* How advanced a run state is, for reducing several runs to one answer.
|
|
541
|
+
*
|
|
542
|
+
* Producing outranks starting, and both outrank a run that has ended: what a
|
|
543
|
+
* caller asking "what is this session's encoding doing" wants to know is
|
|
544
|
+
* whether anything is being made, not what the quietest of them is up to.
|
|
545
|
+
*/
|
|
546
|
+
const RUN_STATE_ORDER = [
|
|
547
|
+
ENCODE_RUN_STATE.ENDED_FAILED,
|
|
548
|
+
ENCODE_RUN_STATE.ENDED_COMPLETE,
|
|
549
|
+
ENCODE_RUN_STATE.STOPPED,
|
|
550
|
+
ENCODE_RUN_STATE.IDLE,
|
|
551
|
+
ENCODE_RUN_STATE.RETRY_WAIT,
|
|
552
|
+
ENCODE_RUN_STATE.SUSPENDED,
|
|
553
|
+
ENCODE_RUN_STATE.STARTING,
|
|
554
|
+
ENCODE_RUN_STATE.PRODUCING
|
|
555
|
+
];
|