@torrent-tv/proxy 2.97.30 → 2.98.0
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/bin/cli.js +2 -0
- package/package.json +1 -1
- package/server.js +15 -5
- package/services/encode/Renditions.js +17 -2
- package/services/encode/hwaccel.js +3 -2
- package/services/encode/quality/EncodeCost.js +27 -8
- package/services/encode/quality/output-format.js +2 -1
- package/services/server/wire-outputs.js +22 -1
- package/services/torrent/map-zones.js +91 -26
- package/services/viewer/target-input-expansion.js +12 -4
package/bin/cli.js
CHANGED
|
@@ -88,6 +88,7 @@ program
|
|
|
88
88
|
.option("--no-transcode-audio", "Disable optional HLS AAC audio transcoding")
|
|
89
89
|
.option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
|
|
90
90
|
.option("--delivery-sink", "Serve /api/delivery-sink, a torrent-free byte stream for delivery testing")
|
|
91
|
+
.option("--pin-preset <name>", "Encode every software picture at this libx264 preset only, for runs that must be compared at one quality (off by default)")
|
|
91
92
|
.option("--profile-start <seconds>", "Profile the main thread's CPU for this many seconds from the first source opened, written beside the state (off by default)")
|
|
92
93
|
.option(
|
|
93
94
|
"--send-chunk-bytes <bytes>",
|
|
@@ -394,6 +395,7 @@ try {
|
|
|
394
395
|
logFile: options.logFile ?? "",
|
|
395
396
|
deliverySink: options.deliverySink === true,
|
|
396
397
|
profileStartSeconds: Number(options.profileStart) || 0,
|
|
398
|
+
pinnedPreset: typeof options.pinPreset === "string" && options.pinPreset.trim() ? options.pinPreset.trim() : null,
|
|
397
399
|
// Late-bound the same way `webRtcManager` is below: the torrent pool is
|
|
398
400
|
// built inside `startProxyServer`, before `dataChannelHandler` — the
|
|
399
401
|
// thing that actually owns a channel to push down — exists.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -127,6 +127,9 @@ export async function startProxyServer({
|
|
|
127
127
|
deliverySink = false,
|
|
128
128
|
// Seconds of main-thread CPU profile from the first source opened; 0 is off.
|
|
129
129
|
profileStartSeconds = 0,
|
|
130
|
+
// The one software preset every picture is encoded at, for runs compared at
|
|
131
|
+
// one quality; null leaves the choice to the machine.
|
|
132
|
+
pinnedPreset = null,
|
|
130
133
|
// What viewers' connections carry beyond the film, per byte of film; the
|
|
131
134
|
// transport counts it and is built after this server, so it is late-bound.
|
|
132
135
|
serviceShare = () => null,
|
|
@@ -543,7 +546,7 @@ export async function startProxyServer({
|
|
|
543
546
|
// ranges closer than a piece hold the same pieces as one.
|
|
544
547
|
const pieceLength = Number(torrentPool.knownTorrent(map.sourceKey)?.pieceLength);
|
|
545
548
|
const passStartedAt = performance.now();
|
|
546
|
-
let
|
|
549
|
+
let rangeCount = 0;
|
|
547
550
|
const converted = [];
|
|
548
551
|
const demands = [];
|
|
549
552
|
// What each output's next run would copy, asked of the swarm as one group
|
|
@@ -629,12 +632,18 @@ export async function startProxyServer({
|
|
|
629
632
|
const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
|
|
630
633
|
const convert = ranges => {
|
|
631
634
|
const joined = joinedRangesOf(ranges, pieceLength);
|
|
632
|
-
|
|
635
|
+
rangeCount += joined.length;
|
|
633
636
|
// What the segment reads is kept for the forecast in a table of its
|
|
634
637
|
// own, whatever form the torrent is told it in.
|
|
635
638
|
segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
|
|
636
|
-
|
|
637
|
-
|
|
639
|
+
// ONE ZONE FOR THE SEGMENT, its demand stated once for all the
|
|
640
|
+
// ranges it reads. The demand — level, priority, deadline, the map
|
|
641
|
+
// zone it belongs to — is this pass's; the ranges are the list kept
|
|
642
|
+
// for the segment, unchanged from pass to pass. A zone for each
|
|
643
|
+
// range copied the demand into every one: ten thousand objects a
|
|
644
|
+
// pass over a two-hour AVI, made again 21-61 times a minute on Home
|
|
645
|
+
// Assistant (r26, torrent-tv/meta#178).
|
|
646
|
+
converted.push({ ...demand, downloadInterval: { from: zone.owner.from, to: zone.owner.to }, ranges: joined });
|
|
638
647
|
};
|
|
639
648
|
// The bytes one segment needs are a fact of the file and the output, so
|
|
640
649
|
// they are worked out once. A whole film is thousands of segments and
|
|
@@ -694,7 +703,7 @@ export async function startProxyServer({
|
|
|
694
703
|
convert(input.ranges);
|
|
695
704
|
}
|
|
696
705
|
const result = expanded();
|
|
697
|
-
notePass({ complete, segments
|
|
706
|
+
notePass({ complete, segments: converted.length, ranges: rangeCount, zones: result.length, ms: performance.now() - passStartedAt });
|
|
698
707
|
if (complete) resolvedPasses.set(fileKey, { ...pass, result });
|
|
699
708
|
else resolvedPasses.delete(fileKey);
|
|
700
709
|
return result;
|
|
@@ -792,6 +801,7 @@ export async function startProxyServer({
|
|
|
792
801
|
});
|
|
793
802
|
let sourcePreparation;
|
|
794
803
|
const outputParts = wireOutputs({
|
|
804
|
+
pinnedPreset,
|
|
795
805
|
indexMemory,
|
|
796
806
|
serviceShare,
|
|
797
807
|
readMetadataActivity: () => containerOrchestrator.activity(),
|
|
@@ -1358,8 +1358,23 @@ export class Renditions {
|
|
|
1358
1358
|
if (!base) {
|
|
1359
1359
|
return null;
|
|
1360
1360
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1361
|
+
const offered = this.#host.qualityOffer.offeredHeightsFor(base);
|
|
1362
|
+
if (!offered.includes(height)) {
|
|
1363
|
+
if (!this.#host.outputs.splicableHeights(base).includes(height)) {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
// A STEP THE MASTER LISTS AND THE OFFER NO LONGER HOLDS. The offer moves
|
|
1367
|
+
// with what the machine measures, and a page preparing a step it was
|
|
1368
|
+
// asked for can arrive after the ask was withdrawn: field r28, 360p
|
|
1369
|
+
// asked at 21:40:44.308, the offer down to 400p at 21:40:46.143, the
|
|
1370
|
+
// warm-up at 46.170 answered "no such variant" (torrent-tv/meta#178).
|
|
1371
|
+
// The step exists; it is not available now, and the page keeps what it
|
|
1372
|
+
// plays and follows the next ask.
|
|
1373
|
+
this.#host.logger.info(`transcode ${base.id} warming ${height}p for ${consumerId}: not offered now (offering ${offered.map(one => `${one}p`).join(" ") || "nothing"})`);
|
|
1374
|
+
const error = new Error(`${height}p is not offered now.`);
|
|
1375
|
+
error.code = OUTPUT_UNAVAILABLE;
|
|
1376
|
+
error.details = { reason: `${height}p is not offered on this machine now`, figures: { height, offeredHeights: offered } };
|
|
1377
|
+
throw error;
|
|
1363
1378
|
}
|
|
1364
1379
|
const index = this.#host.outputTimes.segmentIndexForTime(base, positionSeconds);
|
|
1365
1380
|
const variant = await this.resolveVariantSession(baseSessionId, height, index, consumerId);
|
|
@@ -1715,7 +1715,8 @@ export function buildResolutionLadder(ceilingWidth, ceilingHeight) {
|
|
|
1715
1715
|
* @param {{ width: number, height: number }} ceiling
|
|
1716
1716
|
* @param {number} outputFps
|
|
1717
1717
|
* @param {{ decodeModel?: object | null, source?: { megapixelsPerSecond: number, megabitsPerSecond: number } | null, requiredSpeed?: number | null }} [cost]
|
|
1718
|
-
* @returns {{ width: number, height: number, preset: string, ladder: Array<{ width: number, height: number }>, rungIndex: number } | null}
|
|
1718
|
+
* @returns {{ width: number, height: number, preset: string, ladder: Array<{ width: number, height: number }>, rungIndex: number, bar: number } | null}
|
|
1719
|
+
* `bar` is the speed the choice had to clear.
|
|
1719
1720
|
*/
|
|
1720
1721
|
export function chooseSoftwareEncodeSettings(benchmark, ceiling, outputFps, cost = {}) {
|
|
1721
1722
|
if (!Array.isArray(benchmark) || benchmark.length === 0) {
|
|
@@ -1750,7 +1751,7 @@ export function chooseSoftwareEncodeSettings(benchmark, ceiling, outputFps, cost
|
|
|
1750
1751
|
}
|
|
1751
1752
|
const chosen = ladder[chosenIndex];
|
|
1752
1753
|
const preset = pickSoftwarePreset(benchmark, chosen.width * chosen.height * fps, cost, chosen);
|
|
1753
|
-
return { width: chosen.width, height: chosen.height, preset, ladder, rungIndex: chosenIndex };
|
|
1754
|
+
return { width: chosen.width, height: chosen.height, preset, ladder, rungIndex: chosenIndex, bar };
|
|
1754
1755
|
}
|
|
1755
1756
|
|
|
1756
1757
|
/**
|
|
@@ -1103,7 +1103,8 @@ export class EncodeCost {
|
|
|
1103
1103
|
sourceHeight,
|
|
1104
1104
|
outputFps,
|
|
1105
1105
|
source = null,
|
|
1106
|
-
requiredSpeed = null
|
|
1106
|
+
requiredSpeed = null,
|
|
1107
|
+
observedDecodeCostSec = null
|
|
1107
1108
|
}) {
|
|
1108
1109
|
if (!transcodeVideo || this.#host().encoderKind !== "software" || !this.#host().benchmark) {
|
|
1109
1110
|
return null;
|
|
@@ -1113,26 +1114,44 @@ export class EncodeCost {
|
|
|
1113
1114
|
return null;
|
|
1114
1115
|
}
|
|
1115
1116
|
const concurrentCostSec = this.#committedMachineCostSec();
|
|
1117
|
+
// THE FILE'S OWN DECODE COST, where its encoder has already been read: the
|
|
1118
|
+
// opening had it and the choice priced decoding from the startup model
|
|
1119
|
+
// instead (review of r28, torrent-tv/meta#178).
|
|
1120
|
+
const decodeCostSec = Number.isFinite(observedDecodeCostSec) && observedDecodeCostSec > 0 ? observedDecodeCostSec : null;
|
|
1121
|
+
/** Every size and preset the choice priced, with the speed it was priced at. */
|
|
1122
|
+
const considered = [];
|
|
1116
1123
|
const speedForPreset = (benchmark, frame, outputPixelsPerSec) => {
|
|
1117
|
-
|
|
1118
|
-
return null;
|
|
1119
|
-
}
|
|
1120
|
-
return this.#speedOnThisMachine({
|
|
1124
|
+
const speed = concurrentCostSec === null ? null : this.#speedOnThisMachine({
|
|
1121
1125
|
benchmark,
|
|
1122
1126
|
source,
|
|
1123
1127
|
width: frame.width,
|
|
1124
1128
|
height: frame.height,
|
|
1125
1129
|
fps: outputPixelsPerSec / (frame.width * frame.height),
|
|
1126
|
-
observedDecodeCostSec:
|
|
1130
|
+
observedDecodeCostSec: decodeCostSec,
|
|
1127
1131
|
concurrentCostSec
|
|
1128
1132
|
});
|
|
1133
|
+
considered.push({ preset: benchmark[0]?.preset, width: frame.width, height: frame.height, speed });
|
|
1134
|
+
return speed;
|
|
1129
1135
|
};
|
|
1130
|
-
|
|
1136
|
+
const choice = chooseSoftwareEncodeSettings(
|
|
1131
1137
|
this.#host().benchmark,
|
|
1132
1138
|
{ width: ceiling.w, height: ceiling.h },
|
|
1133
1139
|
outputFps,
|
|
1134
|
-
{ decodeModel: this.#host().decodeModel, source, requiredSpeed, speedForPreset }
|
|
1140
|
+
{ decodeModel: this.#host().decodeModel, source, requiredSpeed, speedForPreset, observedDecodeCostSec: decodeCostSec }
|
|
1135
1141
|
);
|
|
1142
|
+
// WHAT THE CHOICE WAS MADE FROM, said once per choice: three runs of one
|
|
1143
|
+
// file on one host opened at three presets and nothing said why (r25, r26,
|
|
1144
|
+
// r28, torrent-tv/meta#178).
|
|
1145
|
+
if (choice) {
|
|
1146
|
+
const availability = this.#host().availability;
|
|
1147
|
+
const priced = considered.map(({ preset, width, height, speed }) =>
|
|
1148
|
+
`${width}x${height} ${preset}=${Number.isFinite(speed) ? `${speed.toFixed(2)}x` : "unpriced"}`).join(", ");
|
|
1149
|
+
logger.info(`encode choice: ${choice.width}x${choice.height}@${Number(outputFps).toFixed(3)} ${choice.preset} against ${choice.bar.toFixed(2)}x; ` +
|
|
1150
|
+
`priced ${priced}; decoding ${decodeCostSec === null ? "from the startup model" : `${decodeCostSec.toFixed(4)} s per second, measured on this file`}; ` +
|
|
1151
|
+
`machine ${availability?.known ? `${Math.round(availability.share * 100)}% to spare` : "share unknown"}; ` +
|
|
1152
|
+
`committed work ${concurrentCostSec === null ? "unmeasured" : `${concurrentCostSec.toFixed(3)} s per second`}`);
|
|
1153
|
+
}
|
|
1154
|
+
return choice;
|
|
1136
1155
|
}
|
|
1137
1156
|
}
|
|
1138
1157
|
|
|
@@ -144,7 +144,8 @@ export function decideOutputFormat({
|
|
|
144
144
|
sourceHeight: source.height,
|
|
145
145
|
outputFps: fps,
|
|
146
146
|
source: source.decode,
|
|
147
|
-
requiredSpeed: cost.requiredSpeed
|
|
147
|
+
requiredSpeed: cost.requiredSpeed,
|
|
148
|
+
observedDecodeCostSec: cost.observedDecodeCostSec ?? null
|
|
148
149
|
});
|
|
149
150
|
const width = budget?.width ?? ceiling?.w ?? 0;
|
|
150
151
|
const height = budget?.height ?? ceiling?.h ?? 0;
|
|
@@ -189,6 +189,9 @@ export function wireOutputs({
|
|
|
189
189
|
keyframeTables = new KeyframeTables(),
|
|
190
190
|
videoEncoder = null,
|
|
191
191
|
softwarePresetBenchmark = null,
|
|
192
|
+
// A software preset every picture is encoded at, for runs compared at one
|
|
193
|
+
// quality (`--pin-preset`); null leaves the choice to the machine.
|
|
194
|
+
pinnedPreset = null,
|
|
192
195
|
// What every encoder this host may use was qualified and measured to do at
|
|
193
196
|
// startup, by kind (`encode/calibration.js`). Takes the place of the one
|
|
194
197
|
// array above, which a test may still hand in on its own.
|
|
@@ -771,8 +774,26 @@ export function wireOutputs({
|
|
|
771
774
|
// Null where nothing was measured, and then nothing is re-encoded here.
|
|
772
775
|
parts.calibration = calibration ?? null;
|
|
773
776
|
const givenModes = Array.isArray(softwarePresetBenchmark) ? softwarePresetBenchmark : null;
|
|
777
|
+
// ONE PRESET FOR RUNS COMPARED AT ONE QUALITY. Three runs of one film on one
|
|
778
|
+
// host opened at `faster`, `veryfast` and `fast`, so no change could be
|
|
779
|
+
// compared at the same picture (r25, r26, r28, torrent-tv/meta#178). Pinned
|
|
780
|
+
// here, every decision that prices or chooses a software mode — the choice,
|
|
781
|
+
// the offer, the machine's admission and the command — sees the one mode,
|
|
782
|
+
// measured as calibrated. A preset calibration did not qualify is not
|
|
783
|
+
// pinned: the machine's own list stands, and the log says so.
|
|
784
|
+
let pinWarned = false;
|
|
785
|
+
const pinned = (modes) => {
|
|
786
|
+
if (!pinnedPreset || !Array.isArray(modes) || parts.encoders.current?.kind !== "software") return modes;
|
|
787
|
+
const kept = modes.filter(mode => mode.preset === pinnedPreset);
|
|
788
|
+
if (kept.length > 0) return kept;
|
|
789
|
+
if (!pinWarned) {
|
|
790
|
+
pinWarned = true;
|
|
791
|
+
logger.warn(`encode: --pin-preset ${pinnedPreset} names no calibrated mode (${modes.map(mode => mode.preset).join(", ")}); not pinned`);
|
|
792
|
+
}
|
|
793
|
+
return modes;
|
|
794
|
+
};
|
|
774
795
|
Object.defineProperty(parts, "softwarePresetBenchmark", {
|
|
775
|
-
get: () => (parts.calibration
|
|
796
|
+
get: () => pinned(parts.calibration
|
|
776
797
|
? parts.calibration.modesFor(parts.encoders.current?.kind)
|
|
777
798
|
: givenModes),
|
|
778
799
|
enumerable: true
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|