@torrent-tv/proxy 2.98.0 → 2.98.1
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/package.json +1 -1
- package/services/encode/EncodePlan.js +55 -38
- package/services/encode/output/Timeline.js +6 -18
- package/services/viewer/DownloadMaps.js +61 -5
- package/services/viewer/NativeSourceMap.js +2 -2
- package/services/viewer/PriorityMap.js +319 -200
- package/services/viewer/segment-demands.js +26 -1
package/package.json
CHANGED
|
@@ -991,47 +991,64 @@ function lastOf(windows) {
|
|
|
991
991
|
|
|
992
992
|
function deadlineReaderFor(windows, segmentSeconds) {
|
|
993
993
|
const perSegment = segmentSeconds > 0 ? segmentSeconds : 0;
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// Read through `Number()`, `null` becomes 0 — due NOW — so the film BEHIND
|
|
1011
|
-
// the viewers, which the map marks with exactly that, was the most urgent
|
|
1012
|
-
// material in the file. Everything followed from it: it bought encoders,
|
|
1013
|
-
// it took the run standing in front of the viewer because that run was the
|
|
1014
|
-
// nearest encoder to it, and it did so again on every pass. Field 2026-09-08:
|
|
1015
|
-
// 39 moves in one session, 24 between three adjacent numbers, one viewer
|
|
1016
|
-
// on three encoders, and the picture stood still for 116.7 s in three
|
|
1017
|
-
// interruptions, the worst of them 91.8 s.
|
|
1018
|
-
//
|
|
1019
|
-
// The map has always said it plainly — `{"from":0,"to":57,"priority":1,
|
|
1020
|
-
// "withinSeconds":null,"behind":true}` is in the log of every session — and
|
|
1021
|
-
// this line turned it into its opposite. Fourth time in this repository
|
|
1022
|
-
// that the input to a calculation was not what the calculation assumed.
|
|
1023
|
-
const within = stated === undefined ? 0 : (stated === null ? Number.NaN : Number(stated));
|
|
1024
|
-
if (!Number.isFinite(within)) {
|
|
1025
|
-
// Stated as no time at all: nobody is coming here.
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
994
|
+
// A TABLE BY SEGMENT, filled once per plan. Read for every segment of every
|
|
995
|
+
// arrangement the plan weighs, a search of every window for each index was
|
|
996
|
+
// the plan's own walk times the windows (r26, torrent-tv/meta#178). Filled
|
|
997
|
+
// span by span with the same rule, the soonest of overlapping spans kept.
|
|
998
|
+
let last = -1;
|
|
999
|
+
for (const span of windows) {
|
|
1000
|
+
if (Number.isFinite(span.to) && span.to > last) last = Math.floor(span.to);
|
|
1001
|
+
}
|
|
1002
|
+
const table = new Float64Array(last + 1).fill(Number.POSITIVE_INFINITY);
|
|
1003
|
+
for (const span of windows) {
|
|
1004
|
+
const within = withinOf(span);
|
|
1005
|
+
if (!Number.isFinite(within)) {
|
|
1006
|
+
// Stated as no time at all: nobody is coming here.
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
for (let index = Math.max(0, Math.ceil(span.from)); index <= span.to; index += 1) {
|
|
1028
1010
|
const here = within + (index - span.from) * perSegment;
|
|
1029
|
-
if (here <
|
|
1030
|
-
|
|
1011
|
+
if (here < table[index]) {
|
|
1012
|
+
table[index] = here;
|
|
1031
1013
|
}
|
|
1032
1014
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1015
|
+
}
|
|
1016
|
+
return (index) => (index >= 0 && index < table.length ? table[index] : Number.POSITIVE_INFINITY);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* When a window is first needed, in seconds from now: NaN where it says nobody
|
|
1021
|
+
* is coming.
|
|
1022
|
+
*
|
|
1023
|
+
* A stretch stated with no time is somebody waiting at its near edge: that
|
|
1024
|
+
* is what stating one means. The rest of it grows with the distance, the
|
|
1025
|
+
* same as a stated one — read as due all at once instead, a window as wide
|
|
1026
|
+
* as a viewer's cushion demanded its far end instantly and bought an
|
|
1027
|
+
* encoder to stand beside one already working.
|
|
1028
|
+
* `null` IS A STATEMENT AND IT SAYS NOBODY IS COMING. `undefined` is the
|
|
1029
|
+
* absence of one, and a caller that knows only a position is somebody
|
|
1030
|
+
* waiting at it.
|
|
1031
|
+
*
|
|
1032
|
+
* Read through `Number()`, `null` becomes 0 — due NOW — so the film BEHIND
|
|
1033
|
+
* the viewers, which the map marks with exactly that, was the most urgent
|
|
1034
|
+
* material in the file. Everything followed from it: it bought encoders,
|
|
1035
|
+
* it took the run standing in front of the viewer because that run was the
|
|
1036
|
+
* nearest encoder to it, and it did so again on every pass. Field 2026-09-08:
|
|
1037
|
+
* 39 moves in one session, 24 between three adjacent numbers, one viewer
|
|
1038
|
+
* on three encoders, and the picture stood still for 116.7 s in three
|
|
1039
|
+
* interruptions, the worst of them 91.8 s.
|
|
1040
|
+
*
|
|
1041
|
+
* The map has always said it plainly — `{"from":0,"to":57,"priority":1,
|
|
1042
|
+
* "withinSeconds":null,"behind":true}` is in the log of every session — and
|
|
1043
|
+
* that reading turned it into its opposite. Fourth time in this repository
|
|
1044
|
+
* that the input to a calculation was not what the calculation assumed.
|
|
1045
|
+
*
|
|
1046
|
+
* @param {WantedSpan} span
|
|
1047
|
+
* @returns {number}
|
|
1048
|
+
*/
|
|
1049
|
+
function withinOf(span) {
|
|
1050
|
+
const stated = /** @type {{ withinSeconds?: number }} */ (span).withinSeconds;
|
|
1051
|
+
return stated === undefined ? 0 : (stated === null ? Number.NaN : Number(stated));
|
|
1035
1052
|
}
|
|
1036
1053
|
|
|
1037
1054
|
/**
|
|
@@ -156,8 +156,8 @@ export class Timeline {
|
|
|
156
156
|
* It lived in the session manager, which is a place, not a layer. Here it is
|
|
157
157
|
* beside the table it converts against.
|
|
158
158
|
*
|
|
159
|
-
* @param {import("../../viewer/PriorityMap.js").PriorityMap} map -
|
|
160
|
-
*
|
|
159
|
+
* @param {import("../../viewer/PriorityMap.js").PriorityMap} map - What every
|
|
160
|
+
* viewer of this output wants, in seconds of film.
|
|
161
161
|
* @param {number} segmentCount - How many pieces this output has.
|
|
162
162
|
* @param {{ runsPastEnd?: boolean }} [shape] - `runsPastEnd`: the output's
|
|
163
163
|
* pieces copy a stream, which is cut where its packets end, so a piece
|
|
@@ -188,22 +188,10 @@ export class Timeline {
|
|
|
188
188
|
const until = index + 1 < segmentCount
|
|
189
189
|
? Math.max(from + 1, runsPastEnd ? Math.floor(end) + 1 : Math.ceil(end))
|
|
190
190
|
: map.durationSeconds;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
for (let second = from; second < until && second < map.durationSeconds; second += 1) {
|
|
196
|
-
if (map.priority[second] > priority) {
|
|
197
|
-
priority = map.priority[second];
|
|
198
|
-
}
|
|
199
|
-
if (map.secondsUntilPlayed[second] < withinSeconds) {
|
|
200
|
-
withinSeconds = map.secondsUntilPlayed[second];
|
|
201
|
-
}
|
|
202
|
-
if (map.behind[second] === 0) {
|
|
203
|
-
behind = false;
|
|
204
|
-
}
|
|
205
|
-
if (map.urgent?.[second]) urgent = true;
|
|
206
|
-
}
|
|
191
|
+
// Asked of the map for the piece's seconds as a whole, not second by
|
|
192
|
+
// second: a walk of every second of the film for every output on every
|
|
193
|
+
// plan (torrent-tv/meta#178).
|
|
194
|
+
const { priority, withinSeconds, behind, urgent } = map.on(from, until);
|
|
207
195
|
if (priority === 0) {
|
|
208
196
|
continue;
|
|
209
197
|
}
|
|
@@ -39,6 +39,7 @@ export class DownloadMaps {
|
|
|
39
39
|
const now = Date.now();
|
|
40
40
|
file.sourceZones = zones.map(zone => Number.isFinite(zone.deadlineAt) || !Number.isFinite(zone.withinSeconds)
|
|
41
41
|
? zone : { ...zone, deadlineAt: now + Math.max(0, zone.withinSeconds) * 1000 });
|
|
42
|
+
const sourceZones = zoneIndexOf(file);
|
|
42
43
|
let changed = false;
|
|
43
44
|
// Until the new zones are resolved to bytes, a resolved zone stays while
|
|
44
45
|
// the film it covers is still wanted. A viewer moving on re-cuts every zone
|
|
@@ -49,9 +50,9 @@ export class DownloadMaps {
|
|
|
49
50
|
// a seek away — is withdrawn at once.
|
|
50
51
|
const retained = file.zones.flatMap(zone => {
|
|
51
52
|
const interval = zone.downloadInterval ?? zone;
|
|
52
|
-
const current =
|
|
53
|
+
const current = sourceZones.exactly(interval);
|
|
53
54
|
if (!current) {
|
|
54
|
-
const stillWanted =
|
|
55
|
+
const stillWanted = sourceZones.overlaps(interval);
|
|
55
56
|
if (!stillWanted) changed = true;
|
|
56
57
|
return stillWanted ? [zone] : [];
|
|
57
58
|
}
|
|
@@ -76,7 +77,7 @@ export class DownloadMaps {
|
|
|
76
77
|
changed = true;
|
|
77
78
|
}
|
|
78
79
|
else if (metadata.interval) {
|
|
79
|
-
const current =
|
|
80
|
+
const current = sourceZones.exactly(metadata.interval);
|
|
80
81
|
const deadlineAt = Number.isFinite(current.deadlineAt) ? current.deadlineAt - metadata.leadSeconds * 1000 : current.deadlineAt;
|
|
81
82
|
if (metadata.priority !== current.priority || metadata.urgent !== current.urgent || metadata.deadlineAt !== deadlineAt) changed = true;
|
|
82
83
|
metadata.priority = current.priority;
|
|
@@ -147,7 +148,8 @@ export class DownloadMaps {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
wantsInterval(sourceKey, fileIndex, { from, to }) {
|
|
150
|
-
|
|
151
|
+
const file = this.#files.get(`${sourceKey}:${fileIndex}`);
|
|
152
|
+
return Boolean(file && zoneIndexOf(file).exactly({ from, to }));
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
metadata({ sourceKey, fileIndex, statement, result, priority = 100, urgent = true, deadlineAt = 0, interval, leadSeconds = 0, scope = "playback", order = 0 }) {
|
|
@@ -155,7 +157,7 @@ export class DownloadMaps {
|
|
|
155
157
|
const file = this.#file(sourceKey, fileIndex);
|
|
156
158
|
if (result.kind === "needs-ranges") {
|
|
157
159
|
if (interval && !this.wantsInterval(sourceKey, fileIndex, interval)) return Promise.resolve();
|
|
158
|
-
const current = interval && file.
|
|
160
|
+
const current = interval && zoneIndexOf(file).exactly(interval);
|
|
159
161
|
if (current) {
|
|
160
162
|
priority = current.priority;
|
|
161
163
|
urgent = current.urgent;
|
|
@@ -372,6 +374,60 @@ export class DownloadMaps {
|
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
|
|
377
|
+
/**
|
|
378
|
+
* The map zones of one statement, looked up by interval.
|
|
379
|
+
*
|
|
380
|
+
* Every zone kept from the last pass asks which zone of the new map it belongs
|
|
381
|
+
* to, by its exact interval, and if none, whether any new zone still overlaps
|
|
382
|
+
* it — a search of every new zone for every kept one, thousands by tens on
|
|
383
|
+
* every statement over a film (r26, torrent-tv/meta#178). The exact match is
|
|
384
|
+
* a table, and the overlap a binary search over the zones, which a map states
|
|
385
|
+
* ascending and apart; zones in any other order are searched as before.
|
|
386
|
+
*/
|
|
387
|
+
class ZoneIndex {
|
|
388
|
+
/** The list indexed: replaced whole, never edited in place, so it says whether the index is current. */
|
|
389
|
+
zones;
|
|
390
|
+
#byInterval = new Map();
|
|
391
|
+
#ordered = true;
|
|
392
|
+
|
|
393
|
+
/** @param {Array<{ from: number, to: number }>} zones */
|
|
394
|
+
constructor(zones) {
|
|
395
|
+
this.zones = zones;
|
|
396
|
+
zones.forEach((zone, index) => {
|
|
397
|
+
const key = `${zone.from}:${zone.to}`;
|
|
398
|
+
// The first of equals, as a search from the start would find.
|
|
399
|
+
if (!this.#byInterval.has(key)) this.#byInterval.set(key, zone);
|
|
400
|
+
if (index > 0 && !(zone.from >= zones[index - 1].to)) this.#ordered = false;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The zone with exactly this interval. @param {{ from: number, to: number }} interval */
|
|
405
|
+
exactly({ from, to }) {
|
|
406
|
+
return this.#byInterval.get(`${from}:${to}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Whether any zone overlaps this interval. @param {{ from: number, to: number }} interval */
|
|
410
|
+
overlaps({ from, to }) {
|
|
411
|
+
const zones = this.zones;
|
|
412
|
+
if (!this.#ordered) return zones.some(zone => zone.from < to && from < zone.to);
|
|
413
|
+
// The first zone that ends after `from`; it overlaps if it begins before `to`.
|
|
414
|
+
let low = 0;
|
|
415
|
+
let high = zones.length;
|
|
416
|
+
while (low < high) {
|
|
417
|
+
const middle = (low + high) >> 1;
|
|
418
|
+
if (zones[middle].to <= from) low = middle + 1;
|
|
419
|
+
else high = middle;
|
|
420
|
+
}
|
|
421
|
+
return low < zones.length && zones[low].from < to;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** The index of a file's map zones, made again when they have been replaced. */
|
|
426
|
+
function zoneIndexOf(file) {
|
|
427
|
+
if (file.sourceIndex?.zones !== file.sourceZones) file.sourceIndex = new ZoneIndex(file.sourceZones);
|
|
428
|
+
return file.sourceIndex;
|
|
429
|
+
}
|
|
430
|
+
|
|
375
431
|
/**
|
|
376
432
|
* Zones resolved so far, with the retained zones of every segment they do not
|
|
377
433
|
* yet resolve again: a segment's bytes are stated once, by its newest reading.
|
|
@@ -15,7 +15,7 @@ export async function nativeOriginalSourceMap({ container, viewers, durationSeco
|
|
|
15
15
|
const input = await container.readMappedSourceRanges({ from: zone.from + startTimeSeconds,
|
|
16
16
|
to: Math.min(durationSeconds, zone.to) + startTimeSeconds });
|
|
17
17
|
if (input.kind !== "result") continue;
|
|
18
|
-
const links = viewers.filter((_viewer, index) => maps[index].
|
|
18
|
+
const links = viewers.filter((_viewer, index) => maps[index].at(zone.from).priority === zone.priority)
|
|
19
19
|
.map(viewer => viewer.linkReading()?.linkMbps).filter(rate => Number.isFinite(rate) && rate > 0);
|
|
20
20
|
const bytes = input.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0);
|
|
21
21
|
const deliverySeconds = links.length ? bytes * 8 / (Math.min(...links) * 1_000_000) : 0;
|
|
@@ -46,7 +46,7 @@ export function nativeSourceMap({ index, tracks, viewers, durationSeconds, start
|
|
|
46
46
|
const input = index.inputFor({ trackId: track.trackNumber, from, to });
|
|
47
47
|
if (input.kind === "needs-index") continue;
|
|
48
48
|
if (input.kind !== "result") throw new Error(`Native source input refused: ${input.reason}`);
|
|
49
|
-
const links = viewers.filter((_viewer, viewerIndex) => maps[viewerIndex].
|
|
49
|
+
const links = viewers.filter((_viewer, viewerIndex) => maps[viewerIndex].at(zone.from).priority === zone.priority)
|
|
50
50
|
.map(viewer => viewer.linkReading()?.linkMbps).filter(rate => Number.isFinite(rate) && rate > 0);
|
|
51
51
|
const bytes = input.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0);
|
|
52
52
|
const deliverySeconds = links.length ? bytes * 8 / (Math.min(...links) * 1_000_000) : 0;
|
|
@@ -44,34 +44,45 @@
|
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* THE MAP ITSELF:
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* a list of stretches
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
47
|
+
* THE MAP ITSELF: what each viewer wants, kept as the rules that say it, and
|
|
48
|
+
* answered for a second, for a stretch, or as the stretches of one value.
|
|
49
|
+
*
|
|
50
|
+
* The rules are the ones the map has had since 2026-09-06, when an array of one
|
|
51
|
+
* number per second of film replaced a list of stretches: a priority for every
|
|
52
|
+
* second, the time until a viewer plays it, and whether it lies behind every
|
|
53
|
+
* viewer, is wanted before playback can continue, or has been yielded by a
|
|
54
|
+
* viewer who stopped. What changed (torrent-tv/meta#178) is only how they are
|
|
55
|
+
* held. Every one of those values is a step function of the distance from a
|
|
56
|
+
* viewer, with steps the rules themselves place — where the viewer stands, the
|
|
57
|
+
* end of the urgent allowance, the doubling bands of priority — and the time is
|
|
58
|
+
* that distance. Built as arrays the length of the film, a map cost a pass over
|
|
59
|
+
* every second for every viewer and again for the merge, on every move of a
|
|
60
|
+
* viewer: on Home Assistant the main thread built them 21-61 times a minute
|
|
61
|
+
* over a two-hour film (r26). Held as rules, a question costs what the viewers
|
|
62
|
+
* and their steps cost, and nothing per second of film.
|
|
63
|
+
*
|
|
64
|
+
* Two different questions are asked of the map and only one of them can be
|
|
65
|
+
* answered by a rank:
|
|
55
66
|
*
|
|
56
67
|
* - `priority` orders the work. Only the ORDER between values is meaningful;
|
|
57
68
|
* the scale below says what the numbers separate;
|
|
58
|
-
* -
|
|
59
|
-
* computable at all: whoever fills the map compares "when is this
|
|
60
|
-
* against "when would it arrive", and only the second half is theirs
|
|
61
|
-
* out. `Infinity` where nobody is on their way.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
69
|
+
* - the time until a second is played is a quantity, and it is what makes
|
|
70
|
+
* lateness computable at all: whoever fills the map compares "when is this
|
|
71
|
+
* needed" against "when would it arrive", and only the second half is theirs
|
|
72
|
+
* to work out. `Infinity` where nobody is on their way.
|
|
73
|
+
*
|
|
74
|
+
* The merge keeps what it always did: the highest priority and the soonest
|
|
75
|
+
* time, taken separately — two viewers can tie on the coarse priority while one
|
|
76
|
+
* is genuinely nearer; in front of ANYBODY is in front; a second is urgent if
|
|
77
|
+
* any viewer needs it before playback can continue; and it is deferred only if
|
|
78
|
+
* every viewer in front of it has yielded it.
|
|
79
|
+
*
|
|
80
|
+
* @typedef {object} MapPoint
|
|
81
|
+
* @property {number} priority - Zero where nobody wants it.
|
|
82
|
+
* @property {number} untilSeconds - Seconds until a viewer plays it.
|
|
83
|
+
* @property {boolean} behind - Behind every viewer.
|
|
84
|
+
* @property {boolean} urgent
|
|
85
|
+
* @property {boolean} deferred
|
|
75
86
|
*/
|
|
76
87
|
|
|
77
88
|
/**
|
|
@@ -87,6 +98,7 @@
|
|
|
87
98
|
* @property {boolean} behind - Whether this lies behind every viewer.
|
|
88
99
|
*/
|
|
89
100
|
|
|
101
|
+
|
|
90
102
|
/**
|
|
91
103
|
* Wanted seconds use priorities 1..100. Distance lowers priority relative to
|
|
92
104
|
* the measured urgent allowance. Pause preserves the same priorities until
|
|
@@ -167,28 +179,27 @@ function stepsAway(distanceSeconds, allowanceSeconds) {
|
|
|
167
179
|
return Math.floor(Math.log2(1 + distanceSeconds / allowanceSeconds));
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
/** Where nothing is wanted. */
|
|
183
|
+
const NO_DEMAND = Object.freeze({ priority: NOTHING, untilSeconds: Number.POSITIVE_INFINITY, behind: true, urgent: false, deferred: false });
|
|
184
|
+
|
|
170
185
|
/**
|
|
171
|
-
*
|
|
186
|
+
* The first whole distance whose step is at least `step`, worked out by the
|
|
187
|
+
* same arithmetic the step is read with, so a boundary is exactly where the
|
|
188
|
+
* value changes.
|
|
172
189
|
*
|
|
173
|
-
* @param {number}
|
|
174
|
-
* @
|
|
190
|
+
* @param {number} step
|
|
191
|
+
* @param {number} allowanceSeconds
|
|
192
|
+
* @returns {number}
|
|
175
193
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
durationSeconds: seconds,
|
|
182
|
-
priority: new Uint8Array(seconds),
|
|
183
|
-
secondsUntilPlayed: new Float64Array(seconds).fill(Number.POSITIVE_INFINITY),
|
|
184
|
-
behind: new Uint8Array(seconds).fill(1),
|
|
185
|
-
urgent: new Uint8Array(seconds),
|
|
186
|
-
deferred: new Uint8Array(seconds)
|
|
187
|
-
};
|
|
194
|
+
function firstDistanceOfStep(step, allowanceSeconds) {
|
|
195
|
+
let distance = Math.ceil(allowanceSeconds * (2 ** step - 1));
|
|
196
|
+
while (distance > 0 && stepsAway(distance - 1, allowanceSeconds) >= step) distance -= 1;
|
|
197
|
+
while (stepsAway(distance, allowanceSeconds) < step) distance += 1;
|
|
198
|
+
return distance;
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
/**
|
|
191
|
-
* One viewer's map.
|
|
202
|
+
* One viewer's demand: the rules of their map, with where they stand.
|
|
192
203
|
*
|
|
193
204
|
* **The number is a reading of how soon they will be there.** A viewer watching
|
|
194
205
|
* forward reaches second `x` after `x - p` seconds of film. That distance — not
|
|
@@ -198,195 +209,303 @@ export function emptyMap(durationSeconds) {
|
|
|
198
209
|
* Pause retains urgent preparation first. Once that work is ready, the
|
|
199
210
|
* allowance / (allowance + pauseSeconds) coefficient gradually reduces this
|
|
200
211
|
* viewer's priorities if others consume the file. Resume restores them.
|
|
201
|
-
*
|
|
202
|
-
* **What must NOT be here**, and the boundary is the point: how fast this
|
|
203
|
-
* machine encodes, how many encoders that takes, what a second weighs in bytes,
|
|
204
|
-
* where a piece boundary falls, and what has already been made. Those are
|
|
205
|
-
* answers the encoding and the downloading work out for themselves, from this
|
|
206
|
-
* map and from what each knows about itself.
|
|
207
|
-
*
|
|
208
|
-
* @param {object} params
|
|
209
|
-
* @param {number} params.atSeconds - Where they are watching from.
|
|
210
|
-
* @param {number} params.durationSeconds - How long the film is.
|
|
211
|
-
* @param {number} params.allowanceSeconds - The measured depth below which an
|
|
212
|
-
* interruption reaches this viewer. The width of the first step.
|
|
213
|
-
* @param {boolean} [params.playing] - Whether the picture is moving.
|
|
214
|
-
* @returns {PriorityMap}
|
|
215
212
|
*/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
213
|
+
class ViewerDemand {
|
|
214
|
+
/**
|
|
215
|
+
* @param {{ atSeconds: number, durationSeconds: number, allowanceSeconds: number, playing?: boolean,
|
|
216
|
+
* pauseSeconds?: number, urgentReady?: boolean, viewerCount?: number }} params
|
|
217
|
+
*/
|
|
218
|
+
constructor({ atSeconds, durationSeconds, allowanceSeconds, playing = true, pauseSeconds = 0, urgentReady = false, viewerCount = 1 }) {
|
|
219
|
+
this.durationSeconds = Number.isFinite(durationSeconds) && durationSeconds > 0 ? Math.ceil(durationSeconds) : 0;
|
|
220
|
+
this.at = Number.isFinite(atSeconds) && atSeconds > 0 ? Math.floor(atSeconds) : 0;
|
|
221
|
+
this.allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0 ? allowanceSeconds : this.durationSeconds;
|
|
222
|
+
// The measured urgent span is the time scale. There is no chosen decay
|
|
223
|
+
// period: one span on pause halves the excess above minimum priority.
|
|
224
|
+
this.attenuate = playing === false && urgentReady && viewerCount > 1;
|
|
225
|
+
this.elapsed = Number.isFinite(pauseSeconds) ? Math.max(0, pauseSeconds) : 0;
|
|
226
|
+
this.coefficient = pauseCoefficient({ playing, urgentReady, viewerCount, pauseSeconds: this.elapsed, allowanceSeconds: this.allowance });
|
|
227
|
+
}
|
|
222
228
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return map;
|
|
229
|
+
/** The priority at a whole distance in front of the viewer. @param {number} distance */
|
|
230
|
+
priorityAt(distance) {
|
|
231
|
+
const priority = Math.max(NOBODY_IS_COMING, AT_A_WATCHING_VIEWER - stepsAway(distance, this.allowance));
|
|
232
|
+
return Math.max(NOBODY_IS_COMING, 1 + Math.floor((priority - 1) * this.coefficient));
|
|
228
233
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const coefficient = pauseCoefficient({ playing, urgentReady, viewerCount, pauseSeconds: elapsed, allowanceSeconds: allowance });
|
|
239
|
-
for (let second = 0; second < map.durationSeconds; second += 1) {
|
|
240
|
-
if (second < at) {
|
|
241
|
-
map.priority[second] = NOBODY_IS_COMING;
|
|
242
|
-
continue;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {number} second
|
|
237
|
+
* @returns {MapPoint | null} Null where the film does not reach.
|
|
238
|
+
*/
|
|
239
|
+
pointAt(second) {
|
|
240
|
+
if (!(second >= 0 && second < this.durationSeconds)) return null;
|
|
241
|
+
if (second < this.at) {
|
|
242
|
+
return { priority: NOBODY_IS_COMING, untilSeconds: Number.POSITIVE_INFINITY, behind: true, urgent: false, deferred: false };
|
|
243
243
|
}
|
|
244
|
-
|
|
245
|
-
const distance = second - at;
|
|
246
|
-
const priority = Math.max(NOBODY_IS_COMING, AT_A_WATCHING_VIEWER - stepsAway(distance, allowance));
|
|
247
|
-
map.priority[second] = Math.max(NOBODY_IS_COMING, 1 + Math.floor((priority - 1) * coefficient));
|
|
248
|
-
map.urgent[second] = distance < allowance && !attenuate ? 1 : 0;
|
|
249
|
-
map.deferred[second] = attenuate ? 1 : 0;
|
|
244
|
+
const distance = second - this.at;
|
|
250
245
|
// Urgent preparation keeps its deadline during pause. After completion,
|
|
251
246
|
// pause duration defers this viewer's work without altering other viewers.
|
|
252
|
-
|
|
247
|
+
return { priority: this.priorityAt(distance), untilSeconds: distance + (this.attenuate ? this.elapsed : 0), behind: false,
|
|
248
|
+
urgent: distance < this.allowance && !this.attenuate, deferred: this.attenuate };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Every second where one of this viewer's values may change, inside the film. @returns {number[]} */
|
|
252
|
+
breaks() {
|
|
253
|
+
const breaks = [this.at, this.durationSeconds, this.at + Math.ceil(this.allowance)];
|
|
254
|
+
for (let step = 1; step < AT_A_WATCHING_VIEWER - NOBODY_IS_COMING; step += 1) {
|
|
255
|
+
const second = this.at + firstDistanceOfStep(step, this.allowance);
|
|
256
|
+
if (second >= this.durationSeconds) break;
|
|
257
|
+
breaks.push(second);
|
|
258
|
+
}
|
|
259
|
+
return breaks.filter(second => second > 0 && second <= this.durationSeconds);
|
|
253
260
|
}
|
|
254
|
-
return map;
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
/**
|
|
258
|
-
*
|
|
259
|
-
* the second before it.
|
|
260
|
-
*
|
|
261
|
-
* A viewer's own map is highest where they stand and falls away in front of
|
|
262
|
-
* them, so in a merged map each viewer's position is a step UP — the one place
|
|
263
|
-
* a value is higher than the value just behind it. A viewer standing inside the
|
|
264
|
-
* stretch somebody else is walking towards adds nothing that map does not
|
|
265
|
-
* already ask for, and is not a step; neither is somebody behind everybody.
|
|
266
|
-
*
|
|
267
|
-
* Read by whoever orders work by where people are and must not know what a
|
|
268
|
-
* viewer is — the subtitle walk reads the clusters at these times first.
|
|
269
|
-
*
|
|
270
|
-
* @param {PriorityMap | null | undefined} map
|
|
271
|
-
* @returns {number[]} Seconds, ascending.
|
|
264
|
+
* Every viewer's demand on one film, merged: the map.
|
|
272
265
|
*/
|
|
273
|
-
export
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
266
|
+
export class PriorityMap {
|
|
267
|
+
/** @type {ViewerDemand[]} */
|
|
268
|
+
#viewers;
|
|
269
|
+
/** The film between the breaks, each stretch with its value at its start; worked out once. */
|
|
270
|
+
#pieces = null;
|
|
271
|
+
|
|
272
|
+
/** @param {ViewerDemand[]} [viewers] */
|
|
273
|
+
constructor(viewers = []) {
|
|
274
|
+
this.#viewers = viewers.filter(viewer => viewer.durationSeconds > 0);
|
|
275
|
+
/** The film's length, in whole seconds. */
|
|
276
|
+
this.durationSeconds = this.#viewers.reduce((longest, viewer) => Math.max(longest, viewer.durationSeconds), 0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The viewers' demands, for a merge. */
|
|
280
|
+
get viewers() {
|
|
281
|
+
return this.#viewers;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* What is wanted at one second.
|
|
286
|
+
*
|
|
287
|
+
* @param {number} second
|
|
288
|
+
* @returns {MapPoint}
|
|
289
|
+
*/
|
|
290
|
+
at(second) {
|
|
291
|
+
let priority = NOTHING;
|
|
292
|
+
let untilSeconds = Number.POSITIVE_INFINITY;
|
|
293
|
+
let behind = true;
|
|
294
|
+
let urgent = false;
|
|
295
|
+
let yieldedByAll = true;
|
|
296
|
+
for (const viewer of this.#viewers) {
|
|
297
|
+
const point = viewer.pointAt(second);
|
|
298
|
+
if (!point) continue;
|
|
299
|
+
if (point.priority > priority) priority = point.priority;
|
|
300
|
+
if (point.untilSeconds < untilSeconds) untilSeconds = point.untilSeconds;
|
|
301
|
+
if (!point.behind) {
|
|
302
|
+
// In front of ANYBODY is in front: a stretch one viewer has passed is
|
|
303
|
+
// still the film another is walking towards.
|
|
304
|
+
behind = false;
|
|
305
|
+
// Deferred only if every viewer needing this second has yielded it.
|
|
306
|
+
if (!point.deferred) yieldedByAll = false;
|
|
307
|
+
}
|
|
308
|
+
if (point.urgent) urgent = true;
|
|
309
|
+
}
|
|
310
|
+
if (priority === NOTHING) return NO_DEMAND;
|
|
311
|
+
return { priority, untilSeconds, behind, urgent, deferred: !behind && yieldedByAll };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* What a stretch of seconds wants as a whole, taken as a piece made or not
|
|
316
|
+
* made whole is wanted: as much as its strongest second, as soon as its
|
|
317
|
+
* soonest one, in front of somebody if any second of it is, urgent if any
|
|
318
|
+
* second of it is.
|
|
319
|
+
*
|
|
320
|
+
* A viewer's priority does not rise with distance and their time rises a
|
|
321
|
+
* second a second, so the second of the stretch nearest to them answers both.
|
|
322
|
+
*
|
|
323
|
+
* @param {number} from - First second, inclusive.
|
|
324
|
+
* @param {number} to - Last second, exclusive.
|
|
325
|
+
* @returns {{ priority: number, withinSeconds: number, behind: boolean, urgent: boolean }}
|
|
326
|
+
* Priority zero where nobody wants any of it.
|
|
327
|
+
*/
|
|
328
|
+
on(from, to) {
|
|
329
|
+
let priority = NOTHING;
|
|
330
|
+
let withinSeconds = Number.POSITIVE_INFINITY;
|
|
331
|
+
let behind = true;
|
|
332
|
+
let urgent = false;
|
|
333
|
+
const pieces = this.#piecesOfFilm();
|
|
334
|
+
// The first stretch that ends after `from`.
|
|
335
|
+
let low = 0;
|
|
336
|
+
let high = pieces.length;
|
|
337
|
+
while (low < high) {
|
|
338
|
+
const middle = (low + high) >> 1;
|
|
339
|
+
if (pieces[middle].to <= from) low = middle + 1;
|
|
340
|
+
else high = middle;
|
|
341
|
+
}
|
|
342
|
+
for (let index = low; index < pieces.length && pieces[index].from < to; index += 1) {
|
|
343
|
+
const { from: start, point } = pieces[index];
|
|
344
|
+
if (point.priority === NOTHING) continue;
|
|
345
|
+
if (point.priority > priority) priority = point.priority;
|
|
346
|
+
// Inside a stretch every viewer in front stays in front and their time
|
|
347
|
+
// rises a second a second, so the soonest time rises with it.
|
|
348
|
+
const until = point.untilSeconds + (Math.max(from, start) - start);
|
|
349
|
+
if (until < withinSeconds) withinSeconds = until;
|
|
350
|
+
if (!point.behind) behind = false;
|
|
351
|
+
if (point.urgent) urgent = true;
|
|
352
|
+
}
|
|
353
|
+
return { priority, withinSeconds, behind, urgent };
|
|
277
354
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
355
|
+
|
|
356
|
+
/** @returns {Array<{ from: number, to: number, point: MapPoint }>} */
|
|
357
|
+
#piecesOfFilm() {
|
|
358
|
+
if (this.#pieces) return this.#pieces;
|
|
359
|
+
const breaks = this.#breaks();
|
|
360
|
+
const pieces = [];
|
|
361
|
+
for (let index = 0; index + 1 < breaks.length; index += 1) {
|
|
362
|
+
pieces.push({ from: breaks[index], to: breaks[index + 1], point: this.at(breaks[index]) });
|
|
283
363
|
}
|
|
364
|
+
this.#pieces = pieces;
|
|
365
|
+
return pieces;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Every second where a merged value may change, with 0 and the end, ascending. */
|
|
369
|
+
#breaks() {
|
|
370
|
+
const breaks = new Set([0, this.durationSeconds]);
|
|
371
|
+
for (const viewer of this.#viewers) for (const second of viewer.breaks()) breaks.add(second);
|
|
372
|
+
return [...breaks].filter(second => second >= 0 && second <= this.durationSeconds).sort((left, right) => left - right);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The map as stretches: runs of seconds that agree on everything but the
|
|
377
|
+
* time, ascending, without gaps or overlaps. Seconds nobody wants are left
|
|
378
|
+
* out.
|
|
379
|
+
*
|
|
380
|
+
* THE TIME OF A STRETCH IS THE TIME OF ITS NEAR EDGE. A stretch is met at its
|
|
381
|
+
* beginning, so its beginning is when it must exist; whoever needs the time
|
|
382
|
+
* of a second inside it walks forward from there, at a second of film per
|
|
383
|
+
* second, which is the same arithmetic that put the number here. Merging on
|
|
384
|
+
* the exact time as well would merge nothing at all: the time is the
|
|
385
|
+
* distance, so it differs at every second.
|
|
386
|
+
*
|
|
387
|
+
* @returns {DemandZone[]}
|
|
388
|
+
*/
|
|
389
|
+
runs() {
|
|
390
|
+
/** @type {DemandZone[]} */
|
|
391
|
+
const runs = [];
|
|
392
|
+
for (const { from, to, point } of this.#piecesOfFilm()) {
|
|
393
|
+
if (point.priority === NOTHING) continue;
|
|
394
|
+
const previous = runs[runs.length - 1];
|
|
395
|
+
if (previous && previous.to === from && previous.priority === point.priority && previous.behind === point.behind &&
|
|
396
|
+
previous.urgent === point.urgent && previous.deferred === point.deferred) {
|
|
397
|
+
previous.to = to;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
runs.push({ from, to, priority: point.priority, withinSeconds: point.untilSeconds, behind: point.behind,
|
|
401
|
+
urgent: point.urgent, deferred: point.deferred });
|
|
402
|
+
}
|
|
403
|
+
return runs;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Where viewers stand, read off the map: every second whose priority rises
|
|
408
|
+
* above the second before it.
|
|
409
|
+
*
|
|
410
|
+
* A viewer's own map is highest where they stand and falls away in front of
|
|
411
|
+
* them, so in a merged map each viewer's position is a step UP — the one place
|
|
412
|
+
* a value is higher than the value just behind it. A viewer standing inside the
|
|
413
|
+
* stretch somebody else is walking towards adds nothing that map does not
|
|
414
|
+
* already ask for, and is not a step; neither is somebody behind everybody.
|
|
415
|
+
*
|
|
416
|
+
* Read by whoever orders work by where people are and must not know what a
|
|
417
|
+
* viewer is — the subtitle walk reads the clusters at these times first.
|
|
418
|
+
*
|
|
419
|
+
* @returns {number[]} Seconds, ascending.
|
|
420
|
+
*/
|
|
421
|
+
viewerStarts() {
|
|
422
|
+
const starts = [];
|
|
423
|
+
for (const second of this.#breaks()) {
|
|
424
|
+
if (second >= this.durationSeconds) continue;
|
|
425
|
+
const value = this.at(second).priority;
|
|
426
|
+
const before = second === 0 ? NOBODY_IS_COMING : this.at(second - 1).priority;
|
|
427
|
+
if (value > before && value > NOBODY_IS_COMING) starts.push(second);
|
|
428
|
+
}
|
|
429
|
+
return starts;
|
|
284
430
|
}
|
|
285
|
-
return starts;
|
|
286
431
|
}
|
|
287
432
|
|
|
288
433
|
/**
|
|
289
|
-
*
|
|
434
|
+
* A map with nothing in it, which says the same as nobody watching.
|
|
290
435
|
*
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
436
|
+
* @returns {PriorityMap}
|
|
437
|
+
*/
|
|
438
|
+
export function emptyMap() {
|
|
439
|
+
return new PriorityMap([]);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* How far a viewer's priorities are lowered while they pause beside other
|
|
444
|
+
* viewers: allowance / (allowance + pauseSeconds), once their urgent work is
|
|
445
|
+
* ready; 1 otherwise.
|
|
446
|
+
*
|
|
447
|
+
* @param {{ playing?: boolean, urgentReady?: boolean, viewerCount: number, pauseSeconds?: number, allowanceSeconds?: number }} params
|
|
448
|
+
* @returns {number}
|
|
449
|
+
*/
|
|
450
|
+
export function pauseCoefficient({ playing, urgentReady, viewerCount, pauseSeconds, allowanceSeconds }) {
|
|
451
|
+
if (playing !== false || !urgentReady || viewerCount <= 1) return 1;
|
|
452
|
+
const allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0 ? allowanceSeconds : 1;
|
|
453
|
+
const elapsed = Number.isFinite(pauseSeconds) ? Math.max(0, pauseSeconds) : 0;
|
|
454
|
+
return allowance / (allowance + elapsed);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* One viewer's map.
|
|
296
459
|
*
|
|
297
|
-
*
|
|
460
|
+
* **What must NOT be here**, and the boundary is the point: how fast this
|
|
461
|
+
* machine encodes, how many encoders that takes, what a second weighs in bytes,
|
|
462
|
+
* where a piece boundary falls, and what has already been made. Those are
|
|
463
|
+
* answers the encoding and the downloading work out for themselves, from this
|
|
464
|
+
* map and from what each knows about itself.
|
|
465
|
+
*
|
|
466
|
+
* @param {object} params
|
|
467
|
+
* @param {number} params.atSeconds - Where they are watching from.
|
|
468
|
+
* @param {number} params.durationSeconds - How long the film is.
|
|
469
|
+
* @param {number} params.allowanceSeconds - The measured depth below which an
|
|
470
|
+
* interruption reaches this viewer. The width of the first step.
|
|
471
|
+
* @param {boolean} [params.playing] - Whether the picture is moving.
|
|
472
|
+
* @param {number} [params.pauseSeconds]
|
|
473
|
+
* @param {boolean} [params.urgentReady]
|
|
474
|
+
* @param {number} [params.viewerCount]
|
|
475
|
+
* @returns {PriorityMap}
|
|
476
|
+
*/
|
|
477
|
+
export function mapForViewer(params) {
|
|
478
|
+
return new PriorityMap([new ViewerDemand(params)]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Every viewer's map as one.
|
|
298
483
|
*
|
|
299
484
|
* @param {PriorityMap[]} maps
|
|
300
485
|
* @returns {PriorityMap}
|
|
301
486
|
*/
|
|
302
487
|
export function mergeMaps(maps) {
|
|
303
|
-
|
|
304
|
-
if (all.length === 0) {
|
|
305
|
-
return emptyMap(0);
|
|
306
|
-
}
|
|
307
|
-
const merged = emptyMap(Math.max(...all.map((map) => map.durationSeconds)));
|
|
308
|
-
merged.deferred.fill(1);
|
|
309
|
-
for (const map of all) {
|
|
310
|
-
for (let second = 0; second < map.durationSeconds; second += 1) {
|
|
311
|
-
if (map.priority[second] > merged.priority[second]) {
|
|
312
|
-
merged.priority[second] = map.priority[second];
|
|
313
|
-
}
|
|
314
|
-
if (map.secondsUntilPlayed[second] < merged.secondsUntilPlayed[second]) {
|
|
315
|
-
merged.secondsUntilPlayed[second] = map.secondsUntilPlayed[second];
|
|
316
|
-
}
|
|
317
|
-
// In front of ANYBODY is in front: a stretch one viewer has passed is
|
|
318
|
-
// still the film another is walking towards.
|
|
319
|
-
if (map.behind[second] === 0) {
|
|
320
|
-
merged.behind[second] = 0;
|
|
321
|
-
}
|
|
322
|
-
if (map.urgent?.[second]) merged.urgent[second] = 1;
|
|
323
|
-
// Deferred only if every viewer needing this second has yielded it.
|
|
324
|
-
if (map.behind[second] === 0 && !map.deferred?.[second]) merged.deferred[second] = 0;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
for (let second = 0; second < merged.durationSeconds; second += 1) {
|
|
328
|
-
if (merged.behind[second]) merged.deferred[second] = 0;
|
|
329
|
-
}
|
|
330
|
-
return merged;
|
|
488
|
+
return new PriorityMap((maps ?? []).filter(Boolean).flatMap(map => map.viewers));
|
|
331
489
|
}
|
|
332
490
|
|
|
333
491
|
/**
|
|
334
|
-
* The map as stretches
|
|
335
|
-
*
|
|
336
|
-
* A VIEW of the map, never a second copy of it. Consumers that work in ranges —
|
|
337
|
-
* the swarm is asked for byte ranges, an encoder is given a stretch of pieces —
|
|
338
|
-
* would otherwise each write this walk for themselves.
|
|
492
|
+
* The map as stretches (`PriorityMap.runs`).
|
|
339
493
|
*
|
|
340
|
-
* @param {PriorityMap} map
|
|
341
|
-
* @returns {DemandZone[]}
|
|
342
|
-
* wants are left out.
|
|
494
|
+
* @param {PriorityMap | null | undefined} map
|
|
495
|
+
* @returns {DemandZone[]}
|
|
343
496
|
*/
|
|
344
497
|
export function runsOf(map) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const urgent = map.urgent?.[second] === 1;
|
|
357
|
-
const deferred = map.deferred?.[second] === 1;
|
|
358
|
-
const previous = runs[runs.length - 1];
|
|
359
|
-
if (
|
|
360
|
-
previous
|
|
361
|
-
&& previous.to === second
|
|
362
|
-
&& previous.priority === priority
|
|
363
|
-
&& previous.behind === behind
|
|
364
|
-
&& previous.urgent === urgent
|
|
365
|
-
&& previous.deferred === deferred
|
|
366
|
-
) {
|
|
367
|
-
previous.to = second + 1;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
// THE TIME OF A STRETCH IS THE TIME OF ITS NEAR EDGE. A stretch is met at
|
|
371
|
-
// its beginning, so its beginning is when it must exist; whoever needs the
|
|
372
|
-
// time of a second inside it walks forward from there, at a second of film
|
|
373
|
-
// per second, which is the same arithmetic that put the number here.
|
|
374
|
-
//
|
|
375
|
-
// Merging on the exact time as well would merge nothing at all: the time is
|
|
376
|
-
// the distance, so it differs at every second, and the map came back as one
|
|
377
|
-
// stretch per second of film.
|
|
378
|
-
const withinSeconds = map.secondsUntilPlayed[second];
|
|
379
|
-
runs.push({
|
|
380
|
-
from: second,
|
|
381
|
-
to: second + 1,
|
|
382
|
-
priority,
|
|
383
|
-
withinSeconds,
|
|
384
|
-
behind,
|
|
385
|
-
urgent,
|
|
386
|
-
deferred
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
return runs;
|
|
498
|
+
return map ? map.runs() : [];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Where viewers stand on a map (`PriorityMap.viewerStarts`).
|
|
503
|
+
*
|
|
504
|
+
* @param {PriorityMap | null | undefined} map
|
|
505
|
+
* @returns {number[]}
|
|
506
|
+
*/
|
|
507
|
+
export function viewerStartsOn(map) {
|
|
508
|
+
return map ? map.viewerStarts() : [];
|
|
390
509
|
}
|
|
391
510
|
|
|
392
511
|
/**
|
|
@@ -11,10 +11,27 @@ export function segmentDemands(output, fileIndex, zones) {
|
|
|
11
11
|
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
12
12
|
if (!Array.isArray(grid)) return [];
|
|
13
13
|
const demands = [];
|
|
14
|
+
// THE ZONES ARE WALKED BESIDE THE SEGMENTS, not searched for each of them.
|
|
15
|
+
// A map's zones come ascending and apart (`PriorityMap.runs`) and the grid is
|
|
16
|
+
// ascending, so the zones a segment overlaps begin at or after those of the
|
|
17
|
+
// segment before it. Every zone filtered for every segment was thousands of
|
|
18
|
+
// segments by tens of zones on every pass over a film (r26,
|
|
19
|
+
// torrent-tv/meta#178). Zones in any other order are filtered as before.
|
|
20
|
+
const ordered = ascendingAndApart(zones);
|
|
21
|
+
let first = 0;
|
|
14
22
|
for (let index = 0; index + 1 < grid.length; index++) {
|
|
15
23
|
const from = grid[index], to = grid[index + 1];
|
|
16
24
|
if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
|
|
17
|
-
|
|
25
|
+
let overlapping;
|
|
26
|
+
if (ordered) {
|
|
27
|
+
while (first < zones.length && zones[first].to <= from) first += 1;
|
|
28
|
+
overlapping = [];
|
|
29
|
+
for (let at = first; at < zones.length && zones[at].from < to; at += 1) {
|
|
30
|
+
if (zones[at].priority > 0) overlapping.push(zones[at]);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
overlapping = zones.filter(zone => zone.priority > 0 && zone.from < to && zone.to > from);
|
|
34
|
+
}
|
|
18
35
|
if (!overlapping.length) continue;
|
|
19
36
|
// A zone states the deadline of its near edge. A later segment within it
|
|
20
37
|
// is reached later, at one second of film per second of playback.
|
|
@@ -31,6 +48,14 @@ export function segmentDemands(output, fileIndex, zones) {
|
|
|
31
48
|
return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
/** Whether zones come ascending and without overlaps, as a map's runs do. */
|
|
52
|
+
function ascendingAndApart(zones) {
|
|
53
|
+
for (let index = 1; index < zones.length; index += 1) {
|
|
54
|
+
if (!(zones[index].from >= zones[index - 1].to)) return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
/** Source bytes must arrive before measured processing and delivery consume time. */
|
|
35
60
|
export function sourceDeadline(demand, { encodeSpeed, outputBytes, linkMbps }) {
|
|
36
61
|
const span = demand.to - demand.from;
|