@torrent-tv/proxy 2.97.29 → 2.97.31
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 rangeCount = 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,8 +628,19 @@ 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
|
-
|
|
592
|
-
|
|
631
|
+
const joined = joinedRangesOf(ranges, pieceLength);
|
|
632
|
+
rangeCount += joined.length;
|
|
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
|
+
// ONE ZONE FOR THE SEGMENT, its demand stated once for all the
|
|
637
|
+
// ranges it reads. The demand — level, priority, deadline, the map
|
|
638
|
+
// zone it belongs to — is this pass's; the ranges are the list kept
|
|
639
|
+
// for the segment, unchanged from pass to pass. A zone for each
|
|
640
|
+
// range copied the demand into every one: ten thousand objects a
|
|
641
|
+
// pass over a two-hour AVI, made again 21-61 times a minute on Home
|
|
642
|
+
// Assistant (r26, torrent-tv/meta#178).
|
|
643
|
+
converted.push({ ...demand, downloadInterval: { from: zone.owner.from, to: zone.owner.to }, ranges: joined });
|
|
593
644
|
};
|
|
594
645
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
595
646
|
// they are worked out once. A whole film is thousands of segments and
|
|
@@ -649,6 +700,7 @@ export async function startProxyServer({
|
|
|
649
700
|
convert(input.ranges);
|
|
650
701
|
}
|
|
651
702
|
const result = expanded();
|
|
703
|
+
notePass({ complete, segments: converted.length, ranges: rangeCount, zones: result.length, ms: performance.now() - passStartedAt });
|
|
652
704
|
if (complete) resolvedPasses.set(fileKey, { ...pass, result });
|
|
653
705
|
else resolvedPasses.delete(fileKey);
|
|
654
706
|
return result;
|
|
@@ -751,7 +803,7 @@ export async function startProxyServer({
|
|
|
751
803
|
readMetadataActivity: () => containerOrchestrator.activity(),
|
|
752
804
|
onViewerChanged: () => { void sourcePreparation?.refresh(); onStateChanged(); },
|
|
753
805
|
onEncodersChanged: () => onStateChanged(),
|
|
754
|
-
sourceInputsFor: (output, index) =>
|
|
806
|
+
sourceInputsFor: (output, index) => segmentInputs.inputsFor(output.file.sourceKey, output.outputKey, index),
|
|
755
807
|
readSourceMedia: async (params) => {
|
|
756
808
|
await playbackPlanner.getPlan(params);
|
|
757
809
|
const media = playbackPlanner.getCachedMediaInfo(params);
|
|
@@ -63,6 +63,18 @@ export function deadlineOfMapZone(zone, now = Date.now()) {
|
|
|
63
63
|
Number.isFinite(zone.withinSeconds) ? now + Math.max(0, zone.withinSeconds) * 1000 : Number.POSITIVE_INFINITY;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The byte ranges a zone names: a segment's zone names all the ranges its
|
|
68
|
+
* segment reads, and states its demand once for them (`ranges`); any other
|
|
69
|
+
* zone names one (`byteStart`-`byteEnd`). Inclusive.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ ranges?: Array<[number, number]>, byteStart?: number, byteEnd?: number }} zone
|
|
72
|
+
* @returns {Array<[number, number]>}
|
|
73
|
+
*/
|
|
74
|
+
export function rangesOfZone(zone) {
|
|
75
|
+
return Array.isArray(zone.ranges) ? zone.ranges : [[zone.byteStart, zone.byteEnd]];
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
/**
|
|
67
79
|
* The zones with every repeat of the same bytes left out but the one the swarm
|
|
68
80
|
* would rank first.
|
|
@@ -80,14 +92,9 @@ export function deadlineOfMapZone(zone, now = Date.now()) {
|
|
|
80
92
|
* @returns {Zone[]} In their order, without the repeats.
|
|
81
93
|
*/
|
|
82
94
|
export function withoutRepeatedZones(zones, now = Date.now()) {
|
|
83
|
-
const
|
|
84
|
-
const best =
|
|
85
|
-
zones.
|
|
86
|
-
const kept = best.get(keys[index]);
|
|
87
|
-
const band = { urgency: levelOfMapZone(zone), priority: priorityOfMapZone(zone), deadlineAt: deadlineOfMapZone(zone, now) };
|
|
88
|
-
if (!kept || compareBands(band, kept.band) < 0) best.set(keys[index], { index, band });
|
|
89
|
-
});
|
|
90
|
-
return zones.filter((_zone, index) => best.get(keys[index]).index === index);
|
|
95
|
+
const bands = zones.map(zone => bandOf(zone, now));
|
|
96
|
+
const best = bestOfRepeats(zones.length, index => zones[index].byteStart, index => zones[index].byteEnd, index => bands[index]);
|
|
97
|
+
return zones.filter((_zone, index) => best.get(zones[index].byteStart).get(zones[index].byteEnd) === index);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
/**
|
|
@@ -105,45 +112,103 @@ export function withoutRepeatedZones(zones, now = Date.now()) {
|
|
|
105
112
|
* consecutive segments are consecutive bytes. A zone a metadata read states
|
|
106
113
|
* keeps its own request and order, and is left as it is.
|
|
107
114
|
*
|
|
108
|
-
*
|
|
109
|
-
* @
|
|
115
|
+
* A segment's zone states its demand once for all its ranges
|
|
116
|
+
* ({@link rangesOfZone}); each range is a part of its own here, so a repeat is
|
|
117
|
+
* found range by range, and the band and deadline every range is packed with
|
|
118
|
+
* are the zone's current ones.
|
|
119
|
+
*
|
|
120
|
+
* @param {Array<{ byteStart?: number, byteEnd?: number, ranges?: Array<[number, number]> }>} zones
|
|
110
121
|
* @param {number} [now]
|
|
111
122
|
* @returns {object[]}
|
|
112
123
|
*/
|
|
113
124
|
export function coalescedZones(zones, now = Date.now()) {
|
|
125
|
+
// Every range of every zone is a part, kept in numbers rather than objects:
|
|
126
|
+
// a film's map is ten thousand of them (r26, torrent-tv/meta#178).
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (const zone of zones) count += Array.isArray(zone.ranges) ? zone.ranges.length : 1;
|
|
129
|
+
const starts = new Float64Array(count);
|
|
130
|
+
const ends = new Float64Array(count);
|
|
131
|
+
const owners = new Int32Array(count);
|
|
132
|
+
const bands = zones.map(zone => bandOf(zone, now));
|
|
133
|
+
let at = 0;
|
|
134
|
+
zones.forEach((zone, index) => {
|
|
135
|
+
for (const [start, end] of rangesOfZone(zone)) {
|
|
136
|
+
starts[at] = start;
|
|
137
|
+
ends[at] = end;
|
|
138
|
+
owners[at] = index;
|
|
139
|
+
at += 1;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const best = bestOfRepeats(count, part => starts[part], part => ends[part], part => bands[owners[part]]);
|
|
114
143
|
const kept = [];
|
|
144
|
+
/** Band key → the band and its parts, in the order the bands first appear. */
|
|
115
145
|
const groups = new Map();
|
|
116
|
-
for (
|
|
146
|
+
for (let part = 0; part < count; part++) {
|
|
147
|
+
if (best.get(starts[part]).get(ends[part]) !== part) continue;
|
|
148
|
+
const zone = zones[owners[part]];
|
|
117
149
|
if (zone.requestId !== undefined || zone.downloadOnly || (Number.isSafeInteger(zone.order) && zone.order > 0)) {
|
|
118
150
|
kept.push(zone);
|
|
119
151
|
continue;
|
|
120
152
|
}
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const key = `${urgency}:${priority}`;
|
|
153
|
+
const band = bands[owners[part]];
|
|
154
|
+
const key = band.urgency * 1000 + band.priority;
|
|
124
155
|
let group = groups.get(key);
|
|
125
|
-
if (!group) groups.set(key, group = {
|
|
126
|
-
group.parts.push(
|
|
156
|
+
if (!group) groups.set(key, group = { band, parts: [] });
|
|
157
|
+
group.parts.push(part);
|
|
127
158
|
}
|
|
128
|
-
for (const { urgency, priority, parts } of groups.values()) {
|
|
129
|
-
parts.sort((left, right) => left
|
|
130
|
-
let
|
|
159
|
+
for (const { band: { urgency, priority }, parts } of groups.values()) {
|
|
160
|
+
parts.sort((left, right) => starts[left] - starts[right]);
|
|
161
|
+
let windowStart = 0;
|
|
162
|
+
let windowEnd = -1;
|
|
163
|
+
let windowDeadline = Number.POSITIVE_INFINITY;
|
|
164
|
+
let open = false;
|
|
131
165
|
const close = () => {
|
|
132
|
-
if (!
|
|
133
|
-
kept.push({ byteStart:
|
|
166
|
+
if (!open) return;
|
|
167
|
+
kept.push({ byteStart: windowStart, byteEnd: windowEnd, priority, urgent: urgency === Urgency.NEAR,
|
|
134
168
|
behind: urgency === Urgency.BEHIND, deferred: urgency === Urgency.TAIL,
|
|
135
|
-
...(Number.isFinite(
|
|
169
|
+
...(Number.isFinite(windowDeadline) ? { deadlineAt: windowDeadline } : {}) });
|
|
136
170
|
};
|
|
137
171
|
for (const part of parts) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
const deadlineAt = bands[owners[part]].deadlineAt;
|
|
173
|
+
if (open && starts[part] <= windowEnd + 1) {
|
|
174
|
+
windowEnd = Math.max(windowEnd, ends[part]);
|
|
175
|
+
windowDeadline = Math.min(windowDeadline, deadlineAt);
|
|
141
176
|
} else {
|
|
142
177
|
close();
|
|
143
|
-
|
|
178
|
+
open = true;
|
|
179
|
+
windowStart = starts[part];
|
|
180
|
+
windowEnd = ends[part];
|
|
181
|
+
windowDeadline = deadlineAt;
|
|
144
182
|
}
|
|
145
183
|
}
|
|
146
184
|
close();
|
|
147
185
|
}
|
|
148
186
|
return kept;
|
|
149
187
|
}
|
|
188
|
+
|
|
189
|
+
/** The band a zone's bytes are fetched in. */
|
|
190
|
+
function bandOf(zone, now) {
|
|
191
|
+
return { urgency: levelOfMapZone(zone), priority: priorityOfMapZone(zone), deadlineAt: deadlineOfMapZone(zone, now) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Which of the parts naming each exact byte range is the best: by band
|
|
196
|
+
* ({@link compareBands}), the first of equals. Kept by the range's two numbers
|
|
197
|
+
* rather than a string made of them: making a key of each of a film's ten
|
|
198
|
+
* thousand ranges was most of what packing its map cost (r26,
|
|
199
|
+
* torrent-tv/meta#178).
|
|
200
|
+
*
|
|
201
|
+
* @returns {Map<number, Map<number, number>>} start → end → the best part.
|
|
202
|
+
*/
|
|
203
|
+
function bestOfRepeats(count, startOf, endOf, bandOfPart) {
|
|
204
|
+
const best = new Map();
|
|
205
|
+
for (let part = 0; part < count; part++) {
|
|
206
|
+
const start = startOf(part);
|
|
207
|
+
const end = endOf(part);
|
|
208
|
+
let byEnd = best.get(start);
|
|
209
|
+
if (!byEnd) best.set(start, byEnd = new Map());
|
|
210
|
+
const kept = byEnd.get(end);
|
|
211
|
+
if (kept === undefined || compareBands(bandOfPart(part), bandOfPart(kept)) < 0) byEnd.set(end, part);
|
|
212
|
+
}
|
|
213
|
+
return best;
|
|
214
|
+
}
|
|
@@ -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 }) {
|
|
@@ -152,19 +150,6 @@ export class DownloadMaps {
|
|
|
152
150
|
return this.#files.get(`${sourceKey}:${fileIndex}`)?.sourceZones.some(zone => zone.from === from && zone.to === to) ?? false;
|
|
153
151
|
}
|
|
154
152
|
|
|
155
|
-
/** Exact source ranges already resolved by the download map, without new reads. */
|
|
156
|
-
inputsForOutput(sourceKey, outputKey, index) {
|
|
157
|
-
const sources = [];
|
|
158
|
-
for (const file of this.#files.values()) {
|
|
159
|
-
if (file.sourceKey !== sourceKey) continue;
|
|
160
|
-
const ranges = file.inputs.get(outputKey)?.get(index);
|
|
161
|
-
if (!ranges?.length) continue;
|
|
162
|
-
sources.push({ sourceId: `${sourceKey}:${file.fileIndex}`,
|
|
163
|
-
ranges });
|
|
164
|
-
}
|
|
165
|
-
return sources.length ? sources : null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
153
|
metadata({ sourceKey, fileIndex, statement, result, priority = 100, urgent = true, deadlineAt = 0, interval, leadSeconds = 0, scope = "playback", order = 0 }) {
|
|
169
154
|
if (!Number.isFinite(leadSeconds) || leadSeconds < 0) throw new TypeError("Metadata preparation time must be finite and nonnegative.");
|
|
170
155
|
const file = this.#file(sourceKey, fileIndex);
|
|
@@ -255,7 +240,7 @@ export class DownloadMaps {
|
|
|
255
240
|
const key = `${sourceKey}:${fileIndex}`;
|
|
256
241
|
let file = this.#files.get(key);
|
|
257
242
|
if (!file) {
|
|
258
|
-
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 };
|
|
259
244
|
this.#files.set(key, file);
|
|
260
245
|
}
|
|
261
246
|
return file;
|
|
@@ -265,24 +250,6 @@ export class DownloadMaps {
|
|
|
265
250
|
file.zones = zones;
|
|
266
251
|
// What `#emit` reads has changed, whether or not it is published now.
|
|
267
252
|
file.dirty = true;
|
|
268
|
-
// A pass that resolves what the pass before resolved hands back the same
|
|
269
|
-
// zones (`resolvedPasses` in server.js), and what they say of each segment
|
|
270
|
-
// is the same too.
|
|
271
|
-
const known = this.#inputsOf.get(zones);
|
|
272
|
-
if (known) {
|
|
273
|
-
file.inputs = known;
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
file.inputs = new Map();
|
|
277
|
-
this.#inputsOf.set(zones, file.inputs);
|
|
278
|
-
for (const zone of zones) {
|
|
279
|
-
if (typeof zone.outputKey !== "string" || !Number.isSafeInteger(zone.index)) continue;
|
|
280
|
-
let output = file.inputs.get(zone.outputKey);
|
|
281
|
-
if (!output) file.inputs.set(zone.outputKey, output = new Map());
|
|
282
|
-
let ranges = output.get(zone.index);
|
|
283
|
-
if (!ranges) output.set(zone.index, ranges = []);
|
|
284
|
-
ranges.push({ start: zone.byteStart, end: zone.byteEnd + 1 });
|
|
285
|
-
}
|
|
286
253
|
}
|
|
287
254
|
|
|
288
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
|
+
}
|
|
@@ -102,8 +102,11 @@ function merged(ranges) {
|
|
|
102
102
|
* The map's byte zones with each output's next run input raised to its first
|
|
103
103
|
* segment's place.
|
|
104
104
|
*
|
|
105
|
-
* @template {{ outputKey: string, index: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean,
|
|
106
|
-
*
|
|
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`).
|
|
107
110
|
* @param {(outputKey: string) => number | null} targetBytesOf - What the
|
|
108
111
|
* output's next run would be given to copy; nothing is raised without it.
|
|
109
112
|
* @returns {Zone[]} In their order; raised zones are copies.
|
|
@@ -138,7 +141,7 @@ export function expandToTargetInputs(zones, targetBytesOf) {
|
|
|
138
141
|
if (zone.index === run.first.index) {
|
|
139
142
|
let ranges = held.get(zone.outputKey);
|
|
140
143
|
if (!ranges) held.set(zone.outputKey, ranges = []);
|
|
141
|
-
ranges.push(
|
|
144
|
+
ranges.push(...rangesOf(zone));
|
|
142
145
|
} else if (zone.index > run.first.index) {
|
|
143
146
|
const list = run.after.get(zone.index);
|
|
144
147
|
if (list) list.push(zone);
|
|
@@ -153,7 +156,7 @@ export function expandToTargetInputs(zones, targetBytesOf) {
|
|
|
153
156
|
// or not yet worked out, ends it.
|
|
154
157
|
for (let index = run.first.index + 1; run.after.has(index); index++) {
|
|
155
158
|
const list = run.after.get(index);
|
|
156
|
-
const ranges = list.
|
|
159
|
+
const ranges = list.flatMap(rangesOf);
|
|
157
160
|
if (holding.total + holding.adds(ranges) > run.target) break;
|
|
158
161
|
holding.add(ranges);
|
|
159
162
|
for (const zone of list) {
|
|
@@ -164,3 +167,8 @@ export function expandToTargetInputs(zones, targetBytesOf) {
|
|
|
164
167
|
}
|
|
165
168
|
return raised.size === 0 ? zones : zones.map(zone => raised.get(zone) ?? zone);
|
|
166
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
|
+
}
|