@torrent-tv/proxy 2.98.7 → 2.98.9
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/services/encode/EncodeRuns.js +20 -1
- package/services/encode/SpeedReadings.js +45 -0
- package/services/server/ViewerRequests.js +45 -11
- package/services/server/wire-outputs.js +1 -1
- package/services/viewer/playback-readiness.js +89 -13
- package/services/viewer/segment-input-table.js +14 -9
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { contentionPenalty } from "./contention.js";
|
|
13
|
+
import { LOOKAHEAD_PAUSE_SECONDS } from "./CushionReport.js";
|
|
14
|
+
import { SpeedReadings } from "./SpeedReadings.js";
|
|
13
15
|
import { ENCODE_RUN_STATE, liveRunsOf, runStateOf, wireState } from "./encode-run-state.js";
|
|
14
16
|
import { ENCODE_EXIT } from "./encode-exit.js";
|
|
15
17
|
import { EncodeRun, endOfRun } from "./EncodeRun.js";
|
|
@@ -171,6 +173,8 @@ export class EncodeRuns {
|
|
|
171
173
|
* @type {WeakMap<object, { cpuSeconds: number, filmSeconds: number, reading: boolean, beside: number }>}
|
|
172
174
|
*/
|
|
173
175
|
#cpuOfRun = new WeakMap();
|
|
176
|
+
/** Every speed reading of each output's runs, for the start forecast (`SpeedReadings.js`). */
|
|
177
|
+
#speedReadings = new SpeedReadings(LOOKAHEAD_PAUSE_SECONDS);
|
|
174
178
|
|
|
175
179
|
#lastInputToken = 0;
|
|
176
180
|
#inputFailures = new InputFailures();
|
|
@@ -492,6 +496,18 @@ export class EncodeRuns {
|
|
|
492
496
|
}
|
|
493
497
|
}
|
|
494
498
|
|
|
499
|
+
/**
|
|
500
|
+
* The speed readings of this output's runs measured after `after`, oldest
|
|
501
|
+
* first, within the forecast's horizon.
|
|
502
|
+
*
|
|
503
|
+
* @param {object} output
|
|
504
|
+
* @param {number} [after] - The time of the last reading the asker has.
|
|
505
|
+
* @returns {Array<{ speed: number, at: number }>}
|
|
506
|
+
*/
|
|
507
|
+
speedReadingsOf(output, after) {
|
|
508
|
+
return this.#speedReadings.after(output, after);
|
|
509
|
+
}
|
|
510
|
+
|
|
495
511
|
/**
|
|
496
512
|
* The media intervals of a stored piece of this output, or undefined.
|
|
497
513
|
*
|
|
@@ -1179,7 +1195,10 @@ export class EncodeRuns {
|
|
|
1179
1195
|
// group before it in hand — the leading pictures the run that starts
|
|
1180
1196
|
// there cannot make.
|
|
1181
1197
|
onTail: (name) => this.#keepLeadingPictures(session, admittedInput, name),
|
|
1182
|
-
onSpeedMeasured: () =>
|
|
1198
|
+
onSpeedMeasured: (reading) => {
|
|
1199
|
+
this.#speedReadings.note(session, reading);
|
|
1200
|
+
this.#host.noteRunSpeedMeasured?.(session, run);
|
|
1201
|
+
},
|
|
1183
1202
|
onEnded: (ended) => {
|
|
1184
1203
|
this.#runsByInputToken.delete(inputToken);
|
|
1185
1204
|
if (admittedInput.original) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The speed readings of each output's runs, in the order the runs
|
|
3
|
+
* measured them, over the start forecast's horizon (torrent-tv/meta#178).
|
|
4
|
+
*
|
|
5
|
+
* A run announces a reading at each closed piece; whoever forecasts takes every
|
|
6
|
+
* reading after the last one it has. The forecast used to take only the newest
|
|
7
|
+
* reading of a run alive when it asked: on a cold start on Home Assistant the
|
|
8
|
+
* runs were three pieces long and ended between its polls, and of the readings
|
|
9
|
+
* 0.78, 1.80, 2.25, 2.84 and 2.69x it got 0.78 and 2.69x, and held the picture
|
|
10
|
+
* on 0.78x for 13 s (r37).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class SpeedReadings {
|
|
14
|
+
/** @type {WeakMap<object, Array<{ speed: number, at: number }>>} */
|
|
15
|
+
#byOutput = new WeakMap();
|
|
16
|
+
#keepMs;
|
|
17
|
+
|
|
18
|
+
/** @param {number} keepSeconds - How far back a reading is kept: the forecast's horizon. */
|
|
19
|
+
constructor(keepSeconds) {
|
|
20
|
+
this.#keepMs = Math.max(0, keepSeconds) * 1000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} output
|
|
25
|
+
* @param {{ speed: number, at: number } | null} reading
|
|
26
|
+
*/
|
|
27
|
+
note(output, reading) {
|
|
28
|
+
if (!output || !(reading?.speed > 0) || !Number.isFinite(reading.at)) return;
|
|
29
|
+
let readings = this.#byOutput.get(output);
|
|
30
|
+
if (!readings) this.#byOutput.set(output, readings = []);
|
|
31
|
+
readings.push({ speed: reading.speed, at: reading.at });
|
|
32
|
+
const cutoff = reading.at - this.#keepMs;
|
|
33
|
+
// As the forecast's own trend keeps them: the newest one before the horizon stays.
|
|
34
|
+
while (readings.length > 1 && readings[1].at <= cutoff) readings.shift();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} output
|
|
39
|
+
* @param {number} [after] - The time of the last reading the asker has.
|
|
40
|
+
* @returns {Array<{ speed: number, at: number }>} Oldest first.
|
|
41
|
+
*/
|
|
42
|
+
after(output, after = Number.NEGATIVE_INFINITY) {
|
|
43
|
+
return (this.#byOutput.get(output) ?? []).filter(reading => reading.at > after);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -60,11 +60,38 @@ export class ViewerRequests {
|
|
|
60
60
|
#host;
|
|
61
61
|
/** @type {WeakMap<object, Map<string, object>>} */
|
|
62
62
|
#rateStateBySession = new WeakMap();
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* What the start forecasts cost, said once a minute: how many there were,
|
|
65
|
+
* their time on the main thread, and for each track whether the times of its
|
|
66
|
+
* pieces were kept from the last poll or why they were worked out again. In
|
|
67
|
+
* r37 the forecast took 19.2 s of Home Assistant's main thread in 15 minutes
|
|
68
|
+
* and the times were mostly worked out again (torrent-tv/meta#178).
|
|
69
|
+
*/
|
|
70
|
+
#forecastCosts = { since: Date.now(), polls: 0, ms: 0, maxMs: 0, times: new Map() };
|
|
71
|
+
|
|
72
|
+
#noteForecast(ms, reasons) {
|
|
73
|
+
const costs = this.#forecastCosts;
|
|
74
|
+
costs.polls += 1; costs.ms += ms; costs.maxMs = Math.max(costs.maxMs, ms);
|
|
75
|
+
for (const reason of reasons) costs.times.set(reason, (costs.times.get(reason) ?? 0) + 1);
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
if (now - costs.since < 60_000) return;
|
|
78
|
+
logger.info(`start forecasts in ${((now - costs.since) / 1000).toFixed(1)}s: ${costs.polls}, ` +
|
|
79
|
+
`${Math.round(costs.ms)}ms in all, ${(costs.ms / costs.polls).toFixed(1)}ms each, longest ${Math.round(costs.maxMs)}ms; ` +
|
|
80
|
+
`times of a track's pieces: ${[...costs.times].map(([reason, count]) => `${reason} ${count}`).join(", ")}`);
|
|
81
|
+
Object.assign(costs, { since: now, polls: 0, ms: 0, maxMs: 0, times: new Map() });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The pieces of each output as the start forecast read them last (see
|
|
86
|
+
* `#segmentsOf`), and what the forecast keeps of their time geometry while
|
|
87
|
+
* they stand (`playback-readiness.js` `timescaleOfPieces`).
|
|
88
|
+
*
|
|
89
|
+
* @type {WeakMap<object, { pieces: object[], geometry: object }>}
|
|
90
|
+
*/
|
|
64
91
|
#forecastPieces = new WeakMap();
|
|
65
92
|
|
|
66
93
|
/**
|
|
67
|
-
* @param {object} host - `opening`, `activeOutputFor`, `viewerSecondsOn`, `minimumBufferSecondsFor`, `getSourceStats`, `disposeSession`, `planEncodersSoon`, `waitUntilReady`, `encodeRuns`, `
|
|
94
|
+
* @param {object} host - `opening`, `activeOutputFor`, `viewerSecondsOn`, `minimumBufferSecondsFor`, `getSourceStats`, `disposeSession`, `planEncodersSoon`, `waitUntilReady`, `encodeRuns`, `encodeSpeedReadingsOf`, `outputTimes`, `outputs`, `lookaheadSeconds`, `quality`, `qualityOffer`, `renditions`, `segmentDurationSec`, `segmentStore`, `sourceFiles`, `startupWaitMs`, `viewers`, `acceptsGeneration`, `generationOfRequest`, `noteGivenOutput`, `holdForResponse`, `noteServingVerdict`, `servingVerdictOf`
|
|
68
95
|
*/
|
|
69
96
|
constructor(host) {
|
|
70
97
|
this.#host = host;
|
|
@@ -658,6 +685,7 @@ export class ViewerRequests {
|
|
|
658
685
|
this.#host.initialOutputBitsPerMediaSecondOf?.(output) ?? 0,
|
|
659
686
|
readings: trackRates,
|
|
660
687
|
segments,
|
|
688
|
+
geometry: this.#forecastPieces.get(output)?.geometry,
|
|
661
689
|
readySegmentIndices: [...inventory.keys()],
|
|
662
690
|
segmentSizesBytes: inventory
|
|
663
691
|
});
|
|
@@ -710,7 +738,10 @@ export class ViewerRequests {
|
|
|
710
738
|
// fetched whole by the page before it is chosen, so it never matches here.
|
|
711
739
|
const subtitle = viewer?.subtitle?.sourceKey === session.file.sourceKey &&
|
|
712
740
|
viewer.subtitle.fileIndex === session.file.fileIndex ? viewer.subtitle : null;
|
|
713
|
-
const
|
|
741
|
+
const startedAt = performance.now();
|
|
742
|
+
const predicted = this.#host.playbackReadiness.predict(input);
|
|
743
|
+
this.#noteForecast(performance.now() - startedAt, tracks.map(track => track.geometry?.reason ?? "none"));
|
|
744
|
+
const forecast = withSubtitleReadiness(predicted, subtitle,
|
|
714
745
|
subtitle ? this.#host.subtitleReadyFor?.(viewer) !== false : true);
|
|
715
746
|
forecast.operations = outputs.flatMap((output, index) => {
|
|
716
747
|
const common = { speed: this.#host.playbackReadiness.forecastRate(tracks[index].readings, now, 0, "piece"),
|
|
@@ -808,11 +839,12 @@ export class ViewerRequests {
|
|
|
808
839
|
#segmentsOf(output, sessionInit, inventory, positionSeconds) {
|
|
809
840
|
const timeline = output.timeline;
|
|
810
841
|
const count = timeline?.segmentCount ?? 0;
|
|
811
|
-
let
|
|
812
|
-
if (!
|
|
813
|
-
|
|
814
|
-
this.#forecastPieces.set(output,
|
|
842
|
+
let forecast = this.#forecastPieces.get(output);
|
|
843
|
+
if (!forecast) {
|
|
844
|
+
forecast = { pieces: [], geometry: {} };
|
|
845
|
+
this.#forecastPieces.set(output, forecast);
|
|
815
846
|
}
|
|
847
|
+
const kept = forecast.pieces;
|
|
816
848
|
const segments = [];
|
|
817
849
|
for (let index = 0; index < count; index++) {
|
|
818
850
|
const endSeconds = timeline.publishedStartOf(index + 1);
|
|
@@ -861,10 +893,12 @@ export class ViewerRequests {
|
|
|
861
893
|
trend = new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds);
|
|
862
894
|
state.tracks.set(id, trend);
|
|
863
895
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
896
|
+
// Every reading since this trend's last, not the newest one at the poll:
|
|
897
|
+
// a run that ended between two polls took its readings with it.
|
|
898
|
+
for (const reading of this.#host.encodeSpeedReadingsOf?.(output, trend.lastAt) ?? []) {
|
|
899
|
+
if (reading.at <= now) trend.add(reading.at, reading.speed);
|
|
900
|
+
}
|
|
901
|
+
if (!trend.snapshot()) {
|
|
868
902
|
// ONLY UNTIL A PIECE IS MEASURED does the prediction stand for the
|
|
869
903
|
// encoder, and it is not kept among the readings. Added on every poll
|
|
870
904
|
// between runs, it put the machine's share of that moment — 1 % at a
|
|
@@ -583,7 +583,7 @@ export function wireOutputs({
|
|
|
583
583
|
// source preparation's answer, set on these parts by the assembly.
|
|
584
584
|
subtitleReadyFor: (viewer) => parts.subtitleReadyFor?.(viewer) ?? true,
|
|
585
585
|
sourceInputsFor: (output, index) => parts.sourceInputsFor?.(output, index),
|
|
586
|
-
|
|
586
|
+
encodeSpeedReadingsOf: (output, after) => parts.encodeRuns.speedReadingsOf(output, after),
|
|
587
587
|
projectedEncodeSpeedOf: (output) => parts.encodeCost.projectedSpeedOf(output),
|
|
588
588
|
// The speed the offer predicted for this output when it chose its mode: a
|
|
589
589
|
// forecast with no measured piece yet stands on it rather than on the
|
|
@@ -84,6 +84,11 @@ export class RateTrend {
|
|
|
84
84
|
return true;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** The time of the newest reading kept, or -Infinity. */
|
|
88
|
+
get lastAt() {
|
|
89
|
+
return this.#readings.at(-1)?.at ?? Number.NEGATIVE_INFINITY;
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
snapshot() {
|
|
88
93
|
if (this.#readings.length === 0) {
|
|
89
94
|
return null;
|
|
@@ -139,21 +144,16 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
139
144
|
range.end > range.start).map((range) => ({ start: fromSeconds(range.start), end: fromSeconds(range.end) }))
|
|
140
145
|
: null);
|
|
141
146
|
// One timescale in which every time of this forecast is a whole number of
|
|
142
|
-
// ticks, so that adding and comparing never seek a common denominator.
|
|
147
|
+
// ticks, so that adding and comparing never seek a common denominator. A
|
|
148
|
+
// track's pieces give it the same timescale while they are the same pieces.
|
|
143
149
|
const unit = commonTimescale([
|
|
144
150
|
fromSeconds(finiteNonNegative(input.positionSeconds)), fromSeconds(duration),
|
|
145
151
|
fromSeconds(finiteNonNegative(input.bufferedAheadSeconds)), REPORTED_TIME_ERROR, ...clocks,
|
|
146
152
|
...reported.flatMap((ranges) => (ranges ?? []).flatMap(({ start, end }) => [start, end])),
|
|
147
|
-
...tracks.
|
|
148
|
-
...(segment.mediaRanges?.timescale ? [mediaTime(0n, segment.mediaRanges.timescale)] : []),
|
|
149
|
-
...exactBoundsOf(segment)
|
|
150
|
-
]))
|
|
153
|
+
...tracks.map((track) => mediaTime(0n, timescaleOfPieces(track)))
|
|
151
154
|
]);
|
|
152
155
|
const zero = mediaTime(0n, unit);
|
|
153
|
-
const segmentsOf = tracks.map((track, trackIndex) =>
|
|
154
|
-
(Array.isArray(track.segments) ? track.segments : []).map((segment) => ({
|
|
155
|
-
...segment, exact: exactTimesOf(segment), ranges: playerRanges(segment.mediaRanges, rescale(clocks[trackIndex], unit), unit)
|
|
156
|
-
})), unit));
|
|
156
|
+
const segmentsOf = tracks.map((track, trackIndex) => placedPieces(track, rescale(clocks[trackIndex], unit), unit));
|
|
157
157
|
const origin = latest(zero, ...segmentsOf.map((segments) =>
|
|
158
158
|
segments.find(({ index }) => index === 0)?.ranges?.[0]?.start ?? zero));
|
|
159
159
|
const at = latest(origin, rescale(fromSeconds(finiteNonNegative(input.positionSeconds)), unit));
|
|
@@ -645,10 +645,86 @@ function timesOf(segment) {
|
|
|
645
645
|
return segment.exact ?? exactTimesOf(segment);
|
|
646
646
|
}
|
|
647
647
|
|
|
648
|
-
/**
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
648
|
+
/**
|
|
649
|
+
* WHAT A TRACK'S PIECES GIVE THE FORECAST'S TIME, KEPT BETWEEN POLLS while
|
|
650
|
+
* they are the same pieces. The forecast runs on every poll of a waiting
|
|
651
|
+
* viewer, and gathering the edges of every remaining piece for the timescale,
|
|
652
|
+
* then placing each on the page's clock, was about 3 s of the proxy's main
|
|
653
|
+
* thread in r35's first 15 minutes on Home Assistant (torrent-tv/meta#178).
|
|
654
|
+
* `track.geometry` is kept by whoever keeps the pieces; without it nothing is
|
|
655
|
+
* kept, and the answer is the same either way: a placement is reused only for
|
|
656
|
+
* pieces with the same edges and read ranges, the same timescale and the same
|
|
657
|
+
* clock offset, so no time is rounded for the reuse. Only the times are kept: a
|
|
658
|
+
* piece whose torrent inputs were told again is a new object with the same
|
|
659
|
+
* times, and it is read with its inputs of this poll. In r37 most polls placed
|
|
660
|
+
* every piece again, the MPEG-TS output having no read ranges to keep them by.
|
|
661
|
+
*
|
|
662
|
+
* @param {{ segments?: object[], geometry?: object }} track
|
|
663
|
+
* @returns {bigint} The least timescale holding every edge and every served tick of its pieces.
|
|
664
|
+
*/
|
|
665
|
+
function timescaleOfPieces(track) {
|
|
666
|
+
const segments = Array.isArray(track.segments) ? track.segments : [];
|
|
667
|
+
const geometry = track.geometry;
|
|
668
|
+
const changed = geometry ? piecesChanged(geometry.pieces, segments) : null;
|
|
669
|
+
if (geometry && changed === null) return geometry.timescale;
|
|
670
|
+
if (geometry) geometry.reason = changed;
|
|
671
|
+
const distinct = new Set();
|
|
672
|
+
for (const segment of segments) {
|
|
673
|
+
if (segment.mediaRanges?.timescale) distinct.add(segment.mediaRanges.timescale);
|
|
674
|
+
const { start, end } = exactTimesOf(segment);
|
|
675
|
+
if (start) distinct.add(start.timescale);
|
|
676
|
+
if (end) distinct.add(end.timescale);
|
|
677
|
+
}
|
|
678
|
+
const timescale = commonTimescale([...distinct].map((value) => mediaTime(0n, value)));
|
|
679
|
+
if (geometry) {
|
|
680
|
+
geometry.pieces = segments.map((segment) => ({ index: segment.index, startSeconds: segment.startSeconds,
|
|
681
|
+
endSeconds: segment.endSeconds, mediaRanges: segment.mediaRanges }));
|
|
682
|
+
geometry.timescale = timescale;
|
|
683
|
+
geometry.placed = null;
|
|
684
|
+
}
|
|
685
|
+
return timescale;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* A track's pieces on the page's clock in `unit`: read pieces at their served
|
|
690
|
+
* ranges, the others placed between them. The times are reused while the
|
|
691
|
+
* pieces are those {@link timescaleOfPieces} last took and `unit` and `clock`
|
|
692
|
+
* are the same; everything else of a piece is this poll's.
|
|
693
|
+
*
|
|
694
|
+
* @param {{ segments?: object[], geometry?: object }} track
|
|
695
|
+
* @param {import("./media-time.js").MediaTime} clock - In `unit`.
|
|
696
|
+
* @param {bigint} unit
|
|
697
|
+
*/
|
|
698
|
+
function placedPieces(track, clock, unit) {
|
|
699
|
+
const geometry = track.geometry;
|
|
700
|
+
const segments = Array.isArray(track.segments) ? track.segments : [];
|
|
701
|
+
let times = geometry?.placed && geometry.placedUnit === unit && geometry.placedClock === clock.ticks ? geometry.placed : null;
|
|
702
|
+
if (geometry) geometry.reason = times ? "kept" : !geometry.placed ? geometry.reason ?? "first" : geometry.placedUnit !== unit ? "timescale" : "clock";
|
|
703
|
+
if (!times) {
|
|
704
|
+
times = predictedMediaSegments(segments.map((segment) => ({
|
|
705
|
+
...segment, exact: exactTimesOf(segment), ranges: playerRanges(segment.mediaRanges, clock, unit)
|
|
706
|
+
})), unit).map(({ exact, ranges }) => ({ exact, ranges }));
|
|
707
|
+
if (geometry) Object.assign(geometry, { placed: times, placedUnit: unit, placedClock: clock.ticks });
|
|
708
|
+
}
|
|
709
|
+
return segments.map((segment, index) => ({ ...segment, exact: times[index].exact, ranges: times[index].ranges }));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* How `segments` differ from the pieces `kept` recorded, by the first field
|
|
714
|
+
* their times come from that differs, or null when they are the same pieces.
|
|
715
|
+
*
|
|
716
|
+
* @returns {"first" | "count" | "index" | "edges" | "read ranges" | null}
|
|
717
|
+
*/
|
|
718
|
+
function piecesChanged(kept, segments) {
|
|
719
|
+
if (!Array.isArray(kept)) return "first";
|
|
720
|
+
if (kept.length !== segments.length) return "count";
|
|
721
|
+
for (let index = 0; index < segments.length; index++) {
|
|
722
|
+
const segment = segments[index], was = kept[index];
|
|
723
|
+
if (segment.index !== was.index) return "index";
|
|
724
|
+
if (segment.startSeconds !== was.startSeconds || segment.endSeconds !== was.endSeconds) return "edges";
|
|
725
|
+
if (segment.mediaRanges !== was.mediaRanges) return "read ranges";
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
652
728
|
}
|
|
653
729
|
|
|
654
730
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export class SegmentInputTable {
|
|
13
|
-
/** `sourceKey` → output → segment →
|
|
13
|
+
/** `sourceKey` → output → segment → its files' ranges, and the list `inputsFor` answers while they stand. @type {Map<string, Map<string, Map<number, { files: Map<number, { joined: Array<[number, number]>, ranges: Array<{ start: number, end: number }> }>, inputs: Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null }>>>} */
|
|
14
14
|
#bySource = new Map();
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -25,15 +25,19 @@ export class SegmentInputTable {
|
|
|
25
25
|
if (!outputs) this.#bySource.set(sourceKey, outputs = new Map());
|
|
26
26
|
let segments = outputs.get(outputKey);
|
|
27
27
|
if (!segments) outputs.set(outputKey, segments = new Map());
|
|
28
|
-
let
|
|
29
|
-
if (!
|
|
28
|
+
let segment = segments.get(index);
|
|
29
|
+
if (!segment) segments.set(index, segment = { files: new Map(), inputs: null });
|
|
30
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 })) });
|
|
31
|
+
if (segment.files.get(fileIndex)?.joined === joined) return;
|
|
32
|
+
segment.files.set(fileIndex, { joined, ranges: joined.map(([start, end]) => ({ start, end: end + 1 })) });
|
|
33
|
+
segment.inputs = null;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* The bytes one segment reads, by file, or null where none are known.
|
|
37
|
+
* The bytes one segment reads, by file, or null where none are known. The
|
|
38
|
+
* same list while they stand: the start forecast asks for every segment of
|
|
39
|
+
* the film on each poll, and keeps what it derived from a segment whose
|
|
40
|
+
* inputs are the same list.
|
|
37
41
|
*
|
|
38
42
|
* @param {string} sourceKey
|
|
39
43
|
* @param {string} outputKey
|
|
@@ -41,9 +45,10 @@ export class SegmentInputTable {
|
|
|
41
45
|
* @returns {Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null}
|
|
42
46
|
*/
|
|
43
47
|
inputsFor(sourceKey, outputKey, index) {
|
|
44
|
-
const
|
|
45
|
-
if (!files
|
|
46
|
-
|
|
48
|
+
const segment = this.#bySource.get(sourceKey)?.get(outputKey)?.get(index);
|
|
49
|
+
if (!segment?.files.size) return null;
|
|
50
|
+
segment.inputs ??= [...segment.files].map(([fileIndex, { ranges }]) => ({ sourceId: `${sourceKey}:${fileIndex}`, ranges }));
|
|
51
|
+
return segment.inputs;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** @param {string} sourceKey */
|