@torrent-tv/proxy 2.77.0 → 2.79.0
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 +29 -0
- package/biome.json +1 -0
- package/package.json +1 -1
- package/routes/api/transcode-sessions/net-report/post.js +5 -0
- package/services/encode/EncodePlan.js +309 -272
- package/services/encode/EncodeRun.js +70 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +59 -0
- package/services/encode/open-piece.js +88 -0
- package/services/encode/run-budget.js +89 -0
- package/services/encode/run-command.js +15 -0
- package/services/encode/run-costs.js +94 -0
- package/services/hls-session-manager.js +10657 -10846
- package/services/orchestrators/EncodeOrchestrator.js +147 -20
- package/services/priority/PriorityMap.js +252 -0
- package/services/viewer/Viewer.js +83 -0
- package/test/encode-orchestrator.test.js +36 -8
- package/test/encode-plan.test.js +111 -7
- package/test/one-authority.test.js +105 -0
- package/test/priority-map.test.js +139 -0
- package/test/run-intervals.test.js +67 -303
|
@@ -45,6 +45,12 @@ function orchestrator({ maxRuns = 2 } = {}) {
|
|
|
45
45
|
maxRunsFor: () => maxRuns,
|
|
46
46
|
segmentSeconds: 4,
|
|
47
47
|
restartCostSec: 0.12,
|
|
48
|
+
// A host that has measured what the swarm charges to fetch a second of film
|
|
49
|
+
// again. Without it the drive-or-move comparison has only one side and the
|
|
50
|
+
// plan keeps the encoder rather than paying an unknown price — which is its
|
|
51
|
+
// own check in `encode-plan.test.js` rather than the shape every check here
|
|
52
|
+
// is written against.
|
|
53
|
+
refetchSecPerFilmSecond: () => 0.25,
|
|
48
54
|
now: () => 1000,
|
|
49
55
|
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) },
|
|
50
56
|
makeRun: ({ address, from, to }) => {
|
|
@@ -195,22 +201,25 @@ test("nothing wanted anywhere is said plainly", () => {
|
|
|
195
201
|
assert.match(made.describe(), /nothing wanted/);
|
|
196
202
|
});
|
|
197
203
|
|
|
198
|
-
test("a run adopted with no end holds
|
|
204
|
+
test("a run adopted with no end holds what it has made, not the rest of the film", () => {
|
|
199
205
|
// A session's own encoder is handed to the plan rather than built by it, and
|
|
200
|
-
// it carries `to = -1` — no end
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
206
|
+
// it carries `to = -1` — no end, which means the film's length is not known.
|
|
207
|
+
// Claiming the film from there would leave a viewer further in with no
|
|
208
|
+
// encoder at all: they would wait for this run to encode its way to them,
|
|
209
|
+
// which on a long film is an hour. What it holds is what it has produced,
|
|
210
|
+
// which is a fact rather than a distance nobody measured.
|
|
205
211
|
const { made } = orchestrator({ maxRuns: 2 });
|
|
206
|
-
made.lookaheadSegments = 30;
|
|
207
212
|
const adopted = {
|
|
208
213
|
from: 0,
|
|
209
214
|
to: -1,
|
|
210
215
|
head: 3,
|
|
211
216
|
isAlive: true,
|
|
212
217
|
isStopping: false,
|
|
213
|
-
|
|
218
|
+
// Slow enough that the swarm is not what limits the count here: at 0.25
|
|
219
|
+
// seconds of swarm time per second of film, one encoder at 1x takes a
|
|
220
|
+
// quarter of what is delivered and four may run. The swarm's own limit has
|
|
221
|
+
// its own check below.
|
|
222
|
+
speedX: 1,
|
|
214
223
|
stop() {
|
|
215
224
|
this.isAlive = false;
|
|
216
225
|
}
|
|
@@ -225,3 +234,22 @@ test("a run adopted with no end holds the look-ahead, not the rest of the film",
|
|
|
225
234
|
assert.ok(adopted.isAlive, "and the adopted run was not stopped to make room");
|
|
226
235
|
assert.equal(runs.some((run) => run.from === 200), true, "started where that viewer is waiting");
|
|
227
236
|
});
|
|
237
|
+
|
|
238
|
+
test("the swarm limits the encoders, whatever the processor allows", () => {
|
|
239
|
+
// Every encoder reads the same torrent, so together they cannot consume
|
|
240
|
+
// faster than it is delivered. At 0.25 seconds of swarm time per second of
|
|
241
|
+
// film, one encoder running at 8x takes twice everything there is — so a
|
|
242
|
+
// machine whose processor would allow two gets one.
|
|
243
|
+
const { made, lines } = orchestrator({ maxRuns: 2 });
|
|
244
|
+
made.want({ claimant: "one", address: PICTURE, from: 100, to: 130 });
|
|
245
|
+
made.reconcile();
|
|
246
|
+
made.runsOn(PICTURE)[0].noteSpeed(8);
|
|
247
|
+
made.want({ claimant: "two", address: PICTURE, from: 500, to: 530 });
|
|
248
|
+
made.reconcile();
|
|
249
|
+
|
|
250
|
+
assert.equal(made.runsOn(PICTURE).length, 1);
|
|
251
|
+
assert.ok(
|
|
252
|
+
lines.some((line) => line.includes("what the swarm delivers")),
|
|
253
|
+
"and the line says which limit decided it"
|
|
254
|
+
);
|
|
255
|
+
});
|
package/test/encode-plan.test.js
CHANGED
|
@@ -14,7 +14,19 @@ import { endOfRun } from "../services/encode/EncodeRun.js";
|
|
|
14
14
|
import { firstUnmetWant, planEncoders } from "../services/encode/EncodePlan.js";
|
|
15
15
|
|
|
16
16
|
/** A host that can afford two encoders, four-second segments, a cheap restart. */
|
|
17
|
-
|
|
17
|
+
// A host that has measured itself: the start and the death from its own runs,
|
|
18
|
+
// and what the swarm charges to fetch a second of film again. All four terms
|
|
19
|
+
// have to be present for the drive-or-move comparison to mean anything, and a
|
|
20
|
+
// host missing any of them keeps its encoders instead — which is its own check
|
|
21
|
+
// below rather than the shape every other check is written against.
|
|
22
|
+
const HOST = {
|
|
23
|
+
maxRuns: 2,
|
|
24
|
+
segmentSeconds: 4,
|
|
25
|
+
restartCostSec: 0.12,
|
|
26
|
+
killCostSec: 0.5,
|
|
27
|
+
firstByteWaitSec: 1,
|
|
28
|
+
refetchSecPerFilmSecond: 0.25
|
|
29
|
+
};
|
|
18
30
|
|
|
19
31
|
/**
|
|
20
32
|
* @param {Partial<import("../services/encode/EncodePlan.js").LiveRun>} run
|
|
@@ -121,9 +133,13 @@ test("a covered stretch shorter than a restart is driven through instead", () =>
|
|
|
121
133
|
assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
|
|
122
134
|
});
|
|
123
135
|
|
|
124
|
-
test("a run whose speed nothing has measured is
|
|
125
|
-
//
|
|
126
|
-
//
|
|
136
|
+
test("a run whose speed nothing has measured is kept, not taken away", () => {
|
|
137
|
+
// Moving costs a known amount for an unknown gain, and a run nothing has
|
|
138
|
+
// measured has produced nothing yet — so taking its work away is certainly a
|
|
139
|
+
// loss and the comparison cannot be made. It used to answer the other way,
|
|
140
|
+
// and then every just-started run was moved the moment anything ahead of it
|
|
141
|
+
// was covered, which, once the film ahead had been made, was always: 684
|
|
142
|
+
// starts in 482 seconds in the field on 2026-09-05.
|
|
127
143
|
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
128
144
|
const runA = run({ head: 10, speedX: 0 });
|
|
129
145
|
coverage.claim(runA, 0, 100);
|
|
@@ -134,9 +150,55 @@ test("a run whose speed nothing has measured is moved rather than left driving t
|
|
|
134
150
|
runs: [runA],
|
|
135
151
|
...HOST
|
|
136
152
|
});
|
|
153
|
+
assert.equal(actions.some((action) => action.type === "move"), false);
|
|
154
|
+
assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("both sides of the move are counted, not just the encoder's own time", () => {
|
|
158
|
+
// Driving through costs this run's encode time AND the swarm the same bytes a
|
|
159
|
+
// second time; moving costs the death, the start and the wait for the first
|
|
160
|
+
// bytes. Here driving is dear enough to lose: 20 covered segments of 4 s at
|
|
161
|
+
// 1x is 80 s of encoding, against a move priced at 0.12 + 0.5 + 3 seconds.
|
|
162
|
+
const coverage = new CoverageMap({ segmentCount: 200 });
|
|
163
|
+
const runA = run({ head: 10, speedX: 1 });
|
|
164
|
+
coverage.claim(runA, 0, 200);
|
|
165
|
+
for (let at = 10; at < 30; at += 1) {
|
|
166
|
+
coverage.markReady(at);
|
|
167
|
+
}
|
|
168
|
+
const actions = planEncoders({
|
|
169
|
+
coverage,
|
|
170
|
+
windows: [{ from: 0, to: 190 }],
|
|
171
|
+
runs: [runA],
|
|
172
|
+
...HOST,
|
|
173
|
+
killCostSec: 0.5,
|
|
174
|
+
firstByteWaitSec: 3,
|
|
175
|
+
refetchSecPerFilmSecond: 0.25
|
|
176
|
+
});
|
|
137
177
|
const move = actions.find((action) => action.type === "move");
|
|
138
178
|
assert.ok(move);
|
|
139
|
-
assert.match(move.because, /
|
|
179
|
+
assert.match(move.because, /refetch 20\.00s/);
|
|
180
|
+
assert.match(move.because, /against 3\.62s to move/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a short covered stretch is driven through rather than paid a restart for", () => {
|
|
184
|
+
// One covered segment at 1x is 4 s of encoding plus 1 s of refetch, against a
|
|
185
|
+
// move priced at 0.12 + 0.5 + 30 seconds on a host where the first bytes are
|
|
186
|
+
// slow to come. The comparison, not a rule, decides it.
|
|
187
|
+
const coverage = new CoverageMap({ segmentCount: 200 });
|
|
188
|
+
const runA = run({ head: 10, speedX: 1 });
|
|
189
|
+
coverage.claim(runA, 0, 200);
|
|
190
|
+
coverage.markReady(10);
|
|
191
|
+
const actions = planEncoders({
|
|
192
|
+
coverage,
|
|
193
|
+
windows: [{ from: 0, to: 190 }],
|
|
194
|
+
runs: [runA],
|
|
195
|
+
...HOST,
|
|
196
|
+
killCostSec: 0.5,
|
|
197
|
+
firstByteWaitSec: 30,
|
|
198
|
+
refetchSecPerFilmSecond: 0.25
|
|
199
|
+
});
|
|
200
|
+
assert.equal(actions.some((action) => action.type === "move"), false);
|
|
201
|
+
assert.ok(actions.some((action) => action.type === "keep" && action.run === runA));
|
|
140
202
|
});
|
|
141
203
|
|
|
142
204
|
test("a run with nothing left to make ahead of it is stopped", () => {
|
|
@@ -176,7 +238,12 @@ test("every encoder stops when nobody is watching the output", () => {
|
|
|
176
238
|
);
|
|
177
239
|
});
|
|
178
240
|
|
|
179
|
-
test("a run
|
|
241
|
+
test("a run standing outside every window keeps working: the file is encoded whole", () => {
|
|
242
|
+
// The rule, stated by the user 2026-09-05: while a file is being encoded it
|
|
243
|
+
// is encoded whole, and a viewer decides the ORDER, not whether a run may
|
|
244
|
+
// live. Stopping a run for standing outside a window is what produced the
|
|
245
|
+
// field oscillation of that day — placed by one rule, killed by another,
|
|
246
|
+
// 350-700ms per cycle, nothing ever produced.
|
|
180
247
|
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
181
248
|
const runA = run({ from: 500, to: 600, head: 520 });
|
|
182
249
|
coverage.claim(runA, 500, 600);
|
|
@@ -186,7 +253,44 @@ test("a run making material nobody asked for is stopped", () => {
|
|
|
186
253
|
runs: [runA],
|
|
187
254
|
...HOST
|
|
188
255
|
});
|
|
189
|
-
assert.ok(
|
|
256
|
+
assert.ok(
|
|
257
|
+
!actions.some((action) => action.type === "stop" && action.run === runA),
|
|
258
|
+
"it is making film that will be wanted, and nothing else is making it"
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("the same plan run twice on an unchanged state gives the same answer", () => {
|
|
263
|
+
// What the oscillation actually was: two passes over one state disagreeing
|
|
264
|
+
// with each other. Nothing about the state changes between them here.
|
|
265
|
+
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
266
|
+
const runA = run({ from: 500, to: 600, head: 520 });
|
|
267
|
+
coverage.claim(runA, 500, 600);
|
|
268
|
+
const input = { coverage, windows: [{ from: 0, to: 40 }], runs: [runA], ...HOST };
|
|
269
|
+
|
|
270
|
+
const first = planEncoders(input).map((action) => action.type);
|
|
271
|
+
const second = planEncoders(input).map((action) => action.type);
|
|
272
|
+
|
|
273
|
+
assert.deepEqual(first, second);
|
|
274
|
+
assert.ok(!first.includes("stop"), "and neither pass kills what the other would start");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("a viewer's most urgent zone is filled before a less urgent one", () => {
|
|
278
|
+
const coverage = new CoverageMap({ segmentCount: 1000 });
|
|
279
|
+
const actions = planEncoders({
|
|
280
|
+
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
|
+
windows: [
|
|
284
|
+
{ from: 0, to: 100, priority: 1 },
|
|
285
|
+
{ from: 500, to: 530, priority: 3 }
|
|
286
|
+
],
|
|
287
|
+
runs: [],
|
|
288
|
+
...HOST,
|
|
289
|
+
maxRuns: 1
|
|
290
|
+
});
|
|
291
|
+
const started = actions.filter((action) => action.type === "start").map((action) => action.from);
|
|
292
|
+
|
|
293
|
+
assert.deepEqual(started, [500], "the one machine goes where somebody is stopped");
|
|
190
294
|
});
|
|
191
295
|
|
|
192
296
|
test("two viewers far apart get an encoder each, when the machine can hold two", () => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file One place decides which encoders exist.
|
|
3
|
+
*
|
|
4
|
+
* Not a check of behaviour but of shape, and it is here because the shape is
|
|
5
|
+
* what failed. Three separate places used to decide where an encoder should
|
|
6
|
+
* work and whether it should go on living, and they disagreed on every pass:
|
|
7
|
+
* measured in the field on 2026-09-05, 684 starts and 660 stops in 482 seconds,
|
|
8
|
+
* of which 294 were one place killing what another had just decided to keep,
|
|
9
|
+
* while the viewer's own segment went unmade for 32.3 seconds.
|
|
10
|
+
*
|
|
11
|
+
* Every rule below is one that was broken then. A reader who needs to add a
|
|
12
|
+
* fourth place should read this file first and then not.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} relative
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function source(relative) {
|
|
28
|
+
return readFileSync(path.join(HERE, "..", relative), "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lines of code, without comments or blanks: a rule about what the code does
|
|
33
|
+
* must not be answered by what a comment says about it.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
function statements(text) {
|
|
39
|
+
return text
|
|
40
|
+
.split("\n")
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.filter((line) => line.length > 0 && !line.startsWith("//") && !line.startsWith("*") && !line.startsWith("/*"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("an encoder is stopped for scheduling reasons in exactly one place", () => {
|
|
46
|
+
// The orchestrator decides; nobody else may. What is left in the session
|
|
47
|
+
// manager is teardown — the session is going away and its encoders with it —
|
|
48
|
+
// which is not a decision about which encoders should exist.
|
|
49
|
+
const orchestrator = statements(source("services/orchestrators/EncodeOrchestrator.js"));
|
|
50
|
+
const stopsInOrchestrator = orchestrator.filter((line) => line.includes("run.stop("));
|
|
51
|
+
assert.equal(stopsInOrchestrator.length, 1, "the orchestrator stops runs in one place");
|
|
52
|
+
|
|
53
|
+
const manager = statements(source("services/hls-session-manager.js"));
|
|
54
|
+
const stopsInManager = manager.filter((line) => line.includes(".stop("));
|
|
55
|
+
assert.equal(
|
|
56
|
+
stopsInManager.length,
|
|
57
|
+
2,
|
|
58
|
+
"the session manager stops runs only when a session is torn down: " +
|
|
59
|
+
stopsInManager.join(" / ")
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("nothing outside the encoding layer starts an encoder", () => {
|
|
64
|
+
// A run is built in one place. Two places building them is how a start came
|
|
65
|
+
// to kill what the plan had decided to keep — the killing lived in the
|
|
66
|
+
// building.
|
|
67
|
+
const manager = statements(source("services/hls-session-manager.js"));
|
|
68
|
+
const builds = manager.filter((line) => line.includes("new EncodeRun("));
|
|
69
|
+
assert.equal(builds.length, 1, "one place builds an encoder");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("starting an encoder stops nothing", () => {
|
|
73
|
+
// The rule that broke it: the start path looked for a live run whose own
|
|
74
|
+
// start was not below the new one's and killed it. It is not enough that the
|
|
75
|
+
// line is gone — the words it was written with must not come back.
|
|
76
|
+
const manager = source("services/hls-session-manager.js");
|
|
77
|
+
assert.equal(
|
|
78
|
+
manager.includes("previousRun"),
|
|
79
|
+
false,
|
|
80
|
+
"there is no such thing as the previous run: a session holds several"
|
|
81
|
+
);
|
|
82
|
+
assert.equal(manager.includes("a new run is taking its place"), false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a seek moves the viewer and nothing else", () => {
|
|
86
|
+
// It used to do eleven things and write the position into five places. What
|
|
87
|
+
// follows from a viewer moving is the map's business, and the orchestrators
|
|
88
|
+
// read the map.
|
|
89
|
+
const manager = source("services/hls-session-manager.js");
|
|
90
|
+
const seek = manager.slice(
|
|
91
|
+
manager.indexOf("requestSeek(sessionId, positionSeconds"),
|
|
92
|
+
manager.indexOf("requestSeek(sessionId, positionSeconds") + 2000
|
|
93
|
+
);
|
|
94
|
+
const body = seek.slice(0, seek.indexOf("\n }\n"));
|
|
95
|
+
assert.equal(body.includes("#startEncodeRun"), false, "a seek starts no encoder");
|
|
96
|
+
assert.equal(body.includes("setTimeout"), false, "and waits for nothing");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("how far an encoder may work is answered once", () => {
|
|
100
|
+
// The plan computes the stretch and it reaches ffmpeg. A second computation
|
|
101
|
+
// somewhere else is what made the first one pointless: it was passed and then
|
|
102
|
+
// dropped by a parameter list that did not name it.
|
|
103
|
+
const orchestrator = source("services/orchestrators/EncodeOrchestrator.js");
|
|
104
|
+
assert.ok(orchestrator.includes("makeRun({ address, from, to })"), "the stretch is handed over");
|
|
105
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A map per viewer, merged into one, in the order the work is taken.
|
|
3
|
+
*
|
|
4
|
+
* In seconds of film throughout: it is the only unit every term of the
|
|
5
|
+
* arithmetic is already stated in, and the one two consumers with different
|
|
6
|
+
* states can both be translated from.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mapForViewer, mergeMaps, inWorkingOrder } from "../services/priority/PriorityMap.js";
|
|
12
|
+
|
|
13
|
+
test("a viewer's own position is the most urgent thing there is", () => {
|
|
14
|
+
const map = mapForViewer({
|
|
15
|
+
atSeconds: 100,
|
|
16
|
+
durationSeconds: 1000,
|
|
17
|
+
allowanceSeconds: 8,
|
|
18
|
+
encodeSpeedX: 2
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(map[0].from, 100);
|
|
22
|
+
assert.equal(map[0].to, 108, "above realtime, only the measured allowance");
|
|
23
|
+
for (const zone of map.slice(1)) {
|
|
24
|
+
assert.ok(zone.priority < map[0].priority);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("a machine that cannot keep up needs more encoders, not a longer first zone", () => {
|
|
29
|
+
// An encoder starting where the viewer stands stays ahead of them for
|
|
30
|
+
// `held x s / (1 - s)`, so the SLOWER the machine the shorter that is — and
|
|
31
|
+
// the film is held by more encoders rather than by one working further.
|
|
32
|
+
const slow = mapForViewer({
|
|
33
|
+
atSeconds: 0,
|
|
34
|
+
durationSeconds: 1000,
|
|
35
|
+
allowanceSeconds: 10,
|
|
36
|
+
encodeSpeedX: 0.25
|
|
37
|
+
});
|
|
38
|
+
const quicker = mapForViewer({
|
|
39
|
+
atSeconds: 0,
|
|
40
|
+
durationSeconds: 1000,
|
|
41
|
+
allowanceSeconds: 10,
|
|
42
|
+
encodeSpeedX: 0.5
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.ok(
|
|
46
|
+
slow[0].to - slow[0].from < quicker[0].to - quicker[0].from,
|
|
47
|
+
"the slower machine holds the viewer for less"
|
|
48
|
+
);
|
|
49
|
+
assert.ok(slow.length > quicker.length, "so more encoders are needed to hold the film");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the first zone is exactly what one encoder can hold", () => {
|
|
53
|
+
// `held x s / (1 - s)`: ten seconds held at half speed is ten seconds held,
|
|
54
|
+
// and the encoder is caught exactly there. A longer first zone would stall
|
|
55
|
+
// the viewer inside it; a shorter one would start the next encoder nearer,
|
|
56
|
+
// where its own bound is tighter.
|
|
57
|
+
const map = mapForViewer({
|
|
58
|
+
atSeconds: 0,
|
|
59
|
+
durationSeconds: 100,
|
|
60
|
+
allowanceSeconds: 10,
|
|
61
|
+
encodeSpeedX: 0.5
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.equal(map[0].to, 10);
|
|
65
|
+
assert.equal(map[1].to, 30, "the next holds three times as long, because they arrive later");
|
|
66
|
+
assert.equal(map[2].to, 70);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a viewer holding nothing is told the whole of it is urgent", () => {
|
|
70
|
+
// With nothing held, no partition holds them: the first encoder stays ahead
|
|
71
|
+
// for zero seconds. Slicing the film into equal slivers would pretend
|
|
72
|
+
// otherwise.
|
|
73
|
+
const map = mapForViewer({
|
|
74
|
+
atSeconds: 0,
|
|
75
|
+
durationSeconds: 1000,
|
|
76
|
+
allowanceSeconds: 0,
|
|
77
|
+
encodeSpeedX: 0.25
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(map.length, 1);
|
|
81
|
+
assert.equal(map[0].to, 1000);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("the rest of the track is still wanted, and wanted last", () => {
|
|
85
|
+
const map = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 4, encodeSpeedX: 2 });
|
|
86
|
+
|
|
87
|
+
assert.equal(map[map.length - 1].to, 1000, "the map reaches the end of the film");
|
|
88
|
+
assert.ok(map[map.length - 1].priority > 0, "and the far end is still wanted");
|
|
89
|
+
let previousEnd = 0;
|
|
90
|
+
for (const zone of map) {
|
|
91
|
+
assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
|
|
92
|
+
previousEnd = zone.to;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("nothing is measured yet: the middle zone is left out rather than invented", () => {
|
|
97
|
+
const map = mapForViewer({ atSeconds: 0, durationSeconds: 100, allowanceSeconds: 4, encodeSpeedX: 0 });
|
|
98
|
+
|
|
99
|
+
assert.equal(map.length, 2, "what must be ready, and the rest");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("two viewers merge to the highest priority per second, with no overlaps", () => {
|
|
103
|
+
const first = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
|
|
104
|
+
const second = mapForViewer({ atSeconds: 500, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
|
|
105
|
+
|
|
106
|
+
const merged = mergeMaps([first, second]);
|
|
107
|
+
|
|
108
|
+
let previousEnd = merged[0].from;
|
|
109
|
+
for (const zone of merged) {
|
|
110
|
+
assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
|
|
111
|
+
previousEnd = zone.to;
|
|
112
|
+
}
|
|
113
|
+
const priorityAt = (at) => merged.find((zone) => at >= zone.from && at < zone.to)?.priority;
|
|
114
|
+
assert.equal(priorityAt(0), priorityAt(500), "both viewers' own positions are equally urgent");
|
|
115
|
+
assert.ok(priorityAt(500) > priorityAt(300), "a viewer at 500 outranks the far zone of the one at 0");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("the second viewer's position is not buried under the first viewer's far zone", () => {
|
|
119
|
+
// The case that used to leave a viewer opening the same film further in with
|
|
120
|
+
// no encoder at all, because the first run claimed everything in front of it.
|
|
121
|
+
const first = mapForViewer({ atSeconds: 0, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
|
|
122
|
+
const second = mapForViewer({ atSeconds: 2000, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
|
|
123
|
+
|
|
124
|
+
const order = inWorkingOrder(mergeMaps([first, second]));
|
|
125
|
+
const firstTwo = order.slice(0, 2).map((zone) => zone.from);
|
|
126
|
+
|
|
127
|
+
assert.ok(firstTwo.includes(0), "the first viewer's own position is taken first");
|
|
128
|
+
assert.ok(firstTwo.includes(2000), "so is the second viewer's, before anything less urgent");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("within one priority the earliest film goes first — that is where somebody is stopped", () => {
|
|
132
|
+
const order = inWorkingOrder([
|
|
133
|
+
{ from: 900, to: 1000, priority: 2 },
|
|
134
|
+
{ from: 100, to: 200, priority: 2 },
|
|
135
|
+
{ from: 0, to: 10, priority: 3 }
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
assert.deepEqual(order.map((zone) => zone.from), [0, 100, 900]);
|
|
139
|
+
});
|