@torrent-tv/proxy 2.80.5 → 2.80.6
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 +32 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +19 -1
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +53 -1
- package/services/encode/open-piece.js +22 -1
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +118 -89
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +40 -0
- package/test/output-speed.test.js +86 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
package/test/encode-plan.test.js
CHANGED
|
@@ -12,6 +12,22 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
13
13
|
import { endOfRun } from "../services/encode/EncodeRun.js";
|
|
14
14
|
import { firstUnmetWant, planEncoders } from "../services/encode/EncodePlan.js";
|
|
15
|
+
import { contentionPenalty, penaltiesFrom } from "../services/contention.js";
|
|
16
|
+
|
|
17
|
+
// WHAT A SECOND ENCODER COSTS THE FIRST — measured, never a formula.
|
|
18
|
+
//
|
|
19
|
+
// Addon host, 2026-09-03: 854x480 through libx264 `ultrafast` ran at 7.12x with
|
|
20
|
+
// the machine to itself, and at 4.20x and 4.16x when two ran at once. The
|
|
21
|
+
// penalty is read off that reading by the SAME two functions production uses,
|
|
22
|
+
// so nothing here invents a shape: beyond what was measured the reading is held
|
|
23
|
+
// rather than extrapolated.
|
|
24
|
+
const MEASURED_PENALTIES = penaltiesFrom(7.12, [{ others: 1, speed: 4.18 }]);
|
|
25
|
+
const penaltyFor = (others) => contentionPenalty(others, MEASURED_PENALTIES).penalty;
|
|
26
|
+
|
|
27
|
+
// What a start and a stop cost, measured on the same host: a spawn with its
|
|
28
|
+
// input open is 0.12 s there.
|
|
29
|
+
const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
|
|
30
|
+
|
|
15
31
|
|
|
16
32
|
/** A host that can afford two encoders, four-second segments, a cheap restart. */
|
|
17
33
|
// A host that has measured itself: the start and the death from its own runs,
|
|
@@ -20,9 +36,15 @@ import { firstUnmetWant, planEncoders } from "../services/encode/EncodePlan.js";
|
|
|
20
36
|
// host missing any of them keeps its encoders instead — which is its own check
|
|
21
37
|
// below rather than the shape every other check is written against.
|
|
22
38
|
const HOST = {
|
|
39
|
+
// Measured on the addon host: a second encoder beside the first costs about
|
|
40
|
+
// half its speed. Without it an extra process is free and the score always
|
|
41
|
+
// wants more of them.
|
|
42
|
+
contentionPenaltyFor: penaltyFor,
|
|
43
|
+
// What the startup benchmark says this host encodes at, in realtimes. It is
|
|
44
|
+
// measured before any viewer exists, so the plan always has a speed.
|
|
45
|
+
speedX: 2,
|
|
23
46
|
maxRuns: 2,
|
|
24
47
|
segmentSeconds: 4,
|
|
25
|
-
restartCostSec: 0.12,
|
|
26
48
|
killCostSec: 0.5,
|
|
27
49
|
firstByteWaitSec: 1,
|
|
28
50
|
refetchSecPerFilmSecond: 0.25
|
|
@@ -57,10 +79,18 @@ test("a viewer waiting on nothing made starts one encoder, at what they are wait
|
|
|
57
79
|
);
|
|
58
80
|
});
|
|
59
81
|
|
|
60
|
-
test("a
|
|
61
|
-
// Found by the orchestrator's own checks
|
|
62
|
-
//
|
|
63
|
-
// to
|
|
82
|
+
test("two viewers a couple of numbers apart are never given the same stretch twice", () => {
|
|
83
|
+
// Found by the orchestrator's own checks. Under the old design the second
|
|
84
|
+
// viewer was given an encoder of their own while the run already there had
|
|
85
|
+
// nowhere left to go — two processes side by side for one stretch of film.
|
|
86
|
+
//
|
|
87
|
+
// The model answers it by arithmetic instead of by a rule: the number the
|
|
88
|
+
// second viewer stands on is due now and the encoder behind them needs six
|
|
89
|
+
// seconds, so it IS late; a new one delivers it in two, so placing one is
|
|
90
|
+
// worth it; and the one behind is then bounded to the two numbers it can still
|
|
91
|
+
// make and retires. One encoder, and the second viewer waits four seconds
|
|
92
|
+
// less. What must never happen — two of them running the same stretch — does
|
|
93
|
+
// not.
|
|
64
94
|
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
65
95
|
const runA = run({ from: 100, to: 999, head: 100 });
|
|
66
96
|
coverage.claim(runA, 100, 999);
|
|
@@ -70,7 +100,20 @@ test("a viewer whose cushion reaches past a running encoder starts nothing", ()
|
|
|
70
100
|
runs: [runA],
|
|
71
101
|
...HOST
|
|
72
102
|
});
|
|
73
|
-
|
|
103
|
+
const spans = actions
|
|
104
|
+
.filter((action) => action.type !== "stop")
|
|
105
|
+
.map((action) => [action.from, action.to])
|
|
106
|
+
.sort((left, right) => left[0] - right[0]);
|
|
107
|
+
for (let index = 0; index < spans.length - 1; index += 1) {
|
|
108
|
+
assert.ok(spans[index][1] < spans[index + 1][0],
|
|
109
|
+
`#${spans[index][0]}..#${spans[index][1]} overlaps #${spans[index + 1][0]}`);
|
|
110
|
+
}
|
|
111
|
+
assert.ok(spans.some(([from, to]) => from <= 102 && (to < from || to >= 102)),
|
|
112
|
+
"somebody is making what the second viewer needs");
|
|
113
|
+
// How MANY is the score's answer and not a rule: with these two standing two
|
|
114
|
+
// numbers apart, a second process makes the one in front wait 1.4 s longer and
|
|
115
|
+
// the one behind 2.5 s less, so the pair is better off. What is fixed is that
|
|
116
|
+
// they never share a stretch, which the loop above asserts.
|
|
74
117
|
});
|
|
75
118
|
|
|
76
119
|
test("a viewer whose whole window is already made starts nothing", () => {
|
|
@@ -95,7 +138,11 @@ test("a run is given an end at the edge of what is free", () => {
|
|
|
95
138
|
assert.deepEqual({ from: actions[0].from, to: actions[0].to }, { from: 40, to: 49 });
|
|
96
139
|
});
|
|
97
140
|
|
|
98
|
-
test("a run that has caught up with made material is
|
|
141
|
+
test("a run that has caught up with made material is not killed for it", () => {
|
|
142
|
+
// It has arrived at film somebody else made. Killing it was the old answer and
|
|
143
|
+
// it is never the right one: the work is either taken past the made stretch or
|
|
144
|
+
// left to drive through, and which of those depends on the score, but the
|
|
145
|
+
// encoder goes on existing either way.
|
|
99
146
|
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
100
147
|
const runA = run({ head: 10, from: 0, to: 100 });
|
|
101
148
|
coverage.claim(runA, 0, 100);
|
|
@@ -108,11 +155,19 @@ test("a run that has caught up with made material is moved forward, not killed",
|
|
|
108
155
|
runs: [runA],
|
|
109
156
|
...HOST
|
|
110
157
|
});
|
|
111
|
-
|
|
112
|
-
assert.ok(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
158
|
+
assert.equal(actions.some((action) => action.type === "stop"), false, "not killed");
|
|
159
|
+
assert.ok(
|
|
160
|
+
actions.some((action) => (action.type === "keep" || action.type === "move") && action.run === runA),
|
|
161
|
+
"it is still one of the encoders on this output"
|
|
162
|
+
);
|
|
163
|
+
// And nothing is arranged so that two of them make the same piece.
|
|
164
|
+
const spans = actions
|
|
165
|
+
.filter((action) => action.type !== "stop")
|
|
166
|
+
.map((action) => [action.from, action.to < action.from ? Number.POSITIVE_INFINITY : action.to])
|
|
167
|
+
.sort((left, right) => left[0] - right[0]);
|
|
168
|
+
for (let index = 0; index < spans.length - 1; index += 1) {
|
|
169
|
+
assert.ok(spans[index][1] < spans[index + 1][0], "the stretches do not overlap");
|
|
170
|
+
}
|
|
116
171
|
});
|
|
117
172
|
|
|
118
173
|
test("a covered stretch shorter than a restart is driven through instead", () => {
|
|
@@ -125,7 +180,9 @@ test("a covered stretch shorter than a restart is driven through instead", () =>
|
|
|
125
180
|
coverage.markReady(10);
|
|
126
181
|
const actions = planEncoders({
|
|
127
182
|
coverage,
|
|
128
|
-
|
|
183
|
+
// From where the run stands, so the only question asked is the covered piece
|
|
184
|
+
// under it. Beginning at #0 would also be asking who makes #0..#9.
|
|
185
|
+
windows: [{ from: 10, to: 90 }],
|
|
129
186
|
runs: [runA],
|
|
130
187
|
...HOST
|
|
131
188
|
});
|
|
@@ -133,20 +190,21 @@ test("a covered stretch shorter than a restart is driven through instead", () =>
|
|
|
133
190
|
assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
|
|
134
191
|
});
|
|
135
192
|
|
|
136
|
-
test("a run
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
193
|
+
test("a run standing on film nobody has made is never taken away", () => {
|
|
194
|
+
// The field failure this guards: 684 starts in 482 seconds on 2026-09-05,
|
|
195
|
+
// because a run was moved whenever anything ahead of it had been made.
|
|
196
|
+
//
|
|
197
|
+
// Moving is priced rather than forbidden — a run standing ON made film may
|
|
198
|
+
// well be worth restarting one number along, and the two checks above measure
|
|
199
|
+
// both sides of that. What can never be worth it is moving a run that has
|
|
200
|
+
// nothing made under it or ahead of it: there is no work to skip, so the move
|
|
201
|
+
// buys nothing and costs a start.
|
|
143
202
|
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
144
|
-
const runA = run({ head: 10, speedX:
|
|
203
|
+
const runA = run({ head: 10, speedX: 2 });
|
|
145
204
|
coverage.claim(runA, 0, 100);
|
|
146
|
-
coverage.markReady(10);
|
|
147
205
|
const actions = planEncoders({
|
|
148
206
|
coverage,
|
|
149
|
-
windows: [{ from:
|
|
207
|
+
windows: [{ from: 10, to: 90 }],
|
|
150
208
|
runs: [runA],
|
|
151
209
|
...HOST
|
|
152
210
|
});
|
|
@@ -154,11 +212,13 @@ test("a run whose speed nothing has measured is kept, not taken away", () => {
|
|
|
154
212
|
assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
|
|
155
213
|
});
|
|
156
214
|
|
|
157
|
-
test("
|
|
158
|
-
// Driving through costs this
|
|
159
|
-
// second time
|
|
160
|
-
//
|
|
161
|
-
//
|
|
215
|
+
test("a long stretch of made film is skipped, and the swarm's price is in the reckoning", () => {
|
|
216
|
+
// Driving through costs this encoder's time AND the swarm the same bytes a
|
|
217
|
+
// second time. Twenty made pieces of 4 s at 1x is 80 s of encoding plus 20 s
|
|
218
|
+
// of fetching, against a move priced at 0.12 + 0.5 + 3 seconds — so it skips.
|
|
219
|
+
//
|
|
220
|
+
// There is no separate comparison to read here: both are seconds, both are in
|
|
221
|
+
// the one score, and the arrangement with the smaller total is the one taken.
|
|
162
222
|
const coverage = new CoverageMap({ segmentCount: 200 });
|
|
163
223
|
const runA = run({ head: 10, speedX: 1 });
|
|
164
224
|
coverage.claim(runA, 0, 200);
|
|
@@ -167,17 +227,25 @@ test("both sides of the move are counted, not just the encoder's own time", () =
|
|
|
167
227
|
}
|
|
168
228
|
const actions = planEncoders({
|
|
169
229
|
coverage,
|
|
170
|
-
|
|
230
|
+
// The viewer is PAST the made stretch, so the only question is this encoder:
|
|
231
|
+
// drive through twenty pieces that exist, or skip them. With a viewer at #0
|
|
232
|
+
// as well, something has to cross that stretch whatever happens, and then
|
|
233
|
+
// moving buys nothing — which the score says too, and is why the window
|
|
234
|
+
// starts where the viewer actually is.
|
|
235
|
+
windows: [{ from: 30, to: 190 }],
|
|
171
236
|
runs: [runA],
|
|
172
237
|
...HOST,
|
|
238
|
+
// One encoder, so the question is only about THIS one: drive through the
|
|
239
|
+
// twenty pieces that exist, or skip them. With room for a second, the
|
|
240
|
+
// machine simply buys one and the question never arises.
|
|
241
|
+
maxRuns: 1,
|
|
173
242
|
killCostSec: 0.5,
|
|
174
243
|
firstByteWaitSec: 3,
|
|
175
244
|
refetchSecPerFilmSecond: 0.25
|
|
176
245
|
});
|
|
177
246
|
const move = actions.find((action) => action.type === "move");
|
|
178
|
-
assert.ok(move);
|
|
179
|
-
assert.
|
|
180
|
-
assert.match(move.because, /against 3\.62s to move/);
|
|
247
|
+
assert.ok(move, "it is taken past the made film rather than left to make it again");
|
|
248
|
+
assert.equal(move.from, 30, "to the first thing nobody has");
|
|
181
249
|
});
|
|
182
250
|
|
|
183
251
|
test("a short covered stretch is driven through rather than paid a restart for", () => {
|
|
@@ -190,7 +258,9 @@ test("a short covered stretch is driven through rather than paid a restart for",
|
|
|
190
258
|
coverage.markReady(10);
|
|
191
259
|
const actions = planEncoders({
|
|
192
260
|
coverage,
|
|
193
|
-
|
|
261
|
+
// From where the run stands, so the only question is the covered piece under
|
|
262
|
+
// it. A window starting at #0 would also be asking who makes #0..#9.
|
|
263
|
+
windows: [{ from: 10, to: 190 }],
|
|
194
264
|
runs: [runA],
|
|
195
265
|
...HOST,
|
|
196
266
|
killCostSec: 0.5,
|
|
@@ -201,11 +271,15 @@ test("a short covered stretch is driven through rather than paid a restart for",
|
|
|
201
271
|
assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
|
|
202
272
|
});
|
|
203
273
|
|
|
204
|
-
test("a run with nothing left
|
|
274
|
+
test("a run with nothing left ahead of it does not go on making nothing", () => {
|
|
275
|
+
// Everything from #10 to the end exists. The encoder standing at #10 has
|
|
276
|
+
// nothing to do there; whether it is stopped or taken back to the film before
|
|
277
|
+
// #0 that nobody has made is the score's answer, and both are right answers.
|
|
278
|
+
// What must not happen is that it stays where it is, producing nothing.
|
|
205
279
|
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
206
280
|
const runA = run({ head: 10 });
|
|
207
281
|
coverage.claim(runA, 0, 100);
|
|
208
|
-
for (let index = 10; index
|
|
282
|
+
for (let index = 10; index < 100; index += 1) {
|
|
209
283
|
coverage.markReady(index);
|
|
210
284
|
}
|
|
211
285
|
const actions = planEncoders({
|
|
@@ -214,10 +288,12 @@ test("a run with nothing left to make ahead of it is stopped", () => {
|
|
|
214
288
|
runs: [runA],
|
|
215
289
|
...HOST
|
|
216
290
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
291
|
+
const kept = actions.find((action) => action.type === "keep" && action.run === runA);
|
|
292
|
+
assert.equal(kept, undefined, "it is not left standing on film that already exists");
|
|
293
|
+
const madeAgain = actions.filter((action) => action.type !== "stop");
|
|
294
|
+
for (const action of madeAgain) {
|
|
295
|
+
assert.ok(action.from < 10, "and whatever is made is film nobody has");
|
|
296
|
+
}
|
|
221
297
|
});
|
|
222
298
|
|
|
223
299
|
test("every encoder stops when nobody is watching the output", () => {
|
|
@@ -274,35 +350,51 @@ test("the same plan run twice on an unchanged state gives the same answer", () =
|
|
|
274
350
|
assert.ok(!first.includes("stop"), "and neither pass kills what the other would start");
|
|
275
351
|
});
|
|
276
352
|
|
|
277
|
-
test("
|
|
353
|
+
test("the one machine goes to whoever is due soonest, not to the smallest number", () => {
|
|
354
|
+
// Urgency is a TIME in this model, so the test states one. The far zone is
|
|
355
|
+
// lower in number and needed sooner; the order must come from the time.
|
|
278
356
|
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
279
357
|
const actions = planEncoders({
|
|
280
358
|
coverage,
|
|
281
|
-
// The far zone is lower in number and lower in priority: the order must
|
|
282
|
-
// come from the priority, not from the number.
|
|
283
359
|
windows: [
|
|
284
|
-
{ from: 0, to: 100, priority: 1 },
|
|
285
|
-
{ from: 500, to: 530, priority: 3 }
|
|
360
|
+
{ from: 0, to: 100, priority: 1, withinSeconds: 600 },
|
|
361
|
+
{ from: 500, to: 530, priority: 3, withinSeconds: 0 }
|
|
286
362
|
],
|
|
287
|
-
|
|
363
|
+
// A run whose speed has been measured: how long an encoder takes to reach a
|
|
364
|
+
// number is the whole comparison, and with nothing measured the model has no
|
|
365
|
+
// ground to prefer one place over another and places once.
|
|
366
|
+
runs: [run({ from: 900, to: 999, head: 900 })],
|
|
288
367
|
...HOST,
|
|
289
|
-
maxRuns:
|
|
368
|
+
maxRuns: 2
|
|
290
369
|
});
|
|
291
370
|
const started = actions.filter((action) => action.type === "start").map((action) => action.from);
|
|
292
371
|
|
|
293
372
|
assert.deepEqual(started, [500], "the one machine goes where somebody is stopped");
|
|
294
373
|
});
|
|
295
374
|
|
|
296
|
-
test("two viewers far apart
|
|
375
|
+
test("two viewers far apart are both served, by however many encoders serve them soonest", () => {
|
|
376
|
+
// Two encoders, or one that goes to whichever of them is worse off: the answer
|
|
377
|
+
// is what a second process costs this machine, which is measured, and the
|
|
378
|
+
// score works it out. What must hold is that neither of them is simply left.
|
|
297
379
|
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
298
380
|
const actions = planEncoders({
|
|
299
381
|
coverage,
|
|
300
|
-
windows: [
|
|
301
|
-
|
|
302
|
-
|
|
382
|
+
windows: [
|
|
383
|
+
{ from: 0, to: 30, withinSeconds: 0 },
|
|
384
|
+
{ from: 800, to: 830, withinSeconds: 0 }
|
|
385
|
+
],
|
|
386
|
+
runs: [run({ from: 900, to: 999, head: 900 })],
|
|
387
|
+
...HOST,
|
|
388
|
+
maxRuns: 3
|
|
303
389
|
});
|
|
304
|
-
const
|
|
305
|
-
|
|
390
|
+
const spans = actions
|
|
391
|
+
.filter((action) => action.type !== "stop")
|
|
392
|
+
.map((action) => [action.from, action.to < action.from ? Number.POSITIVE_INFINITY : action.to]);
|
|
393
|
+
assert.ok(spans.some(([from, to]) => from <= 0 && to >= 0), "the one at the beginning is served");
|
|
394
|
+
for (let index = 0; index < spans.length - 1; index += 1) {
|
|
395
|
+
const sorted = [...spans].sort((left, right) => left[0] - right[0]);
|
|
396
|
+
assert.ok(sorted[index][1] < sorted[index + 1][0], "and no two encoders share a number");
|
|
397
|
+
}
|
|
306
398
|
});
|
|
307
399
|
|
|
308
400
|
test("a machine that can hold one gives it to the viewer who is stopped soonest", () => {
|
|
@@ -327,6 +419,12 @@ test("a viewer joining behind a running encoder gets their own, not a dragged on
|
|
|
327
419
|
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
328
420
|
const runA = run({ from: 500, to: 600, head: 510 });
|
|
329
421
|
coverage.claim(runA, 500, 600);
|
|
422
|
+
// A head at #510 means #500..#509 are already made — that is what a head is.
|
|
423
|
+
// Left unsaid, the map believes a viewer is waiting on ten pieces nobody has,
|
|
424
|
+
// and moving the encoder back to make them is then the right answer.
|
|
425
|
+
for (let index = 500; index < 510; index += 1) {
|
|
426
|
+
coverage.markReady(index);
|
|
427
|
+
}
|
|
330
428
|
const actions = planEncoders({
|
|
331
429
|
coverage,
|
|
332
430
|
windows: [{ from: 500, to: 530 }, { from: 100, to: 130 }],
|
|
@@ -366,7 +464,7 @@ test("a run with no end is making what the viewers ahead of it are waiting for",
|
|
|
366
464
|
wanted: [{ from: 0, to: 30 }],
|
|
367
465
|
maxRuns: 2,
|
|
368
466
|
segmentSeconds: 4,
|
|
369
|
-
|
|
467
|
+
...RUN_COSTS
|
|
370
468
|
});
|
|
371
469
|
|
|
372
470
|
assert.deepEqual(
|
|
@@ -381,15 +479,26 @@ test("a run with no end is making what the viewers ahead of it are waiting for",
|
|
|
381
479
|
);
|
|
382
480
|
});
|
|
383
481
|
|
|
384
|
-
test("the
|
|
385
|
-
//
|
|
386
|
-
//
|
|
482
|
+
test("spare budget IS spent on the rest of the film, once nobody is waiting", () => {
|
|
483
|
+
// The user's own correction: film nobody is waiting for still has value,
|
|
484
|
+
// because a viewer seeking back into a part that exists starts playing at
|
|
485
|
+
// once. So what the machine has spare goes to finishing the file.
|
|
486
|
+
//
|
|
487
|
+
// "Once nobody is waiting" is not a condition written anywhere — it falls out
|
|
488
|
+
// of the score. A second process costs the first the measured share of the
|
|
489
|
+
// machine, so while somebody is stopped on a piece, adding one delays that
|
|
490
|
+
// piece and the first term refuses it. Here the near film is already made,
|
|
491
|
+
// nothing is late in either arrangement, and the two that finish the rest
|
|
492
|
+
// sooner win.
|
|
387
493
|
const coverage = new CoverageMap({ segmentCount: 400 });
|
|
494
|
+
for (let index = 0; index <= 9; index += 1) {
|
|
495
|
+
coverage.markReady(index);
|
|
496
|
+
}
|
|
388
497
|
const actions = planEncoders({
|
|
389
498
|
coverage,
|
|
390
499
|
windows: [
|
|
391
|
-
{ from: 0, to: 9, priority: 32 },
|
|
392
|
-
{ from: 10, to: 399, priority: 20 }
|
|
500
|
+
{ from: 0, to: 9, priority: 32, withinSeconds: 0 },
|
|
501
|
+
{ from: 10, to: 399, priority: 20, withinSeconds: 40 }
|
|
393
502
|
],
|
|
394
503
|
runs: [],
|
|
395
504
|
...HOST,
|
|
@@ -397,31 +506,15 @@ test("the machine's whole budget is used, not one encoder per viewer", () => {
|
|
|
397
506
|
});
|
|
398
507
|
const started = actions.filter((action) => action.type === "start");
|
|
399
508
|
|
|
400
|
-
assert.
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
509
|
+
assert.ok(started.length > 1, "the machine does not stand idle while film is unmade");
|
|
510
|
+
const spans = started
|
|
511
|
+
.map((action) => [action.from, action.to])
|
|
512
|
+
.sort((left, right) => left[0] - right[0]);
|
|
513
|
+
for (let index = 0; index < spans.length - 1; index += 1) {
|
|
514
|
+
assert.ok(spans[index][1] < spans[index + 1][0], "and no two of them share a number");
|
|
515
|
+
}
|
|
404
516
|
});
|
|
405
517
|
|
|
406
|
-
test("below realtime the first stretches are what one encoder can hold", () => {
|
|
407
|
-
// At half speed an encoder holds `(q - p)` of film: the first covers ten
|
|
408
|
-
// segments, and the next has to be standing where it stops holding.
|
|
409
|
-
const coverage = new CoverageMap({ segmentCount: 400 });
|
|
410
|
-
const actions = planEncoders({
|
|
411
|
-
coverage,
|
|
412
|
-
windows: [{ from: 10, to: 399, priority: 32 }],
|
|
413
|
-
runs: [{ from: 0, to: 0, head: 0, speedX: 0.5 }],
|
|
414
|
-
...HOST,
|
|
415
|
-
maxRuns: 3
|
|
416
|
-
});
|
|
417
|
-
const started = actions
|
|
418
|
-
.filter((action) => action.type === "start")
|
|
419
|
-
.map((action) => action.from)
|
|
420
|
-
.sort((left, right) => left - right);
|
|
421
|
-
|
|
422
|
-
assert.ok(started.length >= 2, "more than one, because one cannot hold the film");
|
|
423
|
-
assert.ok(started[1] > started[0], "and they grow apart rather than sitting together");
|
|
424
|
-
});
|
|
425
518
|
|
|
426
519
|
test("two encoders never share a segment number", () => {
|
|
427
520
|
// The whole of what went wrong in the field: two encoders writing one name.
|
package/test/open-piece.test.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import test from "node:test";
|
|
23
23
|
import assert from "node:assert/strict";
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
24
25
|
import { mkdtemp, rm, writeFile, readdir } from "node:fs/promises";
|
|
25
26
|
import os from "node:os";
|
|
26
27
|
import path from "node:path";
|
|
@@ -110,3 +111,42 @@ test("only inside the stretch the ended run was given", async () => {
|
|
|
110
111
|
await rm(dir, { recursive: true, force: true });
|
|
111
112
|
}
|
|
112
113
|
});
|
|
114
|
+
|
|
115
|
+
test("a run given no end never reaches past what it made, so a live run's piece is safe", async () => {
|
|
116
|
+
// Field 2026-09-06. Three encoders wrote into one directory; the first was
|
|
117
|
+
// stopped, its stretch was `#0..#-1` — "to the end of the track" — and the
|
|
118
|
+
// cleanup after it took the highest-numbered file anywhere in that directory,
|
|
119
|
+
// which a LIVE encoder had just finished. The number is spent for good,
|
|
120
|
+
// because names only grow, and the picture stood still for 647 seconds.
|
|
121
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "open-piece-"));
|
|
122
|
+
// What the stopped run made: #0 and #1, with #2 left open.
|
|
123
|
+
await writeFile(path.join(dir, "segment-00000.mp4"), Buffer.alloc(1000, 1));
|
|
124
|
+
await writeFile(path.join(dir, "segment-00001.mp4"), Buffer.alloc(1000, 1));
|
|
125
|
+
await writeFile(path.join(dir, "segment-00002.mp4"), Buffer.alloc(0));
|
|
126
|
+
// What a live encoder, working further along the same track, has finished.
|
|
127
|
+
await writeFile(path.join(dir, "segment-00040.mp4"), Buffer.alloc(9000, 1));
|
|
128
|
+
|
|
129
|
+
const removed = await discardOpenPiece(
|
|
130
|
+
dir,
|
|
131
|
+
format,
|
|
132
|
+
{ from: 0, to: -1 },
|
|
133
|
+
null,
|
|
134
|
+
"segment-00001.mp4"
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.equal(removed, 2, "its own open piece goes");
|
|
138
|
+
assert.ok(existsSync(path.join(dir, "segment-00040.mp4")), "the live run's piece stays");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a run that named nothing leaves only its own first piece", async () => {
|
|
142
|
+
// It opened one file and died — 548 ms after starting, in the field. Nothing
|
|
143
|
+
// above that can be its.
|
|
144
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "open-piece-"));
|
|
145
|
+
await writeFile(path.join(dir, "segment-00010.mp4"), Buffer.alloc(0));
|
|
146
|
+
await writeFile(path.join(dir, "segment-00011.mp4"), Buffer.alloc(9000, 1));
|
|
147
|
+
|
|
148
|
+
const removed = await discardOpenPiece(dir, format, { from: 10, to: -1 }, null, null);
|
|
149
|
+
|
|
150
|
+
assert.equal(removed, 10, "the one it opened");
|
|
151
|
+
assert.ok(existsSync(path.join(dir, "segment-00011.mp4")), "and nothing beyond it");
|
|
152
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file How fast this machine produces one output.
|
|
3
|
+
*
|
|
4
|
+
* The figure every decision in the encoding layer rests on: where an encoder
|
|
5
|
+
* goes and how many run are both worked out from arrivals, and an arrival is a
|
|
6
|
+
* distance divided by this. There must therefore ALWAYS be an answer, and each
|
|
7
|
+
* of the three that can be given is a measurement rather than a guess.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { EncodeCost } from "../services/quality/EncodeCost.js";
|
|
13
|
+
import { LiveOutputs } from "../services/output/LiveOutputs.js";
|
|
14
|
+
|
|
15
|
+
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object[]} sessions
|
|
19
|
+
* @param {{ copySpeedX?: number | null, benchmark?: object[] | null }} host
|
|
20
|
+
*/
|
|
21
|
+
function costOf(sessions, host = {}) {
|
|
22
|
+
const sessionsById = new Map(sessions.map((session, index) => [String(index), session]));
|
|
23
|
+
return new EncodeCost({
|
|
24
|
+
liveOutputs: new LiveOutputs({ sessionsById }),
|
|
25
|
+
host: () => ({
|
|
26
|
+
benchmark: host.benchmark ?? null,
|
|
27
|
+
decodeModel: null,
|
|
28
|
+
contentionPenalties: null,
|
|
29
|
+
copySpeedX: host.copySpeedX ?? null,
|
|
30
|
+
availability: null
|
|
31
|
+
}),
|
|
32
|
+
audioCostKey: () => "",
|
|
33
|
+
runningEncoders: () => 0,
|
|
34
|
+
encodersRunningNow: () => 0,
|
|
35
|
+
torrentCostSecFor: () => 0
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("a run on this output outranks every prediction", () => {
|
|
40
|
+
// It is this machine, this material and these settings. Nothing said before
|
|
41
|
+
// the fact beats something seen happening.
|
|
42
|
+
const cost = costOf(
|
|
43
|
+
[{ outputKey: PICTURE, state: "ready", transcodeVideo: false, lastAloneSpeed: 9.5 }],
|
|
44
|
+
{ copySpeedX: 600 }
|
|
45
|
+
);
|
|
46
|
+
assert.equal(cost.speedForOutput(PICTURE), 9.5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a copied picture is priced by the startup copy measurement", () => {
|
|
50
|
+
// The branch that had no figure at all until it was measured: copying neither
|
|
51
|
+
// decodes nor encodes, so neither the preset benchmark nor the decode model
|
|
52
|
+
// describes it, and a copied output was planned with no speed until its own
|
|
53
|
+
// run had been running long enough to report one.
|
|
54
|
+
const cost = costOf(
|
|
55
|
+
[{ outputKey: PICTURE, state: "ready", transcodeVideo: false, lastAloneSpeed: null }],
|
|
56
|
+
{ copySpeedX: 602 }
|
|
57
|
+
);
|
|
58
|
+
assert.equal(cost.speedForOutput(PICTURE), 602);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the fastest reading wins where several sessions produce one output", () => {
|
|
62
|
+
// Two sessions whose output parameters agree ARE one output, so what either
|
|
63
|
+
// of them measured about this machine is true of the other.
|
|
64
|
+
const cost = costOf([
|
|
65
|
+
{ outputKey: PICTURE, state: "ready", transcodeVideo: false, lastAloneSpeed: 4 },
|
|
66
|
+
{ outputKey: PICTURE, state: "ready", transcodeVideo: false, lastAloneSpeed: 7 }
|
|
67
|
+
]);
|
|
68
|
+
assert.equal(cost.speedForOutput(PICTURE), 7);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a disposed session says nothing about what the machine is doing", () => {
|
|
72
|
+
const cost = costOf(
|
|
73
|
+
[{ outputKey: PICTURE, state: "disposed", transcodeVideo: false, lastAloneSpeed: 4 }],
|
|
74
|
+
{ copySpeedX: 602 }
|
|
75
|
+
);
|
|
76
|
+
assert.equal(cost.speedForOutput(PICTURE), 0, "no live session, so nothing to say");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("another output's reading is not borrowed", () => {
|
|
80
|
+
// Two outputs are two different pieces of work — a picture re-encoded to 480p
|
|
81
|
+
// and the same picture copied are not the same speed.
|
|
82
|
+
const cost = costOf([
|
|
83
|
+
{ outputKey: "other", state: "ready", transcodeVideo: false, lastAloneSpeed: 40 }
|
|
84
|
+
]);
|
|
85
|
+
assert.equal(cost.speedForOutput(PICTURE), 0);
|
|
86
|
+
});
|
|
@@ -15,7 +15,7 @@ import assert from "node:assert/strict";
|
|
|
15
15
|
import { TorrentPool } from "../services/torrent-pool.js";
|
|
16
16
|
import { demandFor, forgetTorrent } from "../services/download/registry.js";
|
|
17
17
|
import { Urgency } from "../services/demand/index.js";
|
|
18
|
-
import { mapForViewer } from "../services/priority/PriorityMap.js";
|
|
18
|
+
import { mapForViewer, runsOf } from "../services/priority/PriorityMap.js";
|
|
19
19
|
|
|
20
20
|
/** A torrent that is nothing but one file of a known length. */
|
|
21
21
|
function torrentOf({ length = 1_000_000, offset = 0 } = {}) {
|
|
@@ -58,11 +58,11 @@ test("a zone is stated at the level it means, not at the level of its position",
|
|
|
58
58
|
try {
|
|
59
59
|
// A viewer 300 s into a 3000 s film: one band behind them that nobody is
|
|
60
60
|
// approaching, then bands of decreasing urgency ahead.
|
|
61
|
-
const zones = mapForViewer({
|
|
61
|
+
const zones = runsOf(mapForViewer({
|
|
62
62
|
atSeconds: 300,
|
|
63
63
|
durationSeconds: 3000,
|
|
64
64
|
allowanceSeconds: 10
|
|
65
|
-
});
|
|
65
|
+
}));
|
|
66
66
|
assert.ok(zones.length > 5, "the map should be finer than the register's levels");
|
|
67
67
|
|
|
68
68
|
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
@@ -93,23 +93,41 @@ test("a zone is stated at the level it means, not at the level of its position",
|
|
|
93
93
|
test("a viewer who has stopped the picture makes the whole film wanted last", () => {
|
|
94
94
|
const torrent = torrentOf();
|
|
95
95
|
try {
|
|
96
|
-
const zones = mapForViewer({
|
|
96
|
+
const zones = runsOf(mapForViewer({
|
|
97
97
|
atSeconds: 300,
|
|
98
98
|
durationSeconds: 3000,
|
|
99
99
|
allowanceSeconds: 10,
|
|
100
100
|
playing: false
|
|
101
|
-
});
|
|
101
|
+
}));
|
|
102
102
|
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
103
103
|
|
|
104
104
|
const { register } = demandFor(torrent);
|
|
105
105
|
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
106
|
-
|
|
106
|
+
// A PAUSE REMOVES THE TIME, NOT THE DIRECTION. What they have watched is
|
|
107
|
+
// behind them and what they have not is in front, exactly as while they were
|
|
108
|
+
// watching; what changes is that nothing has a time any more, so every band
|
|
109
|
+
// of theirs yields to anybody who is on their way somewhere.
|
|
110
|
+
assert.ok(stated.length >= 2, "their position survives the pause");
|
|
111
|
+
for (const one of stated) {
|
|
112
|
+
assert.ok(
|
|
113
|
+
one.urgency === Urgency.BEHIND || one.urgency === Urgency.TAIL,
|
|
114
|
+
"and nothing of theirs is urgent, because nobody is coming"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const front = stated.filter((one) => one.urgency === Urgency.TAIL);
|
|
118
|
+
assert.ok(front.length >= 1, "the film in front of them is still in front");
|
|
119
|
+
assert.ok(
|
|
120
|
+
front[0].byteStart >= Math.floor((300 / 3000) * 1_000_000),
|
|
121
|
+
"and it begins where they stopped"
|
|
122
|
+
);
|
|
107
123
|
// Wanted last ABSOLUTELY, not relative to this film's own map. The two
|
|
108
124
|
// speculative levels are withheld across every torrent at once while
|
|
109
125
|
// anything urgent is missing anywhere, so this has to lose to a film
|
|
110
126
|
// somebody is actually watching — and judged against itself alone it would
|
|
111
127
|
// be the most urgent thing there is.
|
|
112
|
-
|
|
128
|
+
const behind = stated.filter((one) => one.urgency === Urgency.BEHIND);
|
|
129
|
+
assert.equal(behind.length, 1, "what they have already watched is wanted last of all");
|
|
130
|
+
assert.equal(behind[0].byteStart, 0, "and it is the stretch from the beginning to where they stopped");
|
|
113
131
|
} finally {
|
|
114
132
|
forgetTorrent(torrent);
|
|
115
133
|
}
|