@torrent-tv/proxy 2.98.2 → 2.98.4
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/encode/MatroskaInput.js +87 -30
- package/services/storage/segment-store/SegmentStore.js +21 -1
- package/services/torrent/PlaybackProjection.js +758 -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
|
@@ -49,6 +49,10 @@ export class DownloadMaps {
|
|
|
49
49
|
// Assistant 2026-10-07, torrent-tv/meta#95). A zone no new zone overlaps —
|
|
50
50
|
// a seek away — is withdrawn at once.
|
|
51
51
|
const retained = file.zones.flatMap(zone => {
|
|
52
|
+
// A window its resolver keeps current itself — the projection of the
|
|
53
|
+
// file's segments, brought to this map by the pass below — stands as it
|
|
54
|
+
// is until the pass states it (`PlaybackProjection`, torrent-tv/meta#178).
|
|
55
|
+
if (zone.standing === true) return [zone];
|
|
52
56
|
const interval = zone.downloadInterval ?? zone;
|
|
53
57
|
const current = sourceZones.exactly(interval);
|
|
54
58
|
if (!current) {
|
|
@@ -343,7 +347,10 @@ export class DownloadMaps {
|
|
|
343
347
|
const kept = file.packed;
|
|
344
348
|
if (kept && kept.zones === file.zones && kept.nativeZones === file.nativeZones) return kept.result;
|
|
345
349
|
const input = [...file.zones, ...file.nativeZones];
|
|
346
|
-
|
|
350
|
+
// Windows already packed by their resolver are not packed again unless
|
|
351
|
+
// something else is to be packed with them.
|
|
352
|
+
const result = file.nativeZones.length === 0 && file.zones.length > 0 && file.zones.every(zone => zone.standing === true)
|
|
353
|
+
? file.zones : this.#pack(input);
|
|
347
354
|
const relative = input.some(zone => !Number.isFinite(zone.deadlineAt) && Number.isFinite(zone.withinSeconds));
|
|
348
355
|
file.packed = relative ? null : { zones: file.zones, nativeZones: file.nativeZones, result };
|
|
349
356
|
return result;
|
|
@@ -435,7 +442,11 @@ function zoneIndexOf(file) {
|
|
|
435
442
|
function withRetained(resolved, retained) {
|
|
436
443
|
const keyOf = zone => typeof zone.outputKey === "string" && Number.isSafeInteger(zone.index) ? `${zone.outputKey}:${zone.index}` : null;
|
|
437
444
|
const resolvedKeys = new Set(resolved.map(keyOf).filter(key => key !== null));
|
|
445
|
+
// Windows a resolver keeps current are all of what it states: the ones it
|
|
446
|
+
// stated before are what it states now.
|
|
447
|
+
const standing = resolved.some(zone => zone.standing === true);
|
|
438
448
|
return [...resolved, ...retained.filter(zone => {
|
|
449
|
+
if (standing && zone.standing === true) return false;
|
|
439
450
|
const key = keyOf(zone);
|
|
440
451
|
return key === null || !resolvedKeys.has(key);
|
|
441
452
|
})];
|
|
@@ -388,6 +388,9 @@ export class Viewer {
|
|
|
388
388
|
* picture is in picture-in-picture. Absent means on screen.
|
|
389
389
|
* @param {boolean} [report.inPictureInPicture] - Whether the picture has been
|
|
390
390
|
* pulled out of the page, which is watching it with the tab hidden.
|
|
391
|
+
* @param {number} [report.generation] - Which of this viewer's viewings the
|
|
392
|
+
* page made the report in (`Assignments`). Absent from a page one release
|
|
393
|
+
* behind; then the report is taken as current.
|
|
391
394
|
* @param {number} [now]
|
|
392
395
|
*/
|
|
393
396
|
report(
|
|
@@ -406,11 +409,18 @@ export class Viewer {
|
|
|
406
409
|
onScreen,
|
|
407
410
|
inPictureInPicture,
|
|
408
411
|
qualityMode,
|
|
409
|
-
visiblePicture
|
|
412
|
+
visiblePicture,
|
|
413
|
+
generation
|
|
410
414
|
},
|
|
411
415
|
now = Date.now()
|
|
412
416
|
) {
|
|
413
417
|
this.noteVisiblePicture(visiblePicture);
|
|
418
|
+
// A REPORT MADE BEFORE THE VIEWER'S LAST SEEK says where they were, not
|
|
419
|
+
// where they are: sent just before the seek and answered after it, it
|
|
420
|
+
// would place them back in the viewing they left, and the deadlines worked
|
|
421
|
+
// out from their position would follow it (torrent-tv/meta#178 review).
|
|
422
|
+
// What it measured of the link is still a measurement of the link.
|
|
423
|
+
const stale = this.isOfAnEarlierViewing(generation);
|
|
414
424
|
// Whether the size on their screen was picked by hand or is the automatic
|
|
415
425
|
// choice. Kept only when stated: a page one release behind says nothing,
|
|
416
426
|
// and is taken as picking, which is what every page was before.
|
|
@@ -428,6 +438,7 @@ export class Viewer {
|
|
|
428
438
|
const linkMeasuredAt = hasLinkMeasurement ? now : this.netReport?.linkMeasuredAt ?? null;
|
|
429
439
|
const sampleMbps = Number(linkSampleMbps);
|
|
430
440
|
const sampleAt = Number(linkSampleAt);
|
|
441
|
+
const previous = this.netReport;
|
|
431
442
|
const hasPlaybackSample = Number.isFinite(sampleMbps) && sampleMbps > 0 &&
|
|
432
443
|
Number.isFinite(sampleAt) && sampleAt > 0;
|
|
433
444
|
const newerPlaybackSample = hasPlaybackSample && sampleAt > (this.netReport?.linkSampleAt ?? 0);
|
|
@@ -458,6 +469,13 @@ export class Viewer {
|
|
|
458
469
|
};
|
|
459
470
|
// Where the picture is now. A report states a place the viewer has played
|
|
460
471
|
// to, not a place they moved to: their buffer's history stands.
|
|
472
|
+
if (stale) {
|
|
473
|
+
this.netReport = { ...this.netReport, positionSeconds: previous?.positionSeconds ?? null,
|
|
474
|
+
bufferedAheadSec: previous?.bufferedAheadSec ?? 0, bufferedRanges: previous?.bufferedRanges ?? null,
|
|
475
|
+
timestampOffsets: previous?.timestampOffsets ?? {}, at: previous?.at ?? now };
|
|
476
|
+
this.seen(now);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
461
479
|
if (Number.isFinite(positionSeconds) && positionSeconds >= 0) {
|
|
462
480
|
this.#place(/** @type {number} */ (positionSeconds), now);
|
|
463
481
|
}
|
|
@@ -491,6 +509,18 @@ export class Viewer {
|
|
|
491
509
|
this.seen(now);
|
|
492
510
|
}
|
|
493
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Whether something the page stated in viewing `generation` was stated
|
|
514
|
+
* before this viewer's last seek. A page that names no viewing is taken as
|
|
515
|
+
* current, as every page was before it could name one.
|
|
516
|
+
*
|
|
517
|
+
* @param {number | undefined} generation
|
|
518
|
+
* @returns {boolean}
|
|
519
|
+
*/
|
|
520
|
+
isOfAnEarlierViewing(generation) {
|
|
521
|
+
return Number.isInteger(generation) && generation < this.assignments.generation;
|
|
522
|
+
}
|
|
523
|
+
|
|
494
524
|
/**
|
|
495
525
|
* Whether this viewer is here.
|
|
496
526
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function
|
|
1
|
+
/** The tracks of this file an output reads, and how: copied or encoded again. */
|
|
2
|
+
export function trackChoicesOf(output, fileIndex) {
|
|
3
3
|
const tracks = [];
|
|
4
4
|
for (const type of ["video", "audio"]) {
|
|
5
5
|
const spec = output.spec?.[type];
|
|
@@ -7,59 +7,5 @@ export function segmentDemands(output, fileIndex, zones) {
|
|
|
7
7
|
tracks.push({ type, index: type === "video" ? 0 : spec.trackIndex,
|
|
8
8
|
mode: type === "video" ? spec.encode ? "transcode" : "copy" : spec.transcode ? "transcode" : "copy" });
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
const grid = output.timeline?.published ?? output.timeline?.boundaries;
|
|
12
|
-
if (!Array.isArray(grid)) return [];
|
|
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;
|
|
22
|
-
for (let index = 0; index + 1 < grid.length; index++) {
|
|
23
|
-
const from = grid[index], to = grid[index + 1];
|
|
24
|
-
if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
|
|
25
|
-
if (ordered) while (first < zones.length && zones[first].to <= from) first += 1;
|
|
26
|
-
// The zones a segment overlaps are folded as they are met, into numbers:
|
|
27
|
-
// a list of them, and the lists made of it for each field, were five
|
|
28
|
-
// arrays for every segment of a film on every pass (r30,
|
|
29
|
-
// torrent-tv/meta#178).
|
|
30
|
-
let owner = null, ownerDeadline = Infinity, priority = 0, urgent = false, behind = true, deferred = true;
|
|
31
|
-
for (let at = ordered ? first : 0; at < zones.length; at += 1) {
|
|
32
|
-
const zone = zones[at];
|
|
33
|
-
if (ordered && !(zone.from < to)) break;
|
|
34
|
-
if (!(zone.priority > 0) || !(zone.from < to && zone.to > from)) continue;
|
|
35
|
-
// A zone states the deadline of its near edge. A later segment within it
|
|
36
|
-
// is reached later, at one second of film per second of playback.
|
|
37
|
-
const deadline = Number.isFinite(zone.deadlineAt) ? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
|
|
38
|
-
if (owner === null || deadline < ownerDeadline) { owner = zone; ownerDeadline = deadline; }
|
|
39
|
-
if (zone.priority > priority) priority = zone.priority;
|
|
40
|
-
if (zone.urgent) urgent = true;
|
|
41
|
-
if (!zone.behind) behind = false;
|
|
42
|
-
if (!zone.deferred) deferred = false;
|
|
43
|
-
}
|
|
44
|
-
if (owner === null) continue;
|
|
45
|
-
demands.push({ index, from, to, tracks, owner, priority, urgent, behind, deferred, deadlineAt: ownerDeadline });
|
|
46
|
-
}
|
|
47
|
-
return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Whether zones come ascending and without overlaps, as a map's runs do. */
|
|
51
|
-
function ascendingAndApart(zones) {
|
|
52
|
-
for (let index = 1; index < zones.length; index += 1) {
|
|
53
|
-
if (!(zones[index].from >= zones[index - 1].to)) return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Source bytes must arrive before measured processing and delivery consume time. */
|
|
59
|
-
export function sourceDeadline(demand, { encodeSpeed, outputBytes, linkMbps }) {
|
|
60
|
-
const span = demand.to - demand.from;
|
|
61
|
-
const processing = Number.isFinite(encodeSpeed) && encodeSpeed > 0 ? span / encodeSpeed : 0;
|
|
62
|
-
const delivery = Number.isFinite(outputBytes) && outputBytes > 0 && Number.isFinite(linkMbps) && linkMbps > 0
|
|
63
|
-
? outputBytes * 8 / (linkMbps * 1_000_000) : 0;
|
|
64
|
-
return Number.isFinite(demand.deadlineAt) ? demand.deadlineAt - (processing + delivery) * 1000 : demand.deadlineAt;
|
|
10
|
+
return tracks;
|
|
65
11
|
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What one pass that turns a file's playback map into bytes is worked out
|
|
3
|
-
* from, so a pass whose inputs have not changed hands back what the last one
|
|
4
|
-
* resolved (torrent-tv/meta#178).
|
|
5
|
-
*
|
|
6
|
-
* The viewer's map moves a second at a time while segments are seconds long,
|
|
7
|
-
* so most passes give the zones the pass before gave: on Home Assistant 30–110
|
|
8
|
-
* of 50–120 download maps a minute came out unchanged, and working them out
|
|
9
|
-
* took 72 s of the main thread in fifteen minutes (r22). The deadlines are left
|
|
10
|
-
* out of what is compared and their order is kept in it: a segment's deadline
|
|
11
|
-
* is when the viewer reaches it, which moves only when they stop or seek, and
|
|
12
|
-
* what is sent to the torrent is compared by the deadlines' order alone
|
|
13
|
-
* (`DownloadMaps` `shapeOf`).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {object} params
|
|
18
|
-
* @param {number} params.pieceLength - The precision the torrent fetches at.
|
|
19
|
-
* @param {number} params.shift - Where the file's own clock starts.
|
|
20
|
-
* @param {Map<string, number | null>} params.targets - Each output's next input, by output.
|
|
21
|
-
* @param {Array<{ outputKey: string, index: number, priority: number, urgent?: boolean,
|
|
22
|
-
* behind?: boolean, deferred?: boolean, from: number, to: number,
|
|
23
|
-
* sourceInterval?: { from: number, to: number } }>} params.demands - In the
|
|
24
|
-
* order of their deadlines.
|
|
25
|
-
* @returns {{ outputs: string[], inputs: Float64Array }}
|
|
26
|
-
*/
|
|
27
|
-
export function passInputs({ pieceLength, shift, targets, demands }) {
|
|
28
|
-
const outputs = [...targets.keys()];
|
|
29
|
-
const position = new Map(outputs.map((outputKey, at) => [outputKey, at]));
|
|
30
|
-
// Numbers in a list made to its length: grown a value at a time, eight for
|
|
31
|
-
// each of a film's 3500 segments, it was most of what comparing a pass cost
|
|
32
|
-
// (r30, torrent-tv/meta#178).
|
|
33
|
-
const inputs = new Float64Array(2 + targets.size + demands.length * 8);
|
|
34
|
-
let at = 0;
|
|
35
|
-
inputs[at++] = pieceLength;
|
|
36
|
-
inputs[at++] = shift;
|
|
37
|
-
for (const target of targets.values()) inputs[at++] = target ?? -1;
|
|
38
|
-
for (const demand of demands) {
|
|
39
|
-
// The segment's own interval too: a cut grid read again can give one
|
|
40
|
-
// number other bytes.
|
|
41
|
-
inputs[at++] = position.get(demand.outputKey) ?? -1;
|
|
42
|
-
inputs[at++] = demand.index;
|
|
43
|
-
inputs[at++] = demand.priority;
|
|
44
|
-
inputs[at++] = (demand.urgent ? 1 : 0) + (demand.behind ? 2 : 0) + (demand.deferred ? 4 : 0);
|
|
45
|
-
inputs[at++] = demand.from;
|
|
46
|
-
inputs[at++] = demand.to;
|
|
47
|
-
inputs[at++] = demand.sourceInterval?.from ?? -1;
|
|
48
|
-
inputs[at++] = demand.sourceInterval?.to ?? -1;
|
|
49
|
-
}
|
|
50
|
-
return { outputs, inputs };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether a pass is worked out from what an earlier one was.
|
|
55
|
-
*
|
|
56
|
-
* @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> } | undefined} last
|
|
57
|
-
* @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> }} next
|
|
58
|
-
* @returns {boolean}
|
|
59
|
-
*/
|
|
60
|
-
export function samePass(last, next) {
|
|
61
|
-
return Boolean(last) && last.tracks === next.tracks &&
|
|
62
|
-
sameValues(last.outputs, next.outputs) && sameValues(last.inputs, next.inputs);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Whether two lists hold the same values in the same order. */
|
|
66
|
-
function sameValues(left, right) {
|
|
67
|
-
if (left.length !== right.length) return false;
|
|
68
|
-
for (let index = 0; index < left.length; index++) if (!Object.is(left[index], right[index])) return false;
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The next run's whole input, asked of the swarm as one group.
|
|
3
|
-
*
|
|
4
|
-
* While anything urgent is missing, the swarm is told only the first class
|
|
5
|
-
* (`torrent/download/SwarmSelection.js`), and at a cold start the map's first
|
|
6
|
-
* class is one segment's input. A peer holding none of its pieces then has
|
|
7
|
-
* nothing to send, and a run is admitted only as far as its input has arrived
|
|
8
|
-
* (`encode/EncodeInputs.js`), so it opens again a few seconds of film later.
|
|
9
|
-
*
|
|
10
|
-
* WebTorrent 2.8.5 keeps no order between several non-zero selections — it
|
|
11
|
-
* rotates them (`shufflePriority`) — so the run's input is not stated as a
|
|
12
|
-
* second group behind the first: the segments it covers take the first
|
|
13
|
-
* segment's place in the map, and the swarm is told them as one group. How
|
|
14
|
-
* far that reaches is what `EncodeInputs` would copy for the run, from its
|
|
15
|
-
* copy rate, its open and the memory allowed; it is not worked out again here.
|
|
16
|
-
* A read stopped on its bytes is stated at a level of its own above any of
|
|
17
|
-
* this, and a seek, which moves the map's first segment, moves this with it
|
|
18
|
-
* (torrent-tv/meta#178).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The bytes a run's input holds, each counted once: the declarations and
|
|
23
|
-
* indexes every segment of an original file names are held once by the run
|
|
24
|
-
* that reads them. Kept merged and in order, so a segment's ranges are
|
|
25
|
-
* counted against it by search rather than by sorting everything held again —
|
|
26
|
-
* a soundtrack's segments add a few bytes each, and its stretch is walked far.
|
|
27
|
-
*/
|
|
28
|
-
class HeldBytes {
|
|
29
|
-
/** Inclusive [start, end], merged, ascending. @type {Array<[number, number]>} */
|
|
30
|
-
#ranges = [];
|
|
31
|
-
total = 0;
|
|
32
|
-
|
|
33
|
-
/** @param {Array<[number, number]>} ranges */
|
|
34
|
-
constructor(ranges = []) {
|
|
35
|
-
this.add(ranges);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** The first held range that ends at or after `at`. */
|
|
39
|
-
#from(at) {
|
|
40
|
-
let low = 0;
|
|
41
|
-
let high = this.#ranges.length;
|
|
42
|
-
while (low < high) {
|
|
43
|
-
const middle = (low + high) >> 1;
|
|
44
|
-
if (this.#ranges[middle][1] < at) low = middle + 1;
|
|
45
|
-
else high = middle;
|
|
46
|
-
}
|
|
47
|
-
return low;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* What these ranges would add, without adding them.
|
|
52
|
-
*
|
|
53
|
-
* @param {Array<[number, number]>} ranges
|
|
54
|
-
* @returns {number}
|
|
55
|
-
*/
|
|
56
|
-
adds(ranges) {
|
|
57
|
-
let added = 0;
|
|
58
|
-
for (const [start, end] of merged(ranges)) {
|
|
59
|
-
let at = start;
|
|
60
|
-
for (let index = this.#from(start); at <= end && index < this.#ranges.length && this.#ranges[index][0] <= end; index++) {
|
|
61
|
-
const [heldStart, heldEnd] = this.#ranges[index];
|
|
62
|
-
if (heldStart > at) added += heldStart - at;
|
|
63
|
-
at = Math.max(at, heldEnd + 1);
|
|
64
|
-
}
|
|
65
|
-
if (at <= end) added += end - at + 1;
|
|
66
|
-
}
|
|
67
|
-
return added;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** @param {Array<[number, number]>} ranges */
|
|
71
|
-
add(ranges) {
|
|
72
|
-
for (const [start, end] of merged(ranges)) {
|
|
73
|
-
this.total += this.adds([[start, end]]);
|
|
74
|
-
// Joined with every held range it touches or overlaps.
|
|
75
|
-
const first = this.#from(start - 1);
|
|
76
|
-
let last = first;
|
|
77
|
-
let joinedStart = start;
|
|
78
|
-
let joinedEnd = end;
|
|
79
|
-
while (last < this.#ranges.length && this.#ranges[last][0] <= end + 1) {
|
|
80
|
-
joinedStart = Math.min(joinedStart, this.#ranges[last][0]);
|
|
81
|
-
joinedEnd = Math.max(joinedEnd, this.#ranges[last][1]);
|
|
82
|
-
last++;
|
|
83
|
-
}
|
|
84
|
-
this.#ranges.splice(first, last - first, [joinedStart, joinedEnd]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Inclusive ranges merged where they overlap or touch, ascending. @param {Array<[number, number]>} ranges */
|
|
90
|
-
function merged(ranges) {
|
|
91
|
-
const sorted = [...ranges].sort((left, right) => left[0] - right[0]);
|
|
92
|
-
const out = [];
|
|
93
|
-
for (const [start, end] of sorted) {
|
|
94
|
-
const previous = out.at(-1);
|
|
95
|
-
if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
|
|
96
|
-
else out.push([start, end]);
|
|
97
|
-
}
|
|
98
|
-
return out;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The map's byte zones with each output's next run input raised to its first
|
|
103
|
-
* segment's place.
|
|
104
|
-
*
|
|
105
|
-
* @template {{ outputKey: string, index: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean,
|
|
106
|
-
* ranges?: Array<[number, number]>, byteStart?: number, byteEnd?: number }} Zone
|
|
107
|
-
* @param {Zone[]} zones - One or more per segment, as the map's pass made
|
|
108
|
-
* them: a segment's zone names all its ranges (`ranges`), any other one
|
|
109
|
-
* range (`byteStart`-`byteEnd`).
|
|
110
|
-
* @param {(outputKey: string) => number | null} targetBytesOf - What the
|
|
111
|
-
* output's next run would be given to copy; nothing is raised without it.
|
|
112
|
-
* @returns {Zone[]} In their order; raised zones are copies.
|
|
113
|
-
*/
|
|
114
|
-
export function expandToTargetInputs(zones, targetBytesOf) {
|
|
115
|
-
// ONE WALK FOR EACH OUTPUT'S FIRST SEGMENT, and the segments after it only
|
|
116
|
-
// as far as the run's input reaches. The map names every segment of a film,
|
|
117
|
-
// and each pass used to group all of them and work out each one's highest
|
|
118
|
-
// priority before looking at the few the run copies: 26 s of the main thread
|
|
119
|
-
// in fifteen minutes of a two-hour AVI on Home Assistant (r24,
|
|
120
|
-
// torrent-tv/meta#178).
|
|
121
|
-
/** @type {Map<string, { index: number, priority: number, zone: Zone }>} */
|
|
122
|
-
const first = new Map();
|
|
123
|
-
for (const zone of zones) {
|
|
124
|
-
const best = first.get(zone.outputKey);
|
|
125
|
-
if (!best || zone.priority > best.priority || (zone.priority === best.priority && zone.index < best.index)) {
|
|
126
|
-
first.set(zone.outputKey, { index: zone.index, priority: zone.priority, zone });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/** @type {Map<string, { first: { index: number, priority: number, zone: Zone }, target: number, after: Map<number, Zone[]> }>} */
|
|
130
|
-
const runs = new Map();
|
|
131
|
-
for (const [outputKey, best] of first) {
|
|
132
|
-
const target = targetBytesOf(outputKey);
|
|
133
|
-
if (target > 0) runs.set(outputKey, { first: best, target, after: new Map() });
|
|
134
|
-
}
|
|
135
|
-
if (runs.size === 0) return zones;
|
|
136
|
-
/** @type {Map<string, Array<[number, number]>>} */
|
|
137
|
-
const held = new Map();
|
|
138
|
-
for (const zone of zones) {
|
|
139
|
-
const run = runs.get(zone.outputKey);
|
|
140
|
-
if (!run) continue;
|
|
141
|
-
if (zone.index === run.first.index) {
|
|
142
|
-
let ranges = held.get(zone.outputKey);
|
|
143
|
-
if (!ranges) held.set(zone.outputKey, ranges = []);
|
|
144
|
-
ranges.push(...rangesOf(zone));
|
|
145
|
-
} else if (zone.index > run.first.index) {
|
|
146
|
-
const list = run.after.get(zone.index);
|
|
147
|
-
if (list) list.push(zone);
|
|
148
|
-
else run.after.set(zone.index, [zone]);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/** @type {Map<Zone, Zone>} */
|
|
152
|
-
const raised = new Map();
|
|
153
|
-
for (const [outputKey, run] of runs) {
|
|
154
|
-
const holding = new HeldBytes(held.get(outputKey) ?? []);
|
|
155
|
-
// The run's input is a stretch of consecutive segments: one already made,
|
|
156
|
-
// or not yet worked out, ends it.
|
|
157
|
-
for (let index = run.first.index + 1; run.after.has(index); index++) {
|
|
158
|
-
const list = run.after.get(index);
|
|
159
|
-
const ranges = list.flatMap(rangesOf);
|
|
160
|
-
if (holding.total + holding.adds(ranges) > run.target) break;
|
|
161
|
-
holding.add(ranges);
|
|
162
|
-
for (const zone of list) {
|
|
163
|
-
raised.set(zone, { ...zone, priority: run.first.priority, urgent: run.first.zone.urgent,
|
|
164
|
-
behind: run.first.zone.behind, deferred: run.first.zone.deferred });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return raised.size === 0 ? zones : zones.map(zone => raised.get(zone) ?? zone);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** The ranges a zone names, inclusive. */
|
|
172
|
-
function rangesOf(zone) {
|
|
173
|
-
return Array.isArray(zone.ranges) ? zone.ranges : [[zone.byteStart, zone.byteEnd]];
|
|
174
|
-
}
|