@torrent-tv/proxy 2.97.28 → 2.97.30
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
CHANGED
package/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { IndexMemory } from "./services/storage/IndexMemory.js";
|
|
|
18
18
|
import { probePackets } from "./services/media/probe-packets.js";
|
|
19
19
|
import { segmentDemands, sourceDeadline } from "./services/viewer/segment-demands.js";
|
|
20
20
|
import { expandToTargetInputs } from "./services/viewer/target-input-expansion.js";
|
|
21
|
+
import { SegmentInputTable } from "./services/viewer/segment-input-table.js";
|
|
21
22
|
import { SourcePreparation } from "./services/viewer/SourcePreparation.js";
|
|
22
23
|
import { handleHealthzGet } from "./routes/healthz/get.js";
|
|
23
24
|
import { handleApiDeliverySinkGet } from "./routes/api/delivery-sink/get.js";
|
|
@@ -185,6 +186,7 @@ export async function startProxyServer({
|
|
|
185
186
|
for (const key of segmentSourceRanges.keys()) if (key.startsWith(`${sourceKey}:`)) segmentSourceRanges.delete(key);
|
|
186
187
|
for (const key of segmentReadings.keys()) if (key.startsWith(`${sourceKey}:`)) segmentReadings.delete(key);
|
|
187
188
|
for (const key of resolvedPasses.keys()) if (key.startsWith(`${sourceKey}:`)) resolvedPasses.delete(key);
|
|
189
|
+
segmentInputs.forget(sourceKey);
|
|
188
190
|
outputParts.encodeInputs?.bytesChanged();
|
|
189
191
|
},
|
|
190
192
|
// Pieces arriving is ANNOUNCED by the thread that owns the swarm; what is
|
|
@@ -476,6 +478,33 @@ export async function startProxyServer({
|
|
|
476
478
|
* from and what it resolved (`playback-pass.js`); forgotten with the source.
|
|
477
479
|
*/
|
|
478
480
|
const resolvedPasses = new Map();
|
|
481
|
+
/** Which source bytes each segment of an output reads, for the forecast (`segment-input-table.js`). */
|
|
482
|
+
const segmentInputs = new SegmentInputTable();
|
|
483
|
+
// WHAT THE PASSES COST, said once a minute: how many were taken whole from
|
|
484
|
+
// the last one and how many worked out again, how many segments and ranges
|
|
485
|
+
// those went through, how many zones they built, and the time from a pass's
|
|
486
|
+
// start to its result — which includes waits for reads, so it bounds the
|
|
487
|
+
// main thread's share rather than measuring it. A pass worked out again
|
|
488
|
+
// whose zones state the same bytes as the last is the repeated work worth
|
|
489
|
+
// removing; how often that happens is what the map's own "unchanged not
|
|
490
|
+
// sent" count says (torrent-tv/meta#178 review).
|
|
491
|
+
const passCosts = { since: Date.now(), reused: 0, worked: 0, incomplete: 0, segments: 0, ranges: 0, zones: 0, built: 0, buildMs: 0, ms: 0, maxMs: 0 };
|
|
492
|
+
const notePass = ({ reused = false, complete = true, segments = 0, ranges = 0, zones = 0, ms = 0 }) => {
|
|
493
|
+
if (reused) passCosts.reused += 1;
|
|
494
|
+
else {
|
|
495
|
+
passCosts.worked += 1;
|
|
496
|
+
if (!complete) passCosts.incomplete += 1;
|
|
497
|
+
passCosts.segments += segments; passCosts.ranges += ranges; passCosts.zones += zones;
|
|
498
|
+
passCosts.ms += ms; passCosts.maxMs = Math.max(passCosts.maxMs, ms);
|
|
499
|
+
}
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
if (now - passCosts.since < 60_000) return;
|
|
502
|
+
logger.info(`download map passes in ${((now - passCosts.since) / 1000).toFixed(1)}s: ${passCosts.reused} taken whole from the last, ` +
|
|
503
|
+
`${passCosts.worked} worked out (${passCosts.incomplete} incomplete) over ${passCosts.segments} segment(s) and ${passCosts.ranges} range(s), ` +
|
|
504
|
+
`giving ${passCosts.zones} zone(s); ${Math.round(passCosts.ms)}ms from start to result in all, longest ${Math.round(passCosts.maxMs)}ms; ` +
|
|
505
|
+
`${passCosts.built} zone list(s) built for the map in ${Math.round(passCosts.buildMs)}ms`);
|
|
506
|
+
Object.assign(passCosts, { since: now, reused: 0, worked: 0, incomplete: 0, segments: 0, ranges: 0, zones: 0, built: 0, buildMs: 0, ms: 0, maxMs: 0 });
|
|
507
|
+
};
|
|
479
508
|
/** A segment's ranges joined at the precision the torrent fetches, once per kept list and piece length. */
|
|
480
509
|
const joinedRanges = new WeakMap();
|
|
481
510
|
const joinedRangesOf = (ranges, pieceLength) => {
|
|
@@ -513,12 +542,20 @@ export async function startProxyServer({
|
|
|
513
542
|
// A zone's ranges are stated to the torrent at the precision it fetches:
|
|
514
543
|
// ranges closer than a piece hold the same pieces as one.
|
|
515
544
|
const pieceLength = Number(torrentPool.knownTorrent(map.sourceKey)?.pieceLength);
|
|
545
|
+
const passStartedAt = performance.now();
|
|
546
|
+
let segments = 0;
|
|
516
547
|
const converted = [];
|
|
517
548
|
const demands = [];
|
|
518
549
|
// What each output's next run would copy, asked of the swarm as one group
|
|
519
550
|
// with its first segment (`target-input-expansion.js`).
|
|
520
551
|
const targets = new Map();
|
|
521
|
-
const expanded = () =>
|
|
552
|
+
const expanded = () => {
|
|
553
|
+
const startedAt = performance.now();
|
|
554
|
+
const zones = expandToTargetInputs(converted, outputKey => targets.get(outputKey) ?? null);
|
|
555
|
+
passCosts.built += 1;
|
|
556
|
+
passCosts.buildMs += performance.now() - startedAt;
|
|
557
|
+
return zones;
|
|
558
|
+
};
|
|
522
559
|
for (const output of outputParts.outputs.values()) {
|
|
523
560
|
if (output.file.sourceKey !== map.sourceKey) continue;
|
|
524
561
|
const consumers = [...outputParts.viewers.forOutput(output).values()].filter(viewer =>
|
|
@@ -552,7 +589,10 @@ export async function startProxyServer({
|
|
|
552
589
|
const fileKey = `${map.sourceKey}:${map.fileIndex}`;
|
|
553
590
|
const pass = { tracks: tracks.value, ...passInputs({ pieceLength, shift, targets, demands }) };
|
|
554
591
|
const last = resolvedPasses.get(fileKey);
|
|
555
|
-
if (samePass(last, pass))
|
|
592
|
+
if (samePass(last, pass)) {
|
|
593
|
+
notePass({ reused: true });
|
|
594
|
+
return last.result;
|
|
595
|
+
}
|
|
556
596
|
// Kept only from a pass that worked every segment out; one that met a
|
|
557
597
|
// reading not there yet is worked out again until it is.
|
|
558
598
|
let complete = true;
|
|
@@ -588,7 +628,12 @@ export async function startProxyServer({
|
|
|
588
628
|
const { wanted, modes, interval, rangesKey } = reading;
|
|
589
629
|
const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
|
|
590
630
|
const convert = ranges => {
|
|
591
|
-
|
|
631
|
+
const joined = joinedRangesOf(ranges, pieceLength);
|
|
632
|
+
segments += 1;
|
|
633
|
+
// What the segment reads is kept for the forecast in a table of its
|
|
634
|
+
// own, whatever form the torrent is told it in.
|
|
635
|
+
segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
|
|
636
|
+
for (const [byteStart, byteEnd] of joined) converted.push({ ...demand,
|
|
592
637
|
downloadInterval: { from: zone.owner.from, to: zone.owner.to }, byteStart, byteEnd });
|
|
593
638
|
};
|
|
594
639
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
@@ -649,6 +694,7 @@ export async function startProxyServer({
|
|
|
649
694
|
convert(input.ranges);
|
|
650
695
|
}
|
|
651
696
|
const result = expanded();
|
|
697
|
+
notePass({ complete, segments, ranges: converted.length, zones: result.length, ms: performance.now() - passStartedAt });
|
|
652
698
|
if (complete) resolvedPasses.set(fileKey, { ...pass, result });
|
|
653
699
|
else resolvedPasses.delete(fileKey);
|
|
654
700
|
return result;
|
|
@@ -751,7 +797,7 @@ export async function startProxyServer({
|
|
|
751
797
|
readMetadataActivity: () => containerOrchestrator.activity(),
|
|
752
798
|
onViewerChanged: () => { void sourcePreparation?.refresh(); onStateChanged(); },
|
|
753
799
|
onEncodersChanged: () => onStateChanged(),
|
|
754
|
-
sourceInputsFor: (output, index) =>
|
|
800
|
+
sourceInputsFor: (output, index) => segmentInputs.inputsFor(output.file.sourceKey, output.outputKey, index),
|
|
755
801
|
readSourceMedia: async (params) => {
|
|
756
802
|
await playbackPlanner.getPlan(params);
|
|
757
803
|
const media = playbackPlanner.getCachedMediaInfo(params);
|
|
@@ -20,8 +20,6 @@ export class DownloadMaps {
|
|
|
20
20
|
#clock;
|
|
21
21
|
/** What the torrent is told of the playback zones: as many windows as say the same thing. */
|
|
22
22
|
#pack;
|
|
23
|
-
/** What a list of zones says each segment reads, by the list. @type {WeakMap<object[], Map>} */
|
|
24
|
-
#inputsOf = new WeakMap();
|
|
25
23
|
|
|
26
24
|
constructor({ publish, resolvePlayback = async map => map.zones, log = null, now = Date.now,
|
|
27
25
|
clock = () => performance.timeOrigin + performance.now(), pack = zones => zones }) {
|
|
@@ -59,10 +57,19 @@ export class DownloadMaps {
|
|
|
59
57
|
}
|
|
60
58
|
const deadlineAt = Number.isFinite(current.deadlineAt)
|
|
61
59
|
? current.deadlineAt + (Math.max(0, zone.from - current.from) - (zone.leadSeconds ?? 0)) * 1000 : current.deadlineAt;
|
|
62
|
-
|
|
60
|
+
// A deadline worked out again on a later clock is no change: the
|
|
61
|
+
// torrent orders by deadlines and is sent their order, which the pass
|
|
62
|
+
// below states with the rest.
|
|
63
|
+
if (Object.keys(current).some(key => key !== "from" && key !== "to" && key !== "deadlineAt" && key !== "withinSeconds" &&
|
|
64
|
+
!Object.is(zone[key], current[key]))) changed = true;
|
|
63
65
|
return [{ ...zone, ...current, from: zone.from, to: zone.to, deadlineAt }];
|
|
64
66
|
});
|
|
65
|
-
|
|
67
|
+
// THE ZONES STAND AS THEY ARE UNLESS ONE IS WITHDRAWN OR RE-WORDED. Set
|
|
68
|
+
// anew on every pass, they were a new list each time, so every metadata
|
|
69
|
+
// statement during the pass packed the whole film again: 24 s of the main
|
|
70
|
+
// thread in fifteen minutes of a two-hour AVI on Home Assistant (r24,
|
|
71
|
+
// torrent-tv/meta#178).
|
|
72
|
+
if (changed) this.#setZones(file, retained);
|
|
66
73
|
for (const [statement, metadata] of file.metadata) {
|
|
67
74
|
if (metadata.interval && !this.wantsInterval(sourceKey, fileIndex, metadata.interval)) {
|
|
68
75
|
file.metadata.delete(statement);
|
|
@@ -143,19 +150,6 @@ export class DownloadMaps {
|
|
|
143
150
|
return this.#files.get(`${sourceKey}:${fileIndex}`)?.sourceZones.some(zone => zone.from === from && zone.to === to) ?? false;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
/** Exact source ranges already resolved by the download map, without new reads. */
|
|
147
|
-
inputsForOutput(sourceKey, outputKey, index) {
|
|
148
|
-
const sources = [];
|
|
149
|
-
for (const file of this.#files.values()) {
|
|
150
|
-
if (file.sourceKey !== sourceKey) continue;
|
|
151
|
-
const ranges = file.inputs.get(outputKey)?.get(index);
|
|
152
|
-
if (!ranges?.length) continue;
|
|
153
|
-
sources.push({ sourceId: `${sourceKey}:${file.fileIndex}`,
|
|
154
|
-
ranges });
|
|
155
|
-
}
|
|
156
|
-
return sources.length ? sources : null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
153
|
metadata({ sourceKey, fileIndex, statement, result, priority = 100, urgent = true, deadlineAt = 0, interval, leadSeconds = 0, scope = "playback", order = 0 }) {
|
|
160
154
|
if (!Number.isFinite(leadSeconds) || leadSeconds < 0) throw new TypeError("Metadata preparation time must be finite and nonnegative.");
|
|
161
155
|
const file = this.#file(sourceKey, fileIndex);
|
|
@@ -246,7 +240,7 @@ export class DownloadMaps {
|
|
|
246
240
|
const key = `${sourceKey}:${fileIndex}`;
|
|
247
241
|
let file = this.#files.get(key);
|
|
248
242
|
if (!file) {
|
|
249
|
-
file = { sourceKey, fileIndex, durationSeconds: 0, zones: [],
|
|
243
|
+
file = { sourceKey, fileIndex, durationSeconds: 0, zones: [], nativeZones: [], sourceZones: [], revision: 0, epoch: ++this.#nextEpoch, metadata: new Map(), pending: Promise.resolve(), dirty: true };
|
|
250
244
|
this.#files.set(key, file);
|
|
251
245
|
}
|
|
252
246
|
return file;
|
|
@@ -256,24 +250,6 @@ export class DownloadMaps {
|
|
|
256
250
|
file.zones = zones;
|
|
257
251
|
// What `#emit` reads has changed, whether or not it is published now.
|
|
258
252
|
file.dirty = true;
|
|
259
|
-
// A pass that resolves what the pass before resolved hands back the same
|
|
260
|
-
// zones (`resolvedPasses` in server.js), and what they say of each segment
|
|
261
|
-
// is the same too.
|
|
262
|
-
const known = this.#inputsOf.get(zones);
|
|
263
|
-
if (known) {
|
|
264
|
-
file.inputs = known;
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
file.inputs = new Map();
|
|
268
|
-
this.#inputsOf.set(zones, file.inputs);
|
|
269
|
-
for (const zone of zones) {
|
|
270
|
-
if (typeof zone.outputKey !== "string" || !Number.isSafeInteger(zone.index)) continue;
|
|
271
|
-
let output = file.inputs.get(zone.outputKey);
|
|
272
|
-
if (!output) file.inputs.set(zone.outputKey, output = new Map());
|
|
273
|
-
let ranges = output.get(zone.index);
|
|
274
|
-
if (!ranges) output.set(zone.index, ranges = []);
|
|
275
|
-
ranges.push({ start: zone.byteStart, end: zone.byteEnd + 1 });
|
|
276
|
-
}
|
|
277
253
|
}
|
|
278
254
|
|
|
279
255
|
#emit(file) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which source bytes each segment of an output reads, as the download
|
|
3
|
+
* map's pass worked them out (torrent-tv/meta#178).
|
|
4
|
+
*
|
|
5
|
+
* A fact of the file and the output, kept apart from what the torrent is told:
|
|
6
|
+
* the far part of a film is stated to the torrent as windows of a band
|
|
7
|
+
* (`download-bands.js`), and the forecast still has to find every segment's own
|
|
8
|
+
* bytes (`ViewerRequests` `#segmentsOf`). It used to read them off the zones
|
|
9
|
+
* sent, which named every segment.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class SegmentInputTable {
|
|
13
|
+
/** `sourceKey` → output → segment → file → its ranges. @type {Map<string, Map<string, Map<number, Map<number, { joined: Array<[number, number]>, ranges: Array<{ start: number, end: number }> }>>>>} */
|
|
14
|
+
#bySource = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} sourceKey
|
|
18
|
+
* @param {number} fileIndex
|
|
19
|
+
* @param {string} outputKey
|
|
20
|
+
* @param {number} index
|
|
21
|
+
* @param {Array<[number, number]>} joined - Inclusive ranges.
|
|
22
|
+
*/
|
|
23
|
+
set(sourceKey, fileIndex, outputKey, index, joined) {
|
|
24
|
+
let outputs = this.#bySource.get(sourceKey);
|
|
25
|
+
if (!outputs) this.#bySource.set(sourceKey, outputs = new Map());
|
|
26
|
+
let segments = outputs.get(outputKey);
|
|
27
|
+
if (!segments) outputs.set(outputKey, segments = new Map());
|
|
28
|
+
let files = segments.get(index);
|
|
29
|
+
if (!files) segments.set(index, files = new Map());
|
|
30
|
+
// The same reading keeps its list: whoever compares inputs compares them by it.
|
|
31
|
+
if (files.get(fileIndex)?.joined === joined) return;
|
|
32
|
+
files.set(fileIndex, { joined, ranges: joined.map(([start, end]) => ({ start, end: end + 1 })) });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The bytes one segment reads, by file, or null where none are known.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} sourceKey
|
|
39
|
+
* @param {string} outputKey
|
|
40
|
+
* @param {number} index
|
|
41
|
+
* @returns {Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null}
|
|
42
|
+
*/
|
|
43
|
+
inputsFor(sourceKey, outputKey, index) {
|
|
44
|
+
const files = this.#bySource.get(sourceKey)?.get(outputKey)?.get(index);
|
|
45
|
+
if (!files?.size) return null;
|
|
46
|
+
return [...files].map(([fileIndex, { ranges }]) => ({ sourceId: `${sourceKey}:${fileIndex}`, ranges }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} sourceKey */
|
|
50
|
+
forget(sourceKey) {
|
|
51
|
+
this.#bySource.delete(sourceKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -19,22 +19,83 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @returns {number}
|
|
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.
|
|
28
27
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
36
86
|
}
|
|
37
|
-
|
|
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;
|
|
38
99
|
}
|
|
39
100
|
|
|
40
101
|
/**
|
|
@@ -48,39 +109,56 @@ function coveredBytes(ranges) {
|
|
|
48
109
|
* @returns {Zone[]} In their order; raised zones are copies.
|
|
49
110
|
*/
|
|
50
111
|
export function expandToTargetInputs(zones, targetBytesOf) {
|
|
51
|
-
|
|
52
|
-
|
|
112
|
+
// ONE WALK FOR EACH OUTPUT'S FIRST SEGMENT, and the segments after it only
|
|
113
|
+
// as far as the run's input reaches. The map names every segment of a film,
|
|
114
|
+
// and each pass used to group all of them and work out each one's highest
|
|
115
|
+
// priority before looking at the few the run copies: 26 s of the main thread
|
|
116
|
+
// in fifteen minutes of a two-hour AVI on Home Assistant (r24,
|
|
117
|
+
// torrent-tv/meta#178).
|
|
118
|
+
/** @type {Map<string, { index: number, priority: number, zone: Zone }>} */
|
|
119
|
+
const first = new Map();
|
|
53
120
|
for (const zone of zones) {
|
|
54
|
-
|
|
55
|
-
if (!
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
else segments.set(zone.index, [zone]);
|
|
121
|
+
const best = first.get(zone.outputKey);
|
|
122
|
+
if (!best || zone.priority > best.priority || (zone.priority === best.priority && zone.index < best.index)) {
|
|
123
|
+
first.set(zone.outputKey, { index: zone.index, priority: zone.priority, zone });
|
|
124
|
+
}
|
|
59
125
|
}
|
|
60
|
-
/** @type {Map<Zone, Zone>} */
|
|
61
|
-
const
|
|
62
|
-
for (const [outputKey,
|
|
126
|
+
/** @type {Map<string, { first: { index: number, priority: number, zone: Zone }, target: number, after: Map<number, Zone[]> }>} */
|
|
127
|
+
const runs = new Map();
|
|
128
|
+
for (const [outputKey, best] of first) {
|
|
63
129
|
const target = targetBytesOf(outputKey);
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
if (target > 0) runs.set(outputKey, { first: best, target, after: new Map() });
|
|
131
|
+
}
|
|
132
|
+
if (runs.size === 0) return zones;
|
|
133
|
+
/** @type {Map<string, Array<[number, number]>>} */
|
|
134
|
+
const held = new Map();
|
|
135
|
+
for (const zone of zones) {
|
|
136
|
+
const run = runs.get(zone.outputKey);
|
|
137
|
+
if (!run) continue;
|
|
138
|
+
if (zone.index === run.first.index) {
|
|
139
|
+
let ranges = held.get(zone.outputKey);
|
|
140
|
+
if (!ranges) held.set(zone.outputKey, ranges = []);
|
|
141
|
+
ranges.push([zone.byteStart, zone.byteEnd]);
|
|
142
|
+
} else if (zone.index > run.first.index) {
|
|
143
|
+
const list = run.after.get(zone.index);
|
|
144
|
+
if (list) list.push(zone);
|
|
145
|
+
else run.after.set(zone.index, [zone]);
|
|
71
146
|
}
|
|
72
|
-
|
|
73
|
-
|
|
147
|
+
}
|
|
148
|
+
/** @type {Map<Zone, Zone>} */
|
|
149
|
+
const raised = new Map();
|
|
150
|
+
for (const [outputKey, run] of runs) {
|
|
151
|
+
const holding = new HeldBytes(held.get(outputKey) ?? []);
|
|
74
152
|
// The run's input is a stretch of consecutive segments: one already made,
|
|
75
153
|
// or not yet worked out, ends it.
|
|
76
|
-
for (let index = first.index + 1;
|
|
77
|
-
const list =
|
|
78
|
-
const ranges =
|
|
79
|
-
if (
|
|
80
|
-
|
|
154
|
+
for (let index = run.first.index + 1; run.after.has(index); index++) {
|
|
155
|
+
const list = run.after.get(index);
|
|
156
|
+
const ranges = list.map(zone => [zone.byteStart, zone.byteEnd]);
|
|
157
|
+
if (holding.total + holding.adds(ranges) > run.target) break;
|
|
158
|
+
holding.add(ranges);
|
|
81
159
|
for (const zone of list) {
|
|
82
|
-
raised.set(zone, { ...zone, priority: first.priority, urgent: first.zone.urgent,
|
|
83
|
-
behind: first.zone.behind, deferred: first.zone.deferred });
|
|
160
|
+
raised.set(zone, { ...zone, priority: run.first.priority, urgent: run.first.zone.urgent,
|
|
161
|
+
behind: run.first.zone.behind, deferred: run.first.zone.deferred });
|
|
84
162
|
}
|
|
85
163
|
}
|
|
86
164
|
}
|