@torrent-tv/proxy 2.80.11 → 2.80.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/CHANGELOG.md +14 -0
- package/CLAUDE.md +6 -0
- package/docs/download-architecture.md +45 -1
- package/docs/encode-architecture.md +180 -0
- package/package.json +1 -1
- package/routes/stream/get.js +324 -300
- package/routes/transcode/session-file/get.js +22 -2
- package/services/encode/SegmentDemand.js +34 -0
- package/services/hls-session-manager.js +19 -17
- package/services/orchestrators/EncodeOrchestrator.js +735 -726
- package/services/output/LiveOutputs.js +267 -282
- package/services/priority/WaitLedger.js +142 -0
- package/services/torrent-pool.js +38 -1
- package/services/torrent-worker/client.js +27 -12
- package/test/priority-map-per-output.test.js +5 -5
- package/test/quality-variants.test.js +4 -4
- package/test/wait-ledger.test.js +100 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file How well the priority map is being served, measured where somebody waits.
|
|
3
|
+
*
|
|
4
|
+
* The map says what matters most. Nothing said whether what mattered most was
|
|
5
|
+
* actually delivered first — so "is the prioritisation any good" had no answer
|
|
6
|
+
* of any kind, for either of the two things that read the map.
|
|
7
|
+
*
|
|
8
|
+
* The measure is the same for both, and it is a wait recorded against the rank
|
|
9
|
+
* the map gave the thing waited for AT THE MOMENT it was asked for. Read that
|
|
10
|
+
* way it says which part is wrong rather than whether the whole scheme is:
|
|
11
|
+
*
|
|
12
|
+
* - long waits at the TOP rank mean the urgent zone is not being served first,
|
|
13
|
+
* which is a fault in whoever acts on the map;
|
|
14
|
+
* - long waits further down with none at the top mean the zones are the wrong
|
|
15
|
+
* width — the urgent one too narrow, so the viewer reaches material that was
|
|
16
|
+
* only ever ranked "soon";
|
|
17
|
+
* - a rank with no waits at all is not a good sign or a bad one, it is silence,
|
|
18
|
+
* and it is reported as such rather than as a zero.
|
|
19
|
+
*
|
|
20
|
+
* There is nothing in here about encoders, torrents, sessions or viewers: it is
|
|
21
|
+
* given a rank and a number of milliseconds.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** How many waits to keep per rank. A median wants a sample, not a history. */
|
|
25
|
+
const HISTORY = 200;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ranks are collapsed into bands before they are counted.
|
|
29
|
+
*
|
|
30
|
+
* The map's ranks are as many as the film needs — 100 down to 1 on a long file
|
|
31
|
+
* — and a table with a hundred rows says nothing a reader can hold. What is
|
|
32
|
+
* being asked is coarse: was the thing waited for what the viewer needs NOW,
|
|
33
|
+
* what they reach shortly, or the rest of the film. So the top rank is its own
|
|
34
|
+
* band, the next few are the second, and everything below is the third.
|
|
35
|
+
*
|
|
36
|
+
* @param {number} rank
|
|
37
|
+
* @param {number} topRank - The highest rank the map currently states.
|
|
38
|
+
* @returns {"now" | "soon" | "later"}
|
|
39
|
+
*/
|
|
40
|
+
export function bandOf(rank, topRank) {
|
|
41
|
+
if (!Number.isFinite(rank) || rank <= 0) {
|
|
42
|
+
return "later";
|
|
43
|
+
}
|
|
44
|
+
const top = Number.isFinite(topRank) && topRank > 0 ? topRank : rank;
|
|
45
|
+
if (rank >= top) {
|
|
46
|
+
return "now";
|
|
47
|
+
}
|
|
48
|
+
// Within a tenth of the top: what a viewer reaches while watching what they
|
|
49
|
+
// hold. A tenth is the map's own shape — its zones widen geometrically — and
|
|
50
|
+
// not a threshold chosen for this table.
|
|
51
|
+
return rank >= top - Math.max(1, Math.round(top / 10)) ? "soon" : "later";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class WaitLedger {
|
|
55
|
+
/** @type {Map<string, Map<string, number[]>>} */
|
|
56
|
+
#waits = new Map();
|
|
57
|
+
|
|
58
|
+
/** @type {Map<string, Map<string, number>>} */
|
|
59
|
+
#counts = new Map();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Somebody waited this long for something the map ranked this highly.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} key - What the waits belong to: an output, or a file.
|
|
65
|
+
* @param {number} waitedMs
|
|
66
|
+
* @param {number} rank - The map's rank for the thing waited for.
|
|
67
|
+
* @param {number} topRank - The highest rank the map states, so the rank can
|
|
68
|
+
* be read as a position rather than as an absolute number.
|
|
69
|
+
*/
|
|
70
|
+
note(key, waitedMs, rank, topRank) {
|
|
71
|
+
if (!key || !Number.isFinite(waitedMs) || waitedMs < 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const band = bandOf(rank, topRank);
|
|
75
|
+
let byBand = this.#waits.get(key);
|
|
76
|
+
if (!byBand) {
|
|
77
|
+
byBand = new Map();
|
|
78
|
+
this.#waits.set(key, byBand);
|
|
79
|
+
}
|
|
80
|
+
const held = byBand.get(band) ?? [];
|
|
81
|
+
held.push(waitedMs);
|
|
82
|
+
while (held.length > HISTORY) {
|
|
83
|
+
held.shift();
|
|
84
|
+
}
|
|
85
|
+
byBand.set(band, held);
|
|
86
|
+
|
|
87
|
+
let counts = this.#counts.get(key);
|
|
88
|
+
if (!counts) {
|
|
89
|
+
counts = new Map();
|
|
90
|
+
this.#counts.set(key, counts);
|
|
91
|
+
}
|
|
92
|
+
counts.set(band, (counts.get(band) ?? 0) + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What the waits say, in one line, or null while nothing has waited.
|
|
97
|
+
*
|
|
98
|
+
* The count is the whole run and the median and worst are the recent sample,
|
|
99
|
+
* because those answer different questions: how often, and how badly.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} key
|
|
102
|
+
* @returns {string | null}
|
|
103
|
+
*/
|
|
104
|
+
describe(key) {
|
|
105
|
+
const byBand = this.#waits.get(key);
|
|
106
|
+
if (!byBand || byBand.size === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const counts = this.#counts.get(key) ?? new Map();
|
|
110
|
+
const parts = [];
|
|
111
|
+
for (const band of ["now", "soon", "later"]) {
|
|
112
|
+
const held = byBand.get(band);
|
|
113
|
+
if (!held || held.length === 0) {
|
|
114
|
+
// Said out loud, because an absent band and a band that never waited
|
|
115
|
+
// are the same silence and neither is a zero.
|
|
116
|
+
parts.push(`${band} none`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const sorted = [...held].sort((a, b) => a - b);
|
|
120
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
121
|
+
parts.push(
|
|
122
|
+
`${band} ${counts.get(band) ?? held.length} wait(s) median ${Math.round(median)}ms ` +
|
|
123
|
+
`worst ${Math.round(sorted[sorted.length - 1])}ms`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return parts.join(", ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} key
|
|
131
|
+
*/
|
|
132
|
+
forget(key) {
|
|
133
|
+
this.#waits.delete(key);
|
|
134
|
+
this.#counts.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One ledger for the process, because the question is asked in two layers and
|
|
140
|
+
* the answer is comparable only if the scale is the same.
|
|
141
|
+
*/
|
|
142
|
+
export const waits = new WaitLedger();
|
package/services/torrent-pool.js
CHANGED
|
@@ -15,7 +15,7 @@ import { rmSync, statfsSync } from "node:fs";
|
|
|
15
15
|
import WebTorrent from "webtorrent";
|
|
16
16
|
import { logger } from "../utils/logger.js";
|
|
17
17
|
import { SharedPieceStore, findSharedStore } from "./piece-store/shared-piece-store.js";
|
|
18
|
-
import { Urgency } from "./demand/index.js";
|
|
18
|
+
import { Urgency, urgencyName } from "./demand/index.js";
|
|
19
19
|
import { demandFor, forgetTorrent, reconcileAll } from "./download/registry.js";
|
|
20
20
|
import { isAtAWatchingViewer, isBehindEverybody, isNobodyComingNow } from "./priority/PriorityMap.js";
|
|
21
21
|
import { deriveSourceKey } from "./torrent-source-key.js";
|
|
@@ -682,6 +682,12 @@ export function dhtNodeCount(client) {
|
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
+
// The last shape said out loud per torrent, so an unchanged one is not
|
|
686
|
+
// repeated. Held here rather than on the pool because `applyPriorityMap`
|
|
687
|
+
// reads nothing but its arguments — which is what lets it be exercised
|
|
688
|
+
// without building a pool, a torrent client or a thread.
|
|
689
|
+
const lastMapSaid = new WeakMap();
|
|
690
|
+
|
|
685
691
|
export class TorrentPool {
|
|
686
692
|
/**
|
|
687
693
|
* In-flight `client.add()` promises keyed by the same key as `torrents`.
|
|
@@ -1099,6 +1105,37 @@ export class TorrentPool {
|
|
|
1099
1105
|
register.withdraw(window.claimant);
|
|
1100
1106
|
}
|
|
1101
1107
|
}
|
|
1108
|
+
// WHAT THE SWARM WAS ACTUALLY TOLD, said on change and never on a timer.
|
|
1109
|
+
// The map was applied in silence: that it had been BUILT was visible in the
|
|
1110
|
+
// encoding's own line, and that the download had received it was visible
|
|
1111
|
+
// nowhere at all — so "is the map reaching the swarm" could only be taken
|
|
1112
|
+
// on trust. Field 2026-09-08: not one line about it in a whole session.
|
|
1113
|
+
//
|
|
1114
|
+
// Per LEVEL rather than per zone, because the register has five levels and
|
|
1115
|
+
// the map has as many bands as the film needs; the fit between them is the
|
|
1116
|
+
// one thing here that could be wrong, and this is what shows it. Megabytes,
|
|
1117
|
+
// because that is what a swarm delivers.
|
|
1118
|
+
const byLevel = new Map();
|
|
1119
|
+
for (const zone of ordered) {
|
|
1120
|
+
const level = levelOf(zone);
|
|
1121
|
+
const seconds = Math.max(0, zone.to - zone.from);
|
|
1122
|
+
const held = byLevel.get(level) ?? { zones: 0, megabytes: 0 };
|
|
1123
|
+
held.zones += 1;
|
|
1124
|
+
held.megabytes += (seconds / duration) * length / 1048576;
|
|
1125
|
+
byLevel.set(level, held);
|
|
1126
|
+
}
|
|
1127
|
+
const shape = [...byLevel.entries()]
|
|
1128
|
+
.sort((left, right) => right[0] - left[0])
|
|
1129
|
+
.map(([level, held]) => `${urgencyName(level)} ${held.zones} zone(s) ${held.megabytes.toFixed(0)}MB`)
|
|
1130
|
+
.join(", ");
|
|
1131
|
+
const said = `${fileIndex}:${shape}`;
|
|
1132
|
+
if (lastMapSaid.get(torrent) !== said) {
|
|
1133
|
+
lastMapSaid.set(torrent, said);
|
|
1134
|
+
logger.info(
|
|
1135
|
+
`torrent-pool: the swarm is told, for "${file.name}": ${shape || "nothing"} ` +
|
|
1136
|
+
`(${ordered.length} band(s) of the map, over ${Math.round(duration)}s of film)`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1102
1139
|
}
|
|
1103
1140
|
|
|
1104
1141
|
#reportStalledDownloads() {
|
|
@@ -228,12 +228,7 @@ export class TorrentWorkerClient {
|
|
|
228
228
|
// Fail everything outstanding rather than leaving callers hanging: a dead
|
|
229
229
|
// worker will never answer, and a stalled request is worse than an error
|
|
230
230
|
// the loading flow can retry.
|
|
231
|
-
|
|
232
|
-
this.#caller.rejectAll(reason);
|
|
233
|
-
for (const [, read] of this.#reads) {
|
|
234
|
-
read.fail(reason);
|
|
235
|
-
}
|
|
236
|
-
this.#reads.clear();
|
|
231
|
+
this.#failEverythingOutstanding(new Error("Torrent worker stopped unexpectedly."));
|
|
237
232
|
});
|
|
238
233
|
|
|
239
234
|
// A worker that ENDS was, until now, not noticed at all: only `message` and
|
|
@@ -252,15 +247,35 @@ export class TorrentWorkerClient {
|
|
|
252
247
|
`torrent-worker: thread ended on its own with code ${code} — nobody asked it to. ` +
|
|
253
248
|
"Everything waiting on it is failed; the proxy has no torrent client until it is rebuilt."
|
|
254
249
|
);
|
|
255
|
-
|
|
256
|
-
this.#caller.rejectAll(reason);
|
|
257
|
-
for (const [, read] of this.#reads) {
|
|
258
|
-
read.fail(reason);
|
|
259
|
-
}
|
|
260
|
-
this.#reads.clear();
|
|
250
|
+
this.#failEverythingOutstanding(new Error("Torrent worker ended unexpectedly."));
|
|
261
251
|
});
|
|
262
252
|
}
|
|
263
253
|
|
|
254
|
+
/**
|
|
255
|
+
* A dead thread will never answer, so everything waiting on it is failed.
|
|
256
|
+
*
|
|
257
|
+
* INCLUDING THE FRAGMENT READS, which is what ffmpeg's input is. Both death
|
|
258
|
+
* handlers walked `#reads` alone and left `#fragmentReaders` untouched, so an
|
|
259
|
+
* encoder's input neither ended nor errored — it went quiet. Field 2026-08-31,
|
|
260
|
+
* three times: the thread died, both ffmpeg runs stayed ALIVE and stopped
|
|
261
|
+
* producing (167 `holding segment-00085.mp4 … encoder alive` lines), the
|
|
262
|
+
* browser drained its cushion and then retried a segment that would never
|
|
263
|
+
* exist, for ever, with nothing shown to the viewer.
|
|
264
|
+
*
|
|
265
|
+
* @param {Error} reason
|
|
266
|
+
*/
|
|
267
|
+
#failEverythingOutstanding(reason) {
|
|
268
|
+
this.#caller.rejectAll(reason);
|
|
269
|
+
for (const [, read] of this.#reads) {
|
|
270
|
+
read.fail(reason);
|
|
271
|
+
}
|
|
272
|
+
this.#reads.clear();
|
|
273
|
+
for (const [, reader] of this.#fragmentReaders) {
|
|
274
|
+
reader.fail(reason);
|
|
275
|
+
}
|
|
276
|
+
this.#fragmentReaders.clear();
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
/**
|
|
265
280
|
* Add (or join) a torrent and register it under `sourceKey`.
|
|
266
281
|
*
|
|
@@ -66,7 +66,7 @@ function over(sessions) {
|
|
|
66
66
|
publish: () => {},
|
|
67
67
|
viewersOf: (session) => viewersOf(session),
|
|
68
68
|
allowanceFor: () => 10,
|
|
69
|
-
watchedBy: (session, viewer) => live.
|
|
69
|
+
watchedBy: (session, viewer) => !live.supersededBy(session, viewer.activeVariantId ?? null)
|
|
70
70
|
});
|
|
71
71
|
return {
|
|
72
72
|
priority,
|
|
@@ -162,7 +162,7 @@ test("a viewer nothing has been heard from is not watching anything", () => {
|
|
|
162
162
|
publish: () => {},
|
|
163
163
|
viewersOf: (session) => viewersOf(session),
|
|
164
164
|
allowanceFor: () => 10,
|
|
165
|
-
watchedBy: (session, viewer) => live.
|
|
165
|
+
watchedBy: (session, viewer) => !live.supersededBy(session, viewer.activeVariantId ?? null)
|
|
166
166
|
});
|
|
167
167
|
const person = viewers.of(picture, "p");
|
|
168
168
|
person.moveTo(300);
|
|
@@ -201,11 +201,11 @@ test("the picture is watched by a person who never moved off it", () => {
|
|
|
201
201
|
const { viewers, live } = over([picture]);
|
|
202
202
|
const person = viewers.of(picture, "p");
|
|
203
203
|
|
|
204
|
-
assert.equal(live.
|
|
204
|
+
assert.equal(live.supersededBy(picture, null), false, "no step is active");
|
|
205
205
|
person.activeVariantId = "pic";
|
|
206
206
|
assert.equal(
|
|
207
|
-
live.
|
|
208
|
-
|
|
207
|
+
live.supersededBy(picture, person.activeVariantId),
|
|
208
|
+
false,
|
|
209
209
|
"and naming the picture itself as the step is the same statement"
|
|
210
210
|
);
|
|
211
211
|
});
|
|
@@ -302,13 +302,13 @@ test("a segment request hands the encoder to the variant the viewer moved to", a
|
|
|
302
302
|
// handed the whole film's priority map — started it again on the very next
|
|
303
303
|
// pass, which this viewer's own move had just triggered.
|
|
304
304
|
assert.equal(
|
|
305
|
-
manager.liveOutputs.
|
|
306
|
-
|
|
305
|
+
manager.liveOutputs.supersededBy(base, viewerOf(base, "").activeVariantId),
|
|
306
|
+
true,
|
|
307
307
|
"the picture they stepped off is nobody's now"
|
|
308
308
|
);
|
|
309
309
|
assert.equal(
|
|
310
|
-
manager.liveOutputs.
|
|
311
|
-
|
|
310
|
+
manager.liveOutputs.supersededBy(variant, viewerOf(variant, "").activeVariantId),
|
|
311
|
+
false,
|
|
312
312
|
"and the rung they moved to is theirs"
|
|
313
313
|
);
|
|
314
314
|
assert.deepEqual(encoder.signals, [], "stopping it is the plan's, from that fact, and not this path's");
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether the priority map is being served in its own order.
|
|
3
|
+
*
|
|
4
|
+
* The map says what matters most. Until 2026-09-08 nothing said whether what
|
|
5
|
+
* mattered most was delivered first, for either of the two things that read it
|
|
6
|
+
* — so a map read backwards would have looked identical in every log line the
|
|
7
|
+
* proxy writes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { bandOf, WaitLedger } from "../services/priority/WaitLedger.js";
|
|
13
|
+
import { SegmentDemand } from "../services/encode/SegmentDemand.js";
|
|
14
|
+
|
|
15
|
+
test("the top rank is its own band, and the rest fall behind it", () => {
|
|
16
|
+
// The map's ranks are as many as the film needs, and a table with a hundred
|
|
17
|
+
// rows says nothing a reader can hold. What is being asked is coarse.
|
|
18
|
+
assert.equal(bandOf(100, 100), "now", "where the viewer is standing");
|
|
19
|
+
assert.equal(bandOf(95, 100), "soon", "what they reach while watching what they hold");
|
|
20
|
+
assert.equal(bandOf(50, 100), "later", "the rest of the film");
|
|
21
|
+
assert.equal(bandOf(0, 100), "later", "in nobody's zone at all");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("a short map has a top band too", () => {
|
|
25
|
+
// A film with three bands must not report every wait as `later` because its
|
|
26
|
+
// numbers are small: a rank is a position in THIS map, not an absolute.
|
|
27
|
+
assert.equal(bandOf(3, 3), "now");
|
|
28
|
+
assert.equal(bandOf(1, 3), "later");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a band that never waited is said out loud, not reported as zero", () => {
|
|
32
|
+
const ledger = new WaitLedger();
|
|
33
|
+
ledger.note("out:1080", 40, 100, 100);
|
|
34
|
+
|
|
35
|
+
const said = ledger.describe("out:1080");
|
|
36
|
+
assert.match(said, /now 1 wait\(s\) median 40ms worst 40ms/);
|
|
37
|
+
assert.match(said, /soon none/, "silence is not a zero");
|
|
38
|
+
assert.match(said, /later none/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("nothing waited at all reads as nothing, so it cannot be mistaken for good", () => {
|
|
42
|
+
assert.equal(new WaitLedger().describe("out:1080"), null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("the count is the whole run and the median is the recent sample", () => {
|
|
46
|
+
// Two different questions: how often, and how badly.
|
|
47
|
+
const ledger = new WaitLedger();
|
|
48
|
+
for (let n = 0; n < 300; n += 1) {
|
|
49
|
+
ledger.note("out:1080", n, 100, 100);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const said = ledger.describe("out:1080");
|
|
53
|
+
assert.match(said, /now 300 wait\(s\)/, "every wait is counted");
|
|
54
|
+
assert.match(said, /worst 299ms/, "and the worst of what is still held");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("waits of two outputs do not mix", () => {
|
|
58
|
+
const ledger = new WaitLedger();
|
|
59
|
+
ledger.note("out:1080", 5000, 100, 100);
|
|
60
|
+
ledger.note("out:480", 10, 100, 100);
|
|
61
|
+
|
|
62
|
+
assert.match(ledger.describe("out:1080"), /now 1 wait\(s\) median 5000ms/);
|
|
63
|
+
assert.match(ledger.describe("out:480"), /now 1 wait\(s\) median 10ms/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the map answers what it says about one segment, and about itself", () => {
|
|
67
|
+
// The rank alone cannot be read: a wait means one thing at the top of a map
|
|
68
|
+
// and another at the bottom, so the highest rank stated comes with it.
|
|
69
|
+
const demand = new SegmentDemand();
|
|
70
|
+
demand.state("out:1080", [
|
|
71
|
+
{ from: 10, to: 12, priority: 100, withinSeconds: 0 },
|
|
72
|
+
{ from: 13, to: 20, priority: 97, withinSeconds: 4 },
|
|
73
|
+
{ from: 21, to: 90, priority: 90, withinSeconds: 40 }
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(demand.rankOf("out:1080", 11), { rank: 100, topRank: 100 });
|
|
77
|
+
assert.deepEqual(demand.rankOf("out:1080", 15), { rank: 97, topRank: 100 });
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
demand.rankOf("out:1080", 500),
|
|
80
|
+
{ rank: 0, topRank: 100 },
|
|
81
|
+
"in nobody's zone is a statement: nothing is coming for it"
|
|
82
|
+
);
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
demand.rankOf("out:none", 11),
|
|
85
|
+
{ rank: 0, topRank: 0 },
|
|
86
|
+
"and a map that has not been built says nothing at all, which is different"
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("overlapping zones give a segment the highest rank that covers it", () => {
|
|
91
|
+
// Two viewers a few seconds apart state stretches that overlap; the segment
|
|
92
|
+
// is as urgent as the most urgent claim on it.
|
|
93
|
+
const demand = new SegmentDemand();
|
|
94
|
+
demand.state("out:1080", [
|
|
95
|
+
{ from: 0, to: 100, priority: 90, withinSeconds: 40 },
|
|
96
|
+
{ from: 10, to: 12, priority: 100, withinSeconds: 0 }
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
assert.equal(demand.rankOf("out:1080", 11).rank, 100);
|
|
100
|
+
});
|