@torrent-tv/proxy 2.80.13 → 2.80.15
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 +22 -0
- package/docs/download-architecture.md +42 -0
- package/docs/encode-architecture.md +87 -0
- package/package.json +1 -1
- package/routes/api/transcode-sessions/net-report/post.js +64 -53
- package/services/download/SwarmSelection.js +20 -4
- package/services/encode/EncodePlan.js +181 -40
- package/services/encode/SegmentStore.js +743 -718
- package/services/encode/run-costs.js +54 -3
- package/services/hls-session-manager.js +13 -14
- package/services/output/LiveOutputs.js +47 -1
- package/services/output/playlists.js +86 -11
- package/services/output/rates.js +125 -0
- package/services/piece-store/piece-lru.js +9 -1
- package/services/priority/PriorityOrchestrator.js +284 -278
- package/services/supply-margin.js +40 -1
- package/services/torrent-worker/piece-reader.js +9 -4
- package/services/torrent-worker/worker.js +5 -0
- package/services/viewer/Viewer.js +345 -297
- package/test/declared-rates.test.js +116 -0
- package/test/encode-plan.test.js +9 -3
- package/test/move-cost.test.js +138 -0
- package/test/piece-lru.test.js +5 -1
- package/test/supply-margin.test.js +25 -8
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What the master playlist declares a variant carries.
|
|
3
|
+
*
|
|
4
|
+
* Measured, and the measurement was available all along: it used to be
|
|
5
|
+
* `height * height * 3.2`, justified by a comment saying no measurement existed
|
|
6
|
+
* before encoding starts. Field 2026-09-08: 3.73 Mbit/s declared for a file
|
|
7
|
+
* carrying 18.4, and the browser sizes its cushion in BYTES from that figure —
|
|
8
|
+
* 120 s asked bought 26 s of film, and the deepest it ever held was 17.1 s.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { declaredRates } from "../services/output/rates.js";
|
|
14
|
+
import { bitrateFor } from "../services/output/playlists.js";
|
|
15
|
+
|
|
16
|
+
// The field file: 2 806 246 976 bytes over 20:18, cut into 291 pieces.
|
|
17
|
+
const FIELD = { fileLength: 2_806_246_976, durationSeconds: 1218.73 };
|
|
18
|
+
|
|
19
|
+
test("the average is the file's own length over its duration", () => {
|
|
20
|
+
const { averageBitsPerSecond } = declaredRates(FIELD);
|
|
21
|
+
|
|
22
|
+
// 18.4 Mbit/s, against the 3.73 that was declared.
|
|
23
|
+
assert.ok(
|
|
24
|
+
Math.abs(averageBitsPerSecond / 1_000_000 - 18.42) < 0.01,
|
|
25
|
+
`got ${averageBitsPerSecond}`
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("nothing measured is stated as nothing, not as a guess", () => {
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
declaredRates({ fileLength: 0, durationSeconds: 0 }),
|
|
32
|
+
{ averageBitsPerSecond: 0, peakOverAverage: 1 }
|
|
33
|
+
);
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
declaredRates({ fileLength: 100, durationSeconds: 0 }),
|
|
36
|
+
{ averageBitsPerSecond: 0, peakOverAverage: 1 }
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("the peak comes from the biggest piece made and the span it covers", () => {
|
|
41
|
+
// The field peak: 38.4 MB over 4.2 s is 73 Mbit/s, four times the average.
|
|
42
|
+
const boundaries = Array.from({ length: 292 }, (_, index) => index * 4.2);
|
|
43
|
+
const { averageBitsPerSecond, peakOverAverage } = declaredRates({
|
|
44
|
+
...FIELD,
|
|
45
|
+
largest: { index: 2, size: 40_288_299 },
|
|
46
|
+
boundaries
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const peak = averageBitsPerSecond * peakOverAverage;
|
|
50
|
+
assert.ok(Math.abs(peak / 1_000_000 - 76.7) < 0.5, `got ${peak / 1_000_000}Mbit/s`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("no piece made yet means the peak is not yet measured, so it equals the average", () => {
|
|
54
|
+
// A ratio invented meanwhile is exactly the fabrication this replaced.
|
|
55
|
+
assert.equal(declaredRates(FIELD).peakOverAverage, 1);
|
|
56
|
+
assert.equal(
|
|
57
|
+
declaredRates({ ...FIELD, largest: { index: -1, size: 0 }, boundaries: [0, 4] }).peakOverAverage,
|
|
58
|
+
1
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the peak is never below the average", () => {
|
|
63
|
+
// A short session whose only piece happens to be a small one must not lower
|
|
64
|
+
// the figure the player sizes its cushion from.
|
|
65
|
+
const boundaries = Array.from({ length: 292 }, (_, index) => index * 4.2);
|
|
66
|
+
assert.equal(
|
|
67
|
+
declaredRates({ ...FIELD, largest: { index: 5, size: 1000 }, boundaries }).peakOverAverage,
|
|
68
|
+
1
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a smaller picture is declared at the pixel share of the source's rate", () => {
|
|
73
|
+
const measured = 18_420_000;
|
|
74
|
+
|
|
75
|
+
assert.equal(
|
|
76
|
+
bitrateFor({ averageBitsPerSecond: measured, height: 1080, sourceHeight: 1080 }),
|
|
77
|
+
Math.round(measured),
|
|
78
|
+
"the source's own height carries the source's bits"
|
|
79
|
+
);
|
|
80
|
+
assert.equal(
|
|
81
|
+
bitrateFor({ averageBitsPerSecond: measured, height: 2160, sourceHeight: 1080 }),
|
|
82
|
+
Math.round(measured),
|
|
83
|
+
"and a height above it carries no more — there is nothing to upscale from"
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
bitrateFor({ averageBitsPerSecond: measured, height: 540, sourceHeight: 1080 }),
|
|
87
|
+
Math.round(measured / 4),
|
|
88
|
+
"half the height is a quarter of the pixels"
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a capped height is declared at its cap, which is exact", () => {
|
|
93
|
+
assert.equal(
|
|
94
|
+
bitrateFor({ averageBitsPerSecond: 18_420_000, height: 720, sourceHeight: 1080, capKbps: 2500 }),
|
|
95
|
+
2_500_000,
|
|
96
|
+
"what we impose is known, not estimated"
|
|
97
|
+
);
|
|
98
|
+
assert.equal(
|
|
99
|
+
bitrateFor({ averageBitsPerSecond: 1_000_000, height: 1080, sourceHeight: 1080, capKbps: 99_000 }),
|
|
100
|
+
1_000_000,
|
|
101
|
+
"and a cap above what the source carries takes nothing away from it"
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the floor is a floor on what may be declared, not a belief about content", () => {
|
|
106
|
+
assert.equal(
|
|
107
|
+
bitrateFor({ averageBitsPerSecond: 0, height: 1080, sourceHeight: 1080 }),
|
|
108
|
+
400_000,
|
|
109
|
+
"a file whose rate is not known yet"
|
|
110
|
+
);
|
|
111
|
+
assert.equal(
|
|
112
|
+
bitrateFor({ averageBitsPerSecond: 18_420_000, height: 4, sourceHeight: 1080 }),
|
|
113
|
+
400_000,
|
|
114
|
+
"and a variant so small the arithmetic would go under it"
|
|
115
|
+
);
|
|
116
|
+
});
|
package/test/encode-plan.test.js
CHANGED
|
@@ -367,9 +367,15 @@ test("the one machine goes to whoever is due soonest, not to the smallest number
|
|
|
367
367
|
...HOST,
|
|
368
368
|
maxRuns: 2
|
|
369
369
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
// WHERE the machine ends up, not how it got there. A run standing at #900
|
|
371
|
+
// reaches nothing anybody wants, so it is taken to #500 rather than killed and
|
|
372
|
+
// replaced — one process instead of a death and a cold start, which is
|
|
373
|
+
// strictly better and is what the plan now answers.
|
|
374
|
+
const placed = actions
|
|
375
|
+
.filter((action) => action.type === "start" || action.type === "move")
|
|
376
|
+
.map((action) => action.from);
|
|
377
|
+
|
|
378
|
+
assert.deepEqual(placed, [500], "the one machine goes where somebody is stopped");
|
|
373
379
|
});
|
|
374
380
|
|
|
375
381
|
test("two viewers far apart are both served, by however many encoders serve them soonest", () => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What moving a running encoder costs, and what the plan does while
|
|
3
|
+
* nobody has measured it.
|
|
4
|
+
*
|
|
5
|
+
* Field 2026-09-08: 39 moves in one session, 24 of them between three adjacent
|
|
6
|
+
* numbers — #58 to #59, #59 to #58, #58 to #60, #60 to #58, six times each,
|
|
7
|
+
* about 0.8 s apart — while the viewer's picture stood still for 116.7 s in
|
|
8
|
+
* three interruptions, the worst of them 91.8 s. The zone the viewer's own
|
|
9
|
+
* position defines slides forward one number at a time, and every slide made
|
|
10
|
+
* standing one number behind it score worse than standing in it.
|
|
11
|
+
*
|
|
12
|
+
* FOUR THINGS, and the first is the one that mattered.
|
|
13
|
+
*
|
|
14
|
+
* 1. The map states `withinSeconds: null` for the film BEHIND the viewers —
|
|
15
|
+
* nobody is waiting there. `deadlineReaderFor` read it through `Number()`,
|
|
16
|
+
* where `null` is 0, so that film was due NOW and was the most urgent
|
|
17
|
+
* material in the file. It bought encoders and it took the run standing in
|
|
18
|
+
* front of the viewer, because that run was the nearest body to it.
|
|
19
|
+
* 2. Residual work took a live run. A zone with no deadline is done with
|
|
20
|
+
* capacity that is left over, and a run already serving a viewer is not left
|
|
21
|
+
* over.
|
|
22
|
+
* 3. `moveSec` was 0 until measured, so a move was free in the arithmetic — and
|
|
23
|
+
* the blindness sustained itself, because the first-output figure takes a
|
|
24
|
+
* reading only from a run that produced something and every run in a thrash
|
|
25
|
+
* is killed before it finishes anything.
|
|
26
|
+
* 4. A staying run was priced as though it would produce instantly, however
|
|
27
|
+
* recently it had started, so the score had no memory of a decision it was
|
|
28
|
+
* still carrying out.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import test from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { RunCosts } from "../services/encode/run-costs.js";
|
|
34
|
+
import { planEncoders } from "../services/encode/EncodePlan.js";
|
|
35
|
+
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
36
|
+
|
|
37
|
+
test("nothing measured means a move is refused, not priced at zero", () => {
|
|
38
|
+
const costs = new RunCosts();
|
|
39
|
+
|
|
40
|
+
const { moveCostSec, firstByteWaitSec, killCostSec } = costs.seconds();
|
|
41
|
+
assert.equal(moveCostSec, Number.POSITIVE_INFINITY, "moving is not free while unpriced");
|
|
42
|
+
// Placing one where there is none is the OTHER question, and it has no
|
|
43
|
+
// alternative: the film gets made or it does not.
|
|
44
|
+
assert.equal(firstByteWaitSec, 0, "placing an encoder is not blocked by an unknown price");
|
|
45
|
+
assert.equal(killCostSec, 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a run killed before producing anything is a lower bound on the first output", () => {
|
|
49
|
+
const costs = new RunCosts();
|
|
50
|
+
|
|
51
|
+
// Exactly what a thrash supplies: a run that lived 800 ms and finished
|
|
52
|
+
// nothing. It says the first output takes AT LEAST that long, which is a fact.
|
|
53
|
+
costs.note({ livedMs: 800, dyingMs: 40 });
|
|
54
|
+
|
|
55
|
+
const { moveCostSec } = costs.seconds();
|
|
56
|
+
assert.ok(Number.isFinite(moveCostSec), "one killed run is enough to stop the blindness");
|
|
57
|
+
assert.ok(Math.abs(moveCostSec - 0.84) < 0.001, `got ${moveCostSec}`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a run that produced something is measured by its first output, not its life", () => {
|
|
61
|
+
const costs = new RunCosts();
|
|
62
|
+
|
|
63
|
+
costs.note({ livedMs: 60_000, firstOutputMs: 900, dyingMs: 100 });
|
|
64
|
+
|
|
65
|
+
const { moveCostSec, firstByteWaitSec } = costs.seconds();
|
|
66
|
+
assert.ok(Math.abs(firstByteWaitSec - 0.9) < 0.001, `got ${firstByteWaitSec}`);
|
|
67
|
+
assert.ok(Math.abs(moveCostSec - 1.0) < 0.001, `got ${moveCostSec}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("the zone sliding one number does not move an encoder that is already reaching it", () => {
|
|
71
|
+
// The field shape exactly: a run standing at #58 with the viewer's urgent zone
|
|
72
|
+
// sliding #58..#59 → #59..#60. Driving through one segment costs the encoder a
|
|
73
|
+
// fraction of a second; moving costs a kill and a cold start.
|
|
74
|
+
const coverage = new CoverageMap();
|
|
75
|
+
coverage.setSegmentCount(482);
|
|
76
|
+
const run = { from: 58, to: 481, head: 58, speedX: 4.45, isAlive: true };
|
|
77
|
+
coverage.claim(run, 58, 481);
|
|
78
|
+
|
|
79
|
+
const actions = planEncoders({
|
|
80
|
+
coverage,
|
|
81
|
+
windows: [
|
|
82
|
+
{ from: 0, to: 57, priority: 1, withinSeconds: null, behind: true },
|
|
83
|
+
{ from: 59, to: 60, priority: 100, withinSeconds: 0, behind: false }
|
|
84
|
+
],
|
|
85
|
+
runs: [run],
|
|
86
|
+
maxRuns: 3,
|
|
87
|
+
segmentSeconds: 4.2,
|
|
88
|
+
speedX: 4.45,
|
|
89
|
+
// Measured on this host: killing takes 40 ms, a fresh encoder's first piece
|
|
90
|
+
// 900 ms. Against that, driving one segment at 4.45x costs 0.94 s — so the
|
|
91
|
+
// two are close, and what settles it is that the move ALSO has to encode
|
|
92
|
+
// the same segment afterwards.
|
|
93
|
+
killCostSec: 0.04,
|
|
94
|
+
firstByteWaitSec: 0.9,
|
|
95
|
+
moveCostSec: 0.94,
|
|
96
|
+
refetchSecPerFilmSecond: 0,
|
|
97
|
+
contentionPenaltyFor: () => 1
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(
|
|
101
|
+
actions.filter((one) => one.type === "move"),
|
|
102
|
+
[],
|
|
103
|
+
"a run one number behind the zone is already on its way into it"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a move that genuinely saves the viewer time still happens", () => {
|
|
108
|
+
// The other half: the viewer jumped fourteen segments ahead, and driving there
|
|
109
|
+
// at 4.45x would take 13 s while a cold start takes 0.94 s. Refusing this
|
|
110
|
+
// would be the opposite fault.
|
|
111
|
+
const coverage = new CoverageMap();
|
|
112
|
+
coverage.setSegmentCount(482);
|
|
113
|
+
const run = { from: 0, to: 481, head: 44, speedX: 4.45, isAlive: true };
|
|
114
|
+
coverage.claim(run, 0, 481);
|
|
115
|
+
|
|
116
|
+
const actions = planEncoders({
|
|
117
|
+
coverage,
|
|
118
|
+
windows: [{ from: 58, to: 59, priority: 100, withinSeconds: 0, behind: false }],
|
|
119
|
+
runs: [run],
|
|
120
|
+
maxRuns: 3,
|
|
121
|
+
segmentSeconds: 4.2,
|
|
122
|
+
speedX: 4.45,
|
|
123
|
+
killCostSec: 0.04,
|
|
124
|
+
firstByteWaitSec: 0.9,
|
|
125
|
+
moveCostSec: 0.94,
|
|
126
|
+
refetchSecPerFilmSecond: 0,
|
|
127
|
+
contentionPenaltyFor: () => 1
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// WHERE the encoder ends up, not how it got there: the run at #44 reaches
|
|
131
|
+
// nothing anybody waits for, so the plan may either take it to #58 or stop it
|
|
132
|
+
// and start one there. Both are one process at #58, and which is cheaper is
|
|
133
|
+
// the measured difference between a kill and a cold start.
|
|
134
|
+
const placed = actions
|
|
135
|
+
.filter((one) => one.type === "move" || one.type === "start")
|
|
136
|
+
.map((one) => one.from);
|
|
137
|
+
assert.deepEqual(placed, [58], "fourteen segments of driving is worth a cold start");
|
|
138
|
+
});
|
package/test/piece-lru.test.js
CHANGED
|
@@ -199,6 +199,10 @@ test("the demand is the union of the readers' windows, not their sum", () => {
|
|
|
199
199
|
|
|
200
200
|
const demand = lru.demand();
|
|
201
201
|
assert.equal(demand.readers, 2);
|
|
202
|
+
// NAMED, because the count read as five encoders on a session that had two:
|
|
203
|
+
// four of the five were zones of the priority map, and only the names could
|
|
204
|
+
// say so.
|
|
205
|
+
assert.deepEqual(demand.names, ["audio", "video"]);
|
|
202
206
|
assert.equal(demand.unionPieces, 80, "100..179 is eighty pieces, not a hundred");
|
|
203
207
|
assert.equal(demand.widestPieces, 50);
|
|
204
208
|
assert.equal(demand.capacity, 88);
|
|
@@ -211,7 +215,7 @@ test("the demand is the union of the readers' windows, not their sum", () => {
|
|
|
211
215
|
lru.unprotect("video");
|
|
212
216
|
assert.deepEqual(
|
|
213
217
|
lru.demand(),
|
|
214
|
-
{ readers: 0, unionPieces: 0, widestPieces: 0, capacity: 88 },
|
|
218
|
+
{ readers: 0, names: [], unionPieces: 0, widestPieces: 0, capacity: 88 },
|
|
215
219
|
"no reader asking for anything is not the same as asking for one piece"
|
|
216
220
|
);
|
|
217
221
|
});
|
|
@@ -50,10 +50,22 @@ test("the margin is what the supply's own interruptions demand", () => {
|
|
|
50
50
|
// derivation calls T: `(v - 1) x T > W` prices what is GAINED between
|
|
51
51
|
// interruptions, and nothing is gained during one.
|
|
52
52
|
assert.ok(Math.abs(answer.medianIntervalSec - 0.73) < 0.001, `got ${answer.medianIntervalSec}`);
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
53
|
+
// THE SHARE OF ITS TIME THE SUPPLY LOST, over whole cycles: from the first
|
|
54
|
+
// interruption's start to the last one's, 12.77 s, of which 9.12 s was
|
|
55
|
+
// interruption. So the reading delivered for 3.65 s of every 12.77, and
|
|
56
|
+
// producing film at that share costs 1/(1 - 0.714) = 3.50x.
|
|
57
|
+
//
|
|
58
|
+
// The formula it replaced divided the WORST interruption by the TYPICAL gap —
|
|
59
|
+
// 1 + 3.16/0.73 = 5.33 — which asks what would happen if the worst recurred
|
|
60
|
+
// at the typical rate, a case that never occurred in this data, and which
|
|
61
|
+
// divides by a gap that goes to zero whenever interruptions arrive in a burst.
|
|
62
|
+
// Field 2026-09-08: 0.79 s over 0.01 s gave 158.60x on a file already
|
|
63
|
+
// downloaded whole, and every quality step was refused against it.
|
|
64
|
+
//
|
|
65
|
+
// The failure this file was written for is still caught: a step admitted at
|
|
66
|
+
// 1.5 ran at 1.05x and stalled, and 3.91x refuses 1.5 exactly as 5.33x did.
|
|
67
|
+
assert.ok(Math.abs(answer.lostShare - 0.7142) < 0.001, `got ${answer.lostShare}`);
|
|
68
|
+
assert.ok(Math.abs(answer.requiredSpeed - 3.4986) < 0.001, `got ${answer.requiredSpeed}`);
|
|
57
69
|
});
|
|
58
70
|
|
|
59
71
|
test("one stall seen by three readers is one interruption, not three", () => {
|
|
@@ -87,7 +99,11 @@ test("overlapping waits merge, and the gap between stalls is what is left", () =
|
|
|
87
99
|
assert.equal(answer.waits, 4, "from four waits");
|
|
88
100
|
assert.equal(answer.worstWaitSec, 10, "the merged stall, not one reader's view of it");
|
|
89
101
|
assert.equal(answer.medianIntervalSec, 10, "20s to 30s is when the encoder ran");
|
|
90
|
-
|
|
102
|
+
// One whole cycle: 10 s to 30 s, of which the 10 s interruption is half. The
|
|
103
|
+
// supply delivered for the other half, so a step must run at twice realtime.
|
|
104
|
+
assert.equal(answer.spanSec, 20);
|
|
105
|
+
assert.equal(answer.lostSec, 10);
|
|
106
|
+
assert.equal(answer.requiredSpeed, 2);
|
|
91
107
|
});
|
|
92
108
|
|
|
93
109
|
test("a copy at 8x clears its own supply with room to spare", () => {
|
|
@@ -95,9 +111,10 @@ test("a copy at 8x clears its own supply with room to spare", () => {
|
|
|
95
111
|
const waits = evenlySpaced(6, 15.5, 4.82);
|
|
96
112
|
const answer = requiredSpeedFrom(waits);
|
|
97
113
|
assert.ok(answer);
|
|
98
|
-
// Waits end 15.5 s apart and last 4.82 s, so the
|
|
99
|
-
//
|
|
100
|
-
// step a stranded viewer is always
|
|
114
|
+
// Waits end 15.5 s apart and last 4.82 s, so the supply delivers for 10.68 s
|
|
115
|
+
// of every 15.5: 0.311 of its time lost, and 1/(1 - 0.311) = 1.45 against the
|
|
116
|
+
// 8x measured. Which is why a copy is the step a stranded viewer is always
|
|
117
|
+
// able to return to.
|
|
101
118
|
assert.ok(answer.requiredSpeed < 1.5, `got ${answer.requiredSpeed}`);
|
|
102
119
|
});
|
|
103
120
|
|