@torrent-tv/proxy 2.98.8 → 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
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,6 +60,27 @@ export class ViewerRequests {
|
|
|
60
60
|
#host;
|
|
61
61
|
/** @type {WeakMap<object, Map<string, object>>} */
|
|
62
62
|
#rateStateBySession = new WeakMap();
|
|
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
|
+
|
|
63
84
|
/**
|
|
64
85
|
* The pieces of each output as the start forecast read them last (see
|
|
65
86
|
* `#segmentsOf`), and what the forecast keeps of their time geometry while
|
|
@@ -70,7 +91,7 @@ export class ViewerRequests {
|
|
|
70
91
|
#forecastPieces = new WeakMap();
|
|
71
92
|
|
|
72
93
|
/**
|
|
73
|
-
* @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`
|
|
74
95
|
*/
|
|
75
96
|
constructor(host) {
|
|
76
97
|
this.#host = host;
|
|
@@ -717,7 +738,10 @@ export class ViewerRequests {
|
|
|
717
738
|
// fetched whole by the page before it is chosen, so it never matches here.
|
|
718
739
|
const subtitle = viewer?.subtitle?.sourceKey === session.file.sourceKey &&
|
|
719
740
|
viewer.subtitle.fileIndex === session.file.fileIndex ? viewer.subtitle : null;
|
|
720
|
-
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,
|
|
721
745
|
subtitle ? this.#host.subtitleReadyFor?.(viewer) !== false : true);
|
|
722
746
|
forecast.operations = outputs.flatMap((output, index) => {
|
|
723
747
|
const common = { speed: this.#host.playbackReadiness.forecastRate(tracks[index].readings, now, 0, "piece"),
|
|
@@ -869,10 +893,12 @@ export class ViewerRequests {
|
|
|
869
893
|
trend = new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds);
|
|
870
894
|
state.tracks.set(id, trend);
|
|
871
895
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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()) {
|
|
876
902
|
// ONLY UNTIL A PIECE IS MEASURED does the prediction stand for the
|
|
877
903
|
// encoder, and it is not kept among the readings. Added on every poll
|
|
878
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;
|
|
@@ -648,8 +653,11 @@ function timesOf(segment) {
|
|
|
648
653
|
* thread in r35's first 15 minutes on Home Assistant (torrent-tv/meta#178).
|
|
649
654
|
* `track.geometry` is kept by whoever keeps the pieces; without it nothing is
|
|
650
655
|
* kept, and the answer is the same either way: a placement is reused only for
|
|
651
|
-
* the same
|
|
652
|
-
* rounded for the reuse.
|
|
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.
|
|
653
661
|
*
|
|
654
662
|
* @param {{ segments?: object[], geometry?: object }} track
|
|
655
663
|
* @returns {bigint} The least timescale holding every edge and every served tick of its pieces.
|
|
@@ -657,7 +665,9 @@ function timesOf(segment) {
|
|
|
657
665
|
function timescaleOfPieces(track) {
|
|
658
666
|
const segments = Array.isArray(track.segments) ? track.segments : [];
|
|
659
667
|
const geometry = track.geometry;
|
|
660
|
-
|
|
668
|
+
const changed = geometry ? piecesChanged(geometry.pieces, segments) : null;
|
|
669
|
+
if (geometry && changed === null) return geometry.timescale;
|
|
670
|
+
if (geometry) geometry.reason = changed;
|
|
661
671
|
const distinct = new Set();
|
|
662
672
|
for (const segment of segments) {
|
|
663
673
|
if (segment.mediaRanges?.timescale) distinct.add(segment.mediaRanges.timescale);
|
|
@@ -667,7 +677,7 @@ function timescaleOfPieces(track) {
|
|
|
667
677
|
}
|
|
668
678
|
const timescale = commonTimescale([...distinct].map((value) => mediaTime(0n, value)));
|
|
669
679
|
if (geometry) {
|
|
670
|
-
geometry.pieces = segments.map((segment) => ({
|
|
680
|
+
geometry.pieces = segments.map((segment) => ({ index: segment.index, startSeconds: segment.startSeconds,
|
|
671
681
|
endSeconds: segment.endSeconds, mediaRanges: segment.mediaRanges }));
|
|
672
682
|
geometry.timescale = timescale;
|
|
673
683
|
geometry.placed = null;
|
|
@@ -677,8 +687,9 @@ function timescaleOfPieces(track) {
|
|
|
677
687
|
|
|
678
688
|
/**
|
|
679
689
|
* A track's pieces on the page's clock in `unit`: read pieces at their served
|
|
680
|
-
* ranges, the others placed between them.
|
|
681
|
-
* {@link timescaleOfPieces} last took and `unit` and `clock`
|
|
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.
|
|
682
693
|
*
|
|
683
694
|
* @param {{ segments?: object[], geometry?: object }} track
|
|
684
695
|
* @param {import("./media-time.js").MediaTime} clock - In `unit`.
|
|
@@ -686,23 +697,34 @@ function timescaleOfPieces(track) {
|
|
|
686
697
|
*/
|
|
687
698
|
function placedPieces(track, clock, unit) {
|
|
688
699
|
const geometry = track.geometry;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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 }));
|
|
695
710
|
}
|
|
696
711
|
|
|
697
|
-
/**
|
|
698
|
-
|
|
699
|
-
|
|
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";
|
|
700
721
|
for (let index = 0; index < segments.length; index++) {
|
|
701
722
|
const segment = segments[index], was = kept[index];
|
|
702
|
-
if (segment
|
|
703
|
-
|
|
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";
|
|
704
726
|
}
|
|
705
|
-
return
|
|
727
|
+
return null;
|
|
706
728
|
}
|
|
707
729
|
|
|
708
730
|
/**
|