@torrent-tv/proxy 2.98.3 → 2.98.5
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/routes/api/transcode-sessions/net-report/post.js +10 -7
- package/server.js +124 -120
- package/services/encode/EncodeInputs.js +1 -1
- package/services/storage/segment-store/SegmentStore.js +21 -1
- package/services/torrent/PlaybackProjection.js +771 -0
- package/services/viewer/DownloadMaps.js +12 -1
- package/services/viewer/Viewer.js +31 -1
- package/services/viewer/segment-demands.js +3 -57
- package/services/viewer/playback-pass.js +0 -70
- package/services/viewer/target-input-expansion.js +0 -174
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The priority map projected onto a file's bytes, KEPT from map to map
|
|
3
|
+
* and brought up to date by what changed (torrent-tv/meta#178, stage C).
|
|
4
|
+
*
|
|
5
|
+
* What the torrent is told about a file being played is worked out from the
|
|
6
|
+
* viewers' map: each output segment takes the map zones it overlaps — the
|
|
7
|
+
* highest priority, its level, and a deadline — its source ranges take the band
|
|
8
|
+
* of the best segment that names them, and the ranges of one band that touch
|
|
9
|
+
* are told as one window. That used to be worked out whole on every map: a
|
|
10
|
+
* pass a second over the 2500-3500 segments of a two-hour film, every segment
|
|
11
|
+
* made into a demand and a zone again, the expansion of the next run's input
|
|
12
|
+
* walked over all of them, and ten thousand ranges packed afresh — while the
|
|
13
|
+
* viewer had moved by a second and a handful of segments had anything new to
|
|
14
|
+
* say (r30: 60 of 60 passes a minute worked out whole).
|
|
15
|
+
*
|
|
16
|
+
* Here the projection is held. Each segment keeps its demand and its ranges,
|
|
17
|
+
* each range knows the segments naming it, each band keeps its ranges in byte
|
|
18
|
+
* order, and the windows are a product of them. A new map changes only what
|
|
19
|
+
* it changes:
|
|
20
|
+
*
|
|
21
|
+
* 1. THE TIME BASE. A deadline is `B + relative`, where `B` is when the viewer
|
|
22
|
+
* would reach the film's start at the rate they play — `now - floor(at)`,
|
|
23
|
+
* read off the map's own zones — and `relative` is where the segment lies.
|
|
24
|
+
* A viewer playing on moves `B` and nothing else, so a map that only moved
|
|
25
|
+
* `B` reworks no segment: the windows' deadlines are `B` plus the earliest
|
|
26
|
+
* relative deadline each of them already holds. Every report of the viewer
|
|
27
|
+
* is taken: nothing is held at an old position for a tolerance's sake. A
|
|
28
|
+
* map whose zones state no single base — two viewers playing apart — is
|
|
29
|
+
* worked out in absolute time, segment by segment, as it always was.
|
|
30
|
+
* 2. THE BANDS. Only where the old and the new map say something different of
|
|
31
|
+
* the film — another priority, level or deadline — is a segment worked out
|
|
32
|
+
* again: the stretches a band boundary crossed, and the segment where the
|
|
33
|
+
* viewer stands.
|
|
34
|
+
* 3. THE REST, each where it happens: a segment made leaves the demand, one
|
|
35
|
+
* lost joins it again; a change of how long a segment takes to make and
|
|
36
|
+
* deliver moves that output's deadlines; the next run's input is raised
|
|
37
|
+
* again when its first segment or its size changes; a segment's ranges join
|
|
38
|
+
* when they are worked out.
|
|
39
|
+
*
|
|
40
|
+
* The rules are the ones the whole pass had — each segment's demand, its
|
|
41
|
+
* deadline less making and delivery, the next run's input raised, the ranges
|
|
42
|
+
* packed (`coalescedZones`) — and the windows are the same:
|
|
43
|
+
* `test/no-torrent/playback-projection.test.js` holds the projection beside
|
|
44
|
+
* that pass (`test/no-torrent/support/whole-pass.js`) on every map of random
|
|
45
|
+
* sequences.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { Urgency } from "./demand/Urgency.js";
|
|
49
|
+
|
|
50
|
+
/** A segment's band as one number: the level, then the priority within it (as `coalescedZones` keys it). */
|
|
51
|
+
function bandKeyOf(level, priority) {
|
|
52
|
+
return level * 1000 + priority;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether band `left` is fetched before band `right`: the level, then the higher priority. */
|
|
56
|
+
function before(left, right) {
|
|
57
|
+
const leftLevel = Math.floor(left / 1000), rightLevel = Math.floor(right / 1000);
|
|
58
|
+
return leftLevel !== rightLevel ? leftLevel < rightLevel : left % 1000 > right % 1000;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The level of a demand, from its own words (`levelOfMapZone` for a zone that states all three). */
|
|
62
|
+
function levelOf(behind, deferred, urgent) {
|
|
63
|
+
return behind ? Urgency.BEHIND : deferred ? Urgency.TAIL : urgent ? Urgency.NEAR : Urgency.AHEAD;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The priority a demand is stated at, within the register's scale (`priorityOfMapZone`). */
|
|
67
|
+
function clampPriority(priority) {
|
|
68
|
+
return Number.isFinite(priority) ? Math.max(1, Math.min(100, priority)) : 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The first entry of a list in byte order that begins at or after `start`. */
|
|
72
|
+
function lowerBound(list, start) {
|
|
73
|
+
let low = 0, high = list.length;
|
|
74
|
+
while (low < high) {
|
|
75
|
+
const middle = (low + high) >> 1;
|
|
76
|
+
if (list[middle].start < start) low = middle + 1;
|
|
77
|
+
else high = middle;
|
|
78
|
+
}
|
|
79
|
+
return low;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether zones come ascending and without overlaps, as a map's runs do. */
|
|
83
|
+
function ascendingAndApart(zones) {
|
|
84
|
+
for (let index = 1; index < zones.length; index += 1) {
|
|
85
|
+
if (!(zones[index].from >= zones[index - 1].to)) return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The time base the zones share — `deadlineAt - from` — or null when they do
|
|
92
|
+
* not share one, or state no deadline at all.
|
|
93
|
+
*/
|
|
94
|
+
function timeBaseOf(zones) {
|
|
95
|
+
let base = null;
|
|
96
|
+
for (const zone of zones) {
|
|
97
|
+
if (!(zone.priority > 0) || !Number.isFinite(zone.deadlineAt)) continue;
|
|
98
|
+
const own = zone.deadlineAt - zone.from * 1000;
|
|
99
|
+
if (base === null) base = own;
|
|
100
|
+
else if (base !== own) return null;
|
|
101
|
+
}
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The first zone that ends after `at`, in ascending zones. */
|
|
106
|
+
function firstEndingAfter(zones, at) {
|
|
107
|
+
let low = 0, high = zones.length;
|
|
108
|
+
while (low < high) {
|
|
109
|
+
const middle = (low + high) >> 1;
|
|
110
|
+
if (zones[middle].to <= at) low = middle + 1;
|
|
111
|
+
else high = middle;
|
|
112
|
+
}
|
|
113
|
+
return low;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The first segment that ends after `at` on a grid of starts. */
|
|
117
|
+
function segmentAt(grid, at) {
|
|
118
|
+
let low = 0, high = grid.length - 1;
|
|
119
|
+
while (low < high) {
|
|
120
|
+
const middle = (low + high) >> 1;
|
|
121
|
+
if (grid[middle + 1] <= at) low = middle + 1;
|
|
122
|
+
else high = middle;
|
|
123
|
+
}
|
|
124
|
+
return low;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What one stretch of film is told by one map: whether a zone covers it, and
|
|
129
|
+
* the zone's priority, words and deadline offset — the deadline of a second
|
|
130
|
+
* `s` in it being `offset + s * 1000` past the time base.
|
|
131
|
+
*/
|
|
132
|
+
function stateAt(zones, ordered, at) {
|
|
133
|
+
let zone = null;
|
|
134
|
+
if (ordered) {
|
|
135
|
+
const index = firstEndingAfter(zones, at);
|
|
136
|
+
if (index < zones.length && zones[index].from <= at) zone = zones[index];
|
|
137
|
+
} else {
|
|
138
|
+
zone = zones.find(candidate => candidate.priority > 0 && candidate.from <= at && candidate.to > at) ?? null;
|
|
139
|
+
}
|
|
140
|
+
return zone && zone.priority > 0 ? zone : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Whether two zones say the same of a second they both cover. */
|
|
144
|
+
function sameStatement(left, right, leftBase, rightBase) {
|
|
145
|
+
if (left === null || right === null) return left === right;
|
|
146
|
+
if (left.priority !== right.priority || Boolean(left.urgent) !== Boolean(right.urgent) ||
|
|
147
|
+
Boolean(left.behind) !== Boolean(right.behind) || Boolean(left.deferred) !== Boolean(right.deferred)) return false;
|
|
148
|
+
const leftOffset = Number.isFinite(left.deadlineAt) ? left.deadlineAt - leftBase - left.from * 1000 : Infinity;
|
|
149
|
+
const rightOffset = Number.isFinite(right.deadlineAt) ? right.deadlineAt - rightBase - right.from * 1000 : Infinity;
|
|
150
|
+
return leftOffset === rightOffset;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The stretches of film where two maps say something different, ascending.
|
|
155
|
+
* Both are read in their own time base, so a map that only moved its base
|
|
156
|
+
* differs nowhere.
|
|
157
|
+
*/
|
|
158
|
+
function differences(previous, next, previousBase, nextBase) {
|
|
159
|
+
const points = new Set();
|
|
160
|
+
for (const zone of previous) { points.add(zone.from); points.add(zone.to); }
|
|
161
|
+
for (const zone of next) { points.add(zone.from); points.add(zone.to); }
|
|
162
|
+
const sorted = [...points].filter(Number.isFinite).sort((left, right) => left - right);
|
|
163
|
+
const previousOrdered = ascendingAndApart(previous), nextOrdered = ascendingAndApart(next);
|
|
164
|
+
const stretches = [];
|
|
165
|
+
for (let index = 0; index + 1 < sorted.length; index += 1) {
|
|
166
|
+
const from = sorted[index], to = sorted[index + 1];
|
|
167
|
+
if (!(to > from)) continue;
|
|
168
|
+
const left = stateAt(previous, previousOrdered, from);
|
|
169
|
+
const right = stateAt(next, nextOrdered, from);
|
|
170
|
+
if (sameStatement(left, right, previousBase, nextBase)) continue;
|
|
171
|
+
const last = stretches.at(-1);
|
|
172
|
+
if (last && last.to === from) last.to = to;
|
|
173
|
+
else stretches.push({ from, to });
|
|
174
|
+
}
|
|
175
|
+
return stretches;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Ranges a run's input holds, each counted once:
|
|
180
|
+
* merged, ascending, searched rather than sorted again.
|
|
181
|
+
*/
|
|
182
|
+
class HeldBytes {
|
|
183
|
+
/** @type {Array<[number, number]>} */
|
|
184
|
+
#ranges = [];
|
|
185
|
+
total = 0;
|
|
186
|
+
|
|
187
|
+
#from(at) {
|
|
188
|
+
let low = 0, high = this.#ranges.length;
|
|
189
|
+
while (low < high) {
|
|
190
|
+
const middle = (low + high) >> 1;
|
|
191
|
+
if (this.#ranges[middle][1] < at) low = middle + 1;
|
|
192
|
+
else high = middle;
|
|
193
|
+
}
|
|
194
|
+
return low;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
adds(ranges) {
|
|
198
|
+
let added = 0;
|
|
199
|
+
for (const [start, end] of merged(ranges)) {
|
|
200
|
+
let at = start;
|
|
201
|
+
for (let index = this.#from(start); at <= end && index < this.#ranges.length && this.#ranges[index][0] <= end; index++) {
|
|
202
|
+
const [heldStart, heldEnd] = this.#ranges[index];
|
|
203
|
+
if (heldStart > at) added += heldStart - at;
|
|
204
|
+
at = Math.max(at, heldEnd + 1);
|
|
205
|
+
}
|
|
206
|
+
if (at <= end) added += end - at + 1;
|
|
207
|
+
}
|
|
208
|
+
return added;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
add(ranges) {
|
|
212
|
+
for (const [start, end] of merged(ranges)) {
|
|
213
|
+
this.total += this.adds([[start, end]]);
|
|
214
|
+
const first = this.#from(start - 1);
|
|
215
|
+
let last = first, joinedStart = start, joinedEnd = end;
|
|
216
|
+
while (last < this.#ranges.length && this.#ranges[last][0] <= end + 1) {
|
|
217
|
+
joinedStart = Math.min(joinedStart, this.#ranges[last][0]);
|
|
218
|
+
joinedEnd = Math.max(joinedEnd, this.#ranges[last][1]);
|
|
219
|
+
last++;
|
|
220
|
+
}
|
|
221
|
+
this.#ranges.splice(first, last - first, [joinedStart, joinedEnd]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Inclusive ranges merged where they overlap or touch, ascending. */
|
|
227
|
+
function merged(ranges) {
|
|
228
|
+
const sorted = [...ranges].sort((left, right) => left[0] - right[0]);
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const [start, end] of sorted) {
|
|
231
|
+
const previous = out.at(-1);
|
|
232
|
+
if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
|
|
233
|
+
else out.push([start, end]);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* One exact byte range of the file and every active segment naming it: it is
|
|
240
|
+
* told once, in the best band among them, at the earliest deadline among the
|
|
241
|
+
* segments in that band (`coalescedZones`, `bestOfRepeats`).
|
|
242
|
+
*/
|
|
243
|
+
class RangeEntry {
|
|
244
|
+
constructor(start, end) {
|
|
245
|
+
this.start = start;
|
|
246
|
+
this.end = end;
|
|
247
|
+
/** Band key → the active segments in that band naming this range. @type {Map<number, Set<Segment>>} */
|
|
248
|
+
this.byBand = new Map();
|
|
249
|
+
/** The band it is told in, or null while nobody active names it. */
|
|
250
|
+
this.placed = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The best band among the segments naming it. */
|
|
254
|
+
best() {
|
|
255
|
+
let best = null;
|
|
256
|
+
for (const key of this.byBand.keys()) if (best === null || before(key, best)) best = key;
|
|
257
|
+
return best;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The earliest relative deadline among the segments of the band it is told in. */
|
|
261
|
+
deadline() {
|
|
262
|
+
let earliest = Infinity;
|
|
263
|
+
for (const segment of this.byBand.get(this.placed) ?? []) if (segment.deadline < earliest) earliest = segment.deadline;
|
|
264
|
+
return earliest;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** One output segment as the projection holds it. */
|
|
269
|
+
class Segment {
|
|
270
|
+
constructor(output, index, from, to) {
|
|
271
|
+
this.output = output;
|
|
272
|
+
this.index = index;
|
|
273
|
+
this.from = from;
|
|
274
|
+
this.to = to;
|
|
275
|
+
/** Whether a zone of the map overlaps it. */
|
|
276
|
+
this.demanded = false;
|
|
277
|
+
/** Whether the output has it made. */
|
|
278
|
+
this.ready = false;
|
|
279
|
+
this.priority = 0;
|
|
280
|
+
this.urgent = false;
|
|
281
|
+
this.behind = true;
|
|
282
|
+
this.deferred = true;
|
|
283
|
+
/** The map zone its deadline is reckoned from. */
|
|
284
|
+
this.owner = null;
|
|
285
|
+
/** Its deadline past the time base before its making and delivery are taken off. */
|
|
286
|
+
this.reached = Infinity;
|
|
287
|
+
/** How long its making and delivery take, in seconds, as `sourceDeadline` reckons it. */
|
|
288
|
+
this.leadSeconds = 0;
|
|
289
|
+
/** Its deadline past the time base: when its source bytes are needed. */
|
|
290
|
+
this.deadline = Infinity;
|
|
291
|
+
/** Its band, and the band it is told in once the next run's input is raised. */
|
|
292
|
+
this.band = null;
|
|
293
|
+
this.told = null;
|
|
294
|
+
/** Its source ranges as the torrent fetches them, once worked out. @type {Array<[number, number]> | null} */
|
|
295
|
+
this.ranges = null;
|
|
296
|
+
/** @type {RangeEntry[]} */
|
|
297
|
+
this.entries = [];
|
|
298
|
+
/** Whether it is in its ranges' entries now. */
|
|
299
|
+
this.active = false;
|
|
300
|
+
/** Whether the file lacks a track it reads: it has no ranges to work out. */
|
|
301
|
+
this.unreadable = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** One output: its grid, how long its segments take to make and deliver, what is made, and its next input. */
|
|
306
|
+
class OutputState {
|
|
307
|
+
constructor(outputKey, grid) {
|
|
308
|
+
this.outputKey = outputKey;
|
|
309
|
+
this.grid = grid;
|
|
310
|
+
/** @type {Segment[]} */
|
|
311
|
+
this.segments = [];
|
|
312
|
+
for (let index = 0; index + 1 < grid.length; index++) {
|
|
313
|
+
this.segments.push(new Segment(this, index, grid[index], grid[index + 1]));
|
|
314
|
+
}
|
|
315
|
+
this.encodeSpeed = undefined;
|
|
316
|
+
this.bytesPerSecond = undefined;
|
|
317
|
+
this.linkMbps = undefined;
|
|
318
|
+
this.readyRevision = undefined;
|
|
319
|
+
this.target = null;
|
|
320
|
+
/** The segments told in the band of the first one, as the next run's input. @type {Set<Segment>} */
|
|
321
|
+
this.raised = new Set();
|
|
322
|
+
this.raisedFrom = null;
|
|
323
|
+
this.raisedBand = null;
|
|
324
|
+
/** Its own priority → its active segments: where the next run's input starts. @type {Map<number, Set<Segment>>} */
|
|
325
|
+
this.byPriority = new Map();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export class PlaybackProjection {
|
|
330
|
+
/** @type {Map<string, OutputState>} */
|
|
331
|
+
#outputs = new Map();
|
|
332
|
+
/** Byte start → byte end → its entry. @type {Map<number, Map<number, RangeEntry>>} */
|
|
333
|
+
#entries = new Map();
|
|
334
|
+
/** Band key → its entries in byte order. @type {Map<number, RangeEntry[]>} */
|
|
335
|
+
#bands = new Map();
|
|
336
|
+
/** Bands whose windows must be made again. @type {Set<number>} */
|
|
337
|
+
#dirty = new Set();
|
|
338
|
+
/** Band key → its windows, relative deadlines. @type {Map<number, Array<{ byteStart: number, byteEnd: number, deadline: number }>>} */
|
|
339
|
+
#bandWindows = new Map();
|
|
340
|
+
#zones = [];
|
|
341
|
+
#ordered = true;
|
|
342
|
+
/** The segments wanted whose ranges are not worked out yet. @type {Set<Segment>} */
|
|
343
|
+
#unresolved = new Set();
|
|
344
|
+
/**
|
|
345
|
+
* Outputs whose next run's input is to be raised again before the windows
|
|
346
|
+
* are told: worked out once for everything a map or a pass changed, not at
|
|
347
|
+
* each segment that changed — on a cold start that was once for each of
|
|
348
|
+
* 4500 segments worked out (r34, torrent-tv/meta#178).
|
|
349
|
+
* @type {Set<OutputState>}
|
|
350
|
+
*/
|
|
351
|
+
#raiseAgain = new Set();
|
|
352
|
+
/** The time base of the last map, or 0 for a map stated in absolute time. */
|
|
353
|
+
#base = 0;
|
|
354
|
+
#absolute = true;
|
|
355
|
+
/** The windows last handed out, kept while nothing they show has changed. */
|
|
356
|
+
#told = null;
|
|
357
|
+
/** What the last update reworked, for the per-minute reading. */
|
|
358
|
+
lastUpdate = { segments: 0, bands: 0, whole: false };
|
|
359
|
+
|
|
360
|
+
/** Forget everything: the file has no viewer. */
|
|
361
|
+
clear() {
|
|
362
|
+
for (const output of [...this.#outputs.values()]) this.#dropOutput(output);
|
|
363
|
+
this.#zones = [];
|
|
364
|
+
this.#told = null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Bring the projection to a new map.
|
|
369
|
+
*
|
|
370
|
+
* @param {object} params
|
|
371
|
+
* @param {Array<{ from: number, to: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean,
|
|
372
|
+
* deadlineAt?: number }>} params.zones - The file's map zones, deadlines as instants.
|
|
373
|
+
* @param {Array<{ outputKey: string, grid: number[], encodeSpeed?: number, bytesPerSecond?: number | null,
|
|
374
|
+
* linkMbps?: number | null, readyRevision: unknown, ready: () => Map<number, number>,
|
|
375
|
+
* target: number | null }>} params.outputs - Every output of the file somebody is watching, with
|
|
376
|
+
* what decides how long its segments take to make and deliver (`sourceDeadline`), what it has made
|
|
377
|
+
* — read again only when its revision moves — and how many bytes its next run would copy.
|
|
378
|
+
* @returns {void}
|
|
379
|
+
*/
|
|
380
|
+
update({ zones, outputs }) {
|
|
381
|
+
const base = timeBaseOf(zones);
|
|
382
|
+
const absolute = base === null;
|
|
383
|
+
const nextBase = absolute ? 0 : base;
|
|
384
|
+
// A map in absolute time moves every deadline with the clock: worked out
|
|
385
|
+
// whole, as before this projection existed.
|
|
386
|
+
const whole = absolute || this.#absolute;
|
|
387
|
+
const stretches = whole ? null : differences(this.#zones, zones, this.#base, nextBase);
|
|
388
|
+
const ordered = ascendingAndApart(zones);
|
|
389
|
+
let reworked = 0;
|
|
390
|
+
const seen = new Set();
|
|
391
|
+
for (const info of outputs) {
|
|
392
|
+
seen.add(info.outputKey);
|
|
393
|
+
let output = this.#outputs.get(info.outputKey);
|
|
394
|
+
if (output && output.grid !== info.grid) {
|
|
395
|
+
this.#dropOutput(output);
|
|
396
|
+
output = undefined;
|
|
397
|
+
}
|
|
398
|
+
const fresh = !output;
|
|
399
|
+
if (fresh) {
|
|
400
|
+
output = new OutputState(info.outputKey, info.grid);
|
|
401
|
+
this.#outputs.set(info.outputKey, output);
|
|
402
|
+
}
|
|
403
|
+
// How long a segment of it takes to make and deliver.
|
|
404
|
+
const leadMoved = fresh || output.encodeSpeed !== info.encodeSpeed || output.bytesPerSecond !== info.bytesPerSecond ||
|
|
405
|
+
output.linkMbps !== info.linkMbps;
|
|
406
|
+
output.encodeSpeed = info.encodeSpeed;
|
|
407
|
+
output.bytesPerSecond = info.bytesPerSecond;
|
|
408
|
+
output.linkMbps = info.linkMbps;
|
|
409
|
+
// What it has made.
|
|
410
|
+
if (fresh || output.readyRevision !== info.readyRevision) {
|
|
411
|
+
output.readyRevision = info.readyRevision;
|
|
412
|
+
const ready = info.ready();
|
|
413
|
+
for (const segment of output.segments) {
|
|
414
|
+
const now = ready.has(segment.index);
|
|
415
|
+
if (now !== segment.ready) {
|
|
416
|
+
segment.ready = now;
|
|
417
|
+
this.#activate(segment);
|
|
418
|
+
this.#track(segment);
|
|
419
|
+
this.#raiseAgain.add(output);
|
|
420
|
+
reworked++;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// What the map says of each segment, where it says something new.
|
|
425
|
+
if (fresh || whole) {
|
|
426
|
+
for (const segment of output.segments) if (this.#demand(segment, zones, ordered, nextBase, leadMoved)) reworked++;
|
|
427
|
+
} else {
|
|
428
|
+
for (const { from, to } of stretches) {
|
|
429
|
+
for (let index = segmentAt(output.grid, from); index < output.segments.length && output.segments[index].from < to; index++) {
|
|
430
|
+
if (this.#demand(output.segments[index], zones, ordered, nextBase, false)) reworked++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (leadMoved) for (const segment of output.segments) if (this.#reckon(segment)) reworked++;
|
|
434
|
+
}
|
|
435
|
+
if (fresh || output.target !== (info.target ?? null)) this.#raiseAgain.add(output);
|
|
436
|
+
output.target = info.target ?? null;
|
|
437
|
+
}
|
|
438
|
+
for (const output of [...this.#outputs.values()]) if (!seen.has(output.outputKey)) this.#dropOutput(output);
|
|
439
|
+
if (nextBase !== this.#base || absolute !== this.#absolute) this.#told = null;
|
|
440
|
+
this.#zones = zones;
|
|
441
|
+
this.#ordered = ordered;
|
|
442
|
+
this.#base = nextBase;
|
|
443
|
+
this.#absolute = absolute;
|
|
444
|
+
this.lastUpdate = { segments: reworked, bands: this.#dirty.size, whole };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* A segment's source ranges, worked out: they join the bands now.
|
|
449
|
+
*
|
|
450
|
+
* @param {string} outputKey
|
|
451
|
+
* @param {number} index
|
|
452
|
+
* @param {Array<[number, number]>} ranges - Inclusive, as the torrent fetches them.
|
|
453
|
+
* @returns {void}
|
|
454
|
+
*/
|
|
455
|
+
setRanges(outputKey, index, ranges) {
|
|
456
|
+
const segment = this.#outputs.get(outputKey)?.segments[index];
|
|
457
|
+
if (!segment || segment.ranges === ranges) return;
|
|
458
|
+
this.#deactivate(segment);
|
|
459
|
+
segment.ranges = ranges;
|
|
460
|
+
segment.entries = [];
|
|
461
|
+
const named = new Set();
|
|
462
|
+
for (const [start, end] of ranges) {
|
|
463
|
+
// A range a segment names twice is one range.
|
|
464
|
+
const key = `${start}:${end}`;
|
|
465
|
+
if (named.has(key)) continue;
|
|
466
|
+
named.add(key);
|
|
467
|
+
let byEnd = this.#entries.get(start);
|
|
468
|
+
if (!byEnd) this.#entries.set(start, byEnd = new Map());
|
|
469
|
+
let entry = byEnd.get(end);
|
|
470
|
+
if (!entry) byEnd.set(end, entry = new RangeEntry(start, end));
|
|
471
|
+
segment.entries.push(entry);
|
|
472
|
+
}
|
|
473
|
+
this.#activate(segment);
|
|
474
|
+
this.#track(segment);
|
|
475
|
+
this.#raiseAgain.add(segment.output);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* A segment that reads a track the file does not have: nothing to work out,
|
|
480
|
+
* and nothing told for it.
|
|
481
|
+
*
|
|
482
|
+
* @param {string} outputKey
|
|
483
|
+
* @param {number} index
|
|
484
|
+
* @returns {void}
|
|
485
|
+
*/
|
|
486
|
+
setUnreadable(outputKey, index) {
|
|
487
|
+
const segment = this.#outputs.get(outputKey)?.segments[index];
|
|
488
|
+
if (!segment) return;
|
|
489
|
+
segment.unreadable = true;
|
|
490
|
+
this.#track(segment);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The demands whose ranges are not worked out yet, in the order the swarm is
|
|
495
|
+
* to fetch them: by deadline, then the higher priority, then the segment.
|
|
496
|
+
*
|
|
497
|
+
* @returns {Array<{ outputKey: string, index: number, from: number, to: number, priority: number, urgent: boolean,
|
|
498
|
+
* behind: boolean, deferred: boolean, deadlineAt: number, leadSeconds: number, owner: object }>}
|
|
499
|
+
*/
|
|
500
|
+
unresolved() {
|
|
501
|
+
const waiting = [];
|
|
502
|
+
for (const segment of this.#unresolved) {
|
|
503
|
+
waiting.push({ outputKey: segment.output.outputKey, index: segment.index, from: segment.from, to: segment.to,
|
|
504
|
+
priority: segment.priority, urgent: segment.urgent, behind: segment.behind, deferred: segment.deferred,
|
|
505
|
+
deadlineAt: this.#base + segment.deadline, leadSeconds: segment.leadSeconds, owner: this.#ownerOf(segment) });
|
|
506
|
+
}
|
|
507
|
+
return waiting.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* What the torrent is told: each band's windows, the bands in the order they
|
|
512
|
+
* are fetched, a window's deadline the earliest of the ranges it holds.
|
|
513
|
+
* The same array while nothing in it has changed.
|
|
514
|
+
*
|
|
515
|
+
* @returns {Array<{ byteStart: number, byteEnd: number, priority: number, urgent: boolean, behind: boolean,
|
|
516
|
+
* deferred: boolean, deadlineAt?: number, standing: true }>}
|
|
517
|
+
*/
|
|
518
|
+
windows() {
|
|
519
|
+
for (const output of this.#raiseAgain) if (this.#outputs.get(output.outputKey) === output) this.#raise(output);
|
|
520
|
+
this.#raiseAgain.clear();
|
|
521
|
+
if (this.#told && this.#dirty.size === 0) return this.#told;
|
|
522
|
+
for (const key of this.#dirty) this.#makeWindows(key);
|
|
523
|
+
this.#dirty.clear();
|
|
524
|
+
const keys = [...this.#bandWindows.keys()].sort((left, right) => before(left, right) ? -1 : before(right, left) ? 1 : 0);
|
|
525
|
+
const told = [];
|
|
526
|
+
for (const key of keys) {
|
|
527
|
+
const level = Math.floor(key / 1000), priority = key % 1000;
|
|
528
|
+
for (const window of this.#bandWindows.get(key)) {
|
|
529
|
+
told.push({ byteStart: window.byteStart, byteEnd: window.byteEnd, priority, urgent: level === Urgency.NEAR,
|
|
530
|
+
behind: level === Urgency.BEHIND, deferred: level === Urgency.TAIL,
|
|
531
|
+
...(Number.isFinite(window.deadline) ? { deadlineAt: this.#base + window.deadline } : {}),
|
|
532
|
+
// Kept current by this projection: a new map does not withdraw or
|
|
533
|
+
// reprice it in the meantime (`DownloadMaps.playback`).
|
|
534
|
+
standing: true });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
this.#told = told;
|
|
538
|
+
return told;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** The demand of one segment, read off the map again; whether anything about it changed. */
|
|
542
|
+
#demand(segment, zones, ordered, base, leadMoved) {
|
|
543
|
+
// The zones it overlaps, folded as they are met.
|
|
544
|
+
let owner = null, ownerDeadline = Infinity, priority = 0, urgent = false, behind = true, deferred = true;
|
|
545
|
+
const { from, to } = segment;
|
|
546
|
+
for (let at = ordered ? firstEndingAfter(zones, from) : 0; at < zones.length; at += 1) {
|
|
547
|
+
const zone = zones[at];
|
|
548
|
+
if (ordered && !(zone.from < to)) break;
|
|
549
|
+
if (!(zone.priority > 0) || !(zone.from < to && zone.to > from)) continue;
|
|
550
|
+
const deadline = Number.isFinite(zone.deadlineAt) ? zone.deadlineAt - base + Math.max(0, from - zone.from) * 1000 : Infinity;
|
|
551
|
+
if (owner === null || deadline < ownerDeadline) { owner = zone; ownerDeadline = deadline; }
|
|
552
|
+
if (zone.priority > priority) priority = zone.priority;
|
|
553
|
+
if (zone.urgent) urgent = true;
|
|
554
|
+
if (!zone.behind) behind = false;
|
|
555
|
+
if (!zone.deferred) deferred = false;
|
|
556
|
+
}
|
|
557
|
+
const demanded = owner !== null;
|
|
558
|
+
const band = demanded ? bandKeyOf(levelOf(behind, deferred, urgent), clampPriority(priority)) : null;
|
|
559
|
+
const changed = demanded !== segment.demanded || band !== segment.band || ownerDeadline !== segment.reached ||
|
|
560
|
+
priority !== segment.priority || urgent !== segment.urgent || behind !== segment.behind || deferred !== segment.deferred;
|
|
561
|
+
segment.owner = owner;
|
|
562
|
+
if (!changed && !leadMoved) return false;
|
|
563
|
+
this.#deactivate(segment);
|
|
564
|
+
segment.demanded = demanded;
|
|
565
|
+
segment.priority = priority;
|
|
566
|
+
segment.urgent = urgent;
|
|
567
|
+
segment.behind = behind;
|
|
568
|
+
segment.deferred = deferred;
|
|
569
|
+
segment.band = band;
|
|
570
|
+
segment.reached = ownerDeadline;
|
|
571
|
+
this.#lead(segment);
|
|
572
|
+
this.#activate(segment);
|
|
573
|
+
this.#track(segment);
|
|
574
|
+
this.#raiseAgain.add(segment.output);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Whether a segment waits for its ranges to be worked out. */
|
|
579
|
+
#track(segment) {
|
|
580
|
+
if (segment.demanded && !segment.ready && segment.ranges === null && !segment.unreadable) this.#unresolved.add(segment);
|
|
581
|
+
else this.#unresolved.delete(segment);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The zone of the current map a segment's deadline is reckoned from. A
|
|
586
|
+
* segment the new map says nothing new of keeps its demand, and the zone it
|
|
587
|
+
* names is looked up only for one still to be worked out, whose read is
|
|
588
|
+
* asked for under it.
|
|
589
|
+
*/
|
|
590
|
+
#ownerOf(segment) {
|
|
591
|
+
const zones = this.#zones;
|
|
592
|
+
let owner = null, ownerDeadline = Infinity;
|
|
593
|
+
for (let at = this.#ordered ? firstEndingAfter(zones, segment.from) : 0; at < zones.length; at += 1) {
|
|
594
|
+
const zone = zones[at];
|
|
595
|
+
if (this.#ordered && !(zone.from < segment.to)) break;
|
|
596
|
+
if (!(zone.priority > 0) || !(zone.from < segment.to && zone.to > segment.from)) continue;
|
|
597
|
+
const deadline = Number.isFinite(zone.deadlineAt) ? zone.deadlineAt + Math.max(0, segment.from - zone.from) * 1000 : Infinity;
|
|
598
|
+
if (owner === null || deadline < ownerDeadline) { owner = zone; ownerDeadline = deadline; }
|
|
599
|
+
}
|
|
600
|
+
return owner;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** How long one segment takes to make and deliver moved: its deadline again. */
|
|
604
|
+
#reckon(segment) {
|
|
605
|
+
if (!segment.demanded) return false;
|
|
606
|
+
const was = segment.deadline;
|
|
607
|
+
const active = segment.active;
|
|
608
|
+
this.#lead(segment);
|
|
609
|
+
if (segment.deadline === was) return false;
|
|
610
|
+
if (active) for (const entry of segment.entries) if (entry.placed === segment.told) this.#dirty.add(entry.placed);
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Its making and delivery taken off its deadline, as `sourceDeadline` does
|
|
616
|
+
* on the instant: the reckoning is done on the relative deadline, so it does
|
|
617
|
+
* not move with the time base.
|
|
618
|
+
*/
|
|
619
|
+
#lead(segment) {
|
|
620
|
+
const { output } = segment;
|
|
621
|
+
const span = segment.to - segment.from;
|
|
622
|
+
const processing = Number.isFinite(output.encodeSpeed) && output.encodeSpeed > 0 ? span / output.encodeSpeed : 0;
|
|
623
|
+
const outputBytes = Number.isFinite(output.bytesPerSecond) && output.bytesPerSecond > 0 ? output.bytesPerSecond * span : null;
|
|
624
|
+
const delivery = Number.isFinite(outputBytes) && outputBytes > 0 && Number.isFinite(output.linkMbps) && output.linkMbps > 0
|
|
625
|
+
? outputBytes * 8 / (output.linkMbps * 1_000_000) : 0;
|
|
626
|
+
if (Number.isFinite(segment.reached)) {
|
|
627
|
+
segment.deadline = segment.reached - (processing + delivery) * 1000;
|
|
628
|
+
segment.leadSeconds = Math.max(0, (segment.reached - segment.deadline) / 1000);
|
|
629
|
+
} else {
|
|
630
|
+
segment.deadline = segment.reached;
|
|
631
|
+
segment.leadSeconds = 0;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** The band a segment is told in: its own, or the first one's while it is the next run's input. */
|
|
636
|
+
#toldBand(segment) {
|
|
637
|
+
const first = segment.output.raisedFrom;
|
|
638
|
+
return first && segment.output.raised.has(segment) ? first.band : segment.band;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Put an active segment into the entries of its ranges. */
|
|
642
|
+
#activate(segment) {
|
|
643
|
+
const active = segment.demanded && !segment.ready && segment.ranges !== null;
|
|
644
|
+
if (!active) {
|
|
645
|
+
this.#deactivate(segment);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (segment.active) this.#deactivate(segment);
|
|
649
|
+
segment.active = true;
|
|
650
|
+
let same = segment.output.byPriority.get(segment.priority);
|
|
651
|
+
if (!same) segment.output.byPriority.set(segment.priority, same = new Set());
|
|
652
|
+
same.add(segment);
|
|
653
|
+
segment.told = this.#toldBand(segment);
|
|
654
|
+
for (const entry of segment.entries) {
|
|
655
|
+
let named = entry.byBand.get(segment.told);
|
|
656
|
+
if (!named) entry.byBand.set(segment.told, named = new Set());
|
|
657
|
+
named.add(segment);
|
|
658
|
+
this.#place(entry);
|
|
659
|
+
if (entry.placed === segment.told) this.#dirty.add(entry.placed);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Take a segment out of the entries of its ranges. */
|
|
664
|
+
#deactivate(segment) {
|
|
665
|
+
if (!segment.active) return;
|
|
666
|
+
segment.active = false;
|
|
667
|
+
const same = segment.output.byPriority.get(segment.priority);
|
|
668
|
+
same?.delete(segment);
|
|
669
|
+
if (same && same.size === 0) segment.output.byPriority.delete(segment.priority);
|
|
670
|
+
for (const entry of segment.entries) {
|
|
671
|
+
const named = entry.byBand.get(segment.told);
|
|
672
|
+
named?.delete(segment);
|
|
673
|
+
if (named && named.size === 0) entry.byBand.delete(segment.told);
|
|
674
|
+
if (entry.placed === segment.told) this.#dirty.add(segment.told);
|
|
675
|
+
this.#place(entry);
|
|
676
|
+
}
|
|
677
|
+
segment.told = null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Tell an entry in its best band, moving it there if that changed. */
|
|
681
|
+
#place(entry) {
|
|
682
|
+
const best = entry.best();
|
|
683
|
+
if (best === entry.placed) return;
|
|
684
|
+
if (entry.placed !== null) {
|
|
685
|
+
const list = this.#bands.get(entry.placed);
|
|
686
|
+
let at = lowerBound(list, entry.start);
|
|
687
|
+
while (at < list.length && list[at] !== entry) at++;
|
|
688
|
+
if (at < list.length) list.splice(at, 1);
|
|
689
|
+
if (list.length === 0) {
|
|
690
|
+
this.#bands.delete(entry.placed);
|
|
691
|
+
this.#bandWindows.delete(entry.placed);
|
|
692
|
+
} else this.#dirty.add(entry.placed);
|
|
693
|
+
}
|
|
694
|
+
entry.placed = best;
|
|
695
|
+
if (best === null) return;
|
|
696
|
+
let list = this.#bands.get(best);
|
|
697
|
+
if (!list) this.#bands.set(best, list = []);
|
|
698
|
+
let at = lowerBound(list, entry.start);
|
|
699
|
+
while (at < list.length && list[at].start === entry.start) at++;
|
|
700
|
+
list.splice(at, 0, entry);
|
|
701
|
+
this.#dirty.add(best);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** The windows of one band: its ranges that touch or overlap joined, each at its earliest deadline. */
|
|
705
|
+
#makeWindows(key) {
|
|
706
|
+
const list = this.#bands.get(key);
|
|
707
|
+
if (!list || list.length === 0) {
|
|
708
|
+
this.#bandWindows.delete(key);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const windows = [];
|
|
712
|
+
let open = null;
|
|
713
|
+
for (const entry of list) {
|
|
714
|
+
const deadline = entry.deadline();
|
|
715
|
+
if (open && entry.start <= open.byteEnd + 1) {
|
|
716
|
+
open.byteEnd = Math.max(open.byteEnd, entry.end);
|
|
717
|
+
open.deadline = Math.min(open.deadline, deadline);
|
|
718
|
+
} else {
|
|
719
|
+
open = { byteStart: entry.start, byteEnd: entry.end, deadline };
|
|
720
|
+
windows.push(open);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
this.#bandWindows.set(key, windows);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The next run's input raised to its first segment's place
|
|
728
|
+
* — the swarm told it as one group with its first segment, since WebTorrent
|
|
729
|
+
* keeps no order between several selections: the first segment is the output's highest
|
|
730
|
+
* priority, then lowest index, among the segments whose ranges are known,
|
|
731
|
+
* and the segments after it are raised one by one while the bytes held stay
|
|
732
|
+
* within what the run would copy, up to the first that is made or not yet
|
|
733
|
+
* worked out.
|
|
734
|
+
*/
|
|
735
|
+
#raise(output) {
|
|
736
|
+
let first = null;
|
|
737
|
+
let top = -Infinity;
|
|
738
|
+
for (const priority of output.byPriority.keys()) if (priority > top) top = priority;
|
|
739
|
+
for (const segment of output.byPriority.get(top) ?? []) if (!first || segment.index < first.index) first = segment;
|
|
740
|
+
const next = new Set();
|
|
741
|
+
if (first && output.target > 0) {
|
|
742
|
+
const holding = new HeldBytes();
|
|
743
|
+
holding.add(first.ranges);
|
|
744
|
+
for (let index = first.index + 1; index < output.segments.length; index++) {
|
|
745
|
+
const segment = output.segments[index];
|
|
746
|
+
if (!segment.active) break;
|
|
747
|
+
if (holding.total + holding.adds(segment.ranges) > output.target) break;
|
|
748
|
+
holding.add(segment.ranges);
|
|
749
|
+
next.add(segment);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const firstMoved = output.raisedFrom !== first || (first && output.raisedBand !== first.band);
|
|
753
|
+
const changed = new Set();
|
|
754
|
+
for (const segment of output.raised) if (!next.has(segment) || firstMoved) changed.add(segment);
|
|
755
|
+
for (const segment of next) if (!output.raised.has(segment) || firstMoved) changed.add(segment);
|
|
756
|
+
output.raised = next;
|
|
757
|
+
output.raisedFrom = first;
|
|
758
|
+
output.raisedBand = first?.band ?? null;
|
|
759
|
+
for (const segment of changed) if (segment.active) this.#activate(segment);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#dropOutput(output) {
|
|
763
|
+
for (const segment of output.segments) {
|
|
764
|
+
this.#deactivate(segment);
|
|
765
|
+
this.#unresolved.delete(segment);
|
|
766
|
+
}
|
|
767
|
+
this.#outputs.delete(output.outputKey);
|
|
768
|
+
this.#raiseAgain.delete(output);
|
|
769
|
+
this.#told = null;
|
|
770
|
+
}
|
|
771
|
+
}
|