@torrent-tv/proxy 2.98.9 → 2.98.11
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/EncodeInputs.js +15 -0
- package/services/encode/EncodeRuns.js +27 -8
- package/services/encode/SpeedReadings.js +12 -8
- package/services/encode/next-input.js +26 -0
- package/services/server/ViewerRequests.js +14 -22
- package/services/server/wire-outputs.js +3 -3
- package/services/viewer/playback-readiness.js +16 -5
package/package.json
CHANGED
|
@@ -28,6 +28,8 @@ export class EncodeInputs {
|
|
|
28
28
|
/** Whether an output's last resolved input was its original file. @type {WeakMap<object, boolean>} */
|
|
29
29
|
#original = new WeakMap();
|
|
30
30
|
#revision = 0;
|
|
31
|
+
/** Output → how long its last input took to be ready, ms. @type {WeakMap<object, number>} */
|
|
32
|
+
#preparedIn = new WeakMap();
|
|
31
33
|
#memoryRevision = 0;
|
|
32
34
|
#held = 0;
|
|
33
35
|
#allowed = 0;
|
|
@@ -270,6 +272,18 @@ export class EncodeInputs {
|
|
|
270
272
|
* @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> } | null} [arrived] -
|
|
271
273
|
* The bytes that arrived, inclusive, when the event names them.
|
|
272
274
|
*/
|
|
275
|
+
/**
|
|
276
|
+
* How long this output's last input took from being asked for to being
|
|
277
|
+
* ready, in milliseconds, or null before one was: the time a run gives the
|
|
278
|
+
* input after it to be prepared in (`EncodeRuns`).
|
|
279
|
+
*
|
|
280
|
+
* @param {object} output
|
|
281
|
+
* @returns {number | null}
|
|
282
|
+
*/
|
|
283
|
+
preparationMsOf(output) {
|
|
284
|
+
return this.#preparedIn.get(output) ?? null;
|
|
285
|
+
}
|
|
286
|
+
|
|
273
287
|
bytesChanged(arrived = null) {
|
|
274
288
|
this.#revision++;
|
|
275
289
|
for (const request of this.#requests.values()) {
|
|
@@ -376,6 +390,7 @@ export class EncodeInputs {
|
|
|
376
390
|
// is commanded over its prefix.
|
|
377
391
|
if (result.kind === "result" && result.to > request.to) result.to = Math.max(request.from, request.to);
|
|
378
392
|
request.result = result;
|
|
393
|
+
if (result.kind === "result") this.#preparedIn.set(request.output, this.#now() - request.timing.askedAt);
|
|
379
394
|
this.#note(request, result.kind === "result" ? `ready #${result.from}..#${result.to}, ${result.bytes ?? "?"} bytes` +
|
|
380
395
|
(Number.isFinite(result.copyMs) ? (result.deferredBytes > 0
|
|
381
396
|
? `, opening part copied in ${result.copyMs}ms, ${result.deferredBytes} bytes of the stretch copied while the run opens`
|
|
@@ -12,6 +12,7 @@ import { spawn } from "node:child_process";
|
|
|
12
12
|
import { contentionPenalty } from "./contention.js";
|
|
13
13
|
import { LOOKAHEAD_PAUSE_SECONDS } from "./CushionReport.js";
|
|
14
14
|
import { SpeedReadings } from "./SpeedReadings.js";
|
|
15
|
+
import { nextInputAtFirstPiece } from "./next-input.js";
|
|
15
16
|
import { ENCODE_RUN_STATE, liveRunsOf, runStateOf, wireState } from "./encode-run-state.js";
|
|
16
17
|
import { ENCODE_EXIT } from "./encode-exit.js";
|
|
17
18
|
import { EncodeRun, endOfRun } from "./EncodeRun.js";
|
|
@@ -497,15 +498,14 @@ export class EncodeRuns {
|
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
/**
|
|
500
|
-
* The speed readings of this output's runs
|
|
501
|
-
*
|
|
501
|
+
* The speed readings of this output's runs within the forecast's horizon,
|
|
502
|
+
* in the order they were taken.
|
|
502
503
|
*
|
|
503
504
|
* @param {object} output
|
|
504
|
-
* @param {number} [after] - The time of the last reading the asker has.
|
|
505
505
|
* @returns {Array<{ speed: number, at: number }>}
|
|
506
506
|
*/
|
|
507
|
-
speedReadingsOf(output
|
|
508
|
-
return this.#speedReadings.
|
|
507
|
+
speedReadingsOf(output) {
|
|
508
|
+
return this.#speedReadings.windowOf(output);
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
/**
|
|
@@ -1097,6 +1097,8 @@ export class EncodeRuns {
|
|
|
1097
1097
|
// killed mid-piece leaving a partial file — is not a correctness problem
|
|
1098
1098
|
// either, because the store serves a segment only once its closure is
|
|
1099
1099
|
// proven, and it is cleared up when the run ends.
|
|
1100
|
+
// Told when this run closes its first piece; see the next stretch below.
|
|
1101
|
+
let atFirstPiece = null;
|
|
1100
1102
|
const run = new EncodeRun({
|
|
1101
1103
|
address: session.outputKey ?? session.id,
|
|
1102
1104
|
encoder: this.#host.videoEncoder,
|
|
@@ -1130,7 +1132,12 @@ export class EncodeRuns {
|
|
|
1130
1132
|
onProgress: (report) => this.#noteRunProgress(session, run, report),
|
|
1131
1133
|
// A closed piece moves this encoder forward, perhaps out of the band that
|
|
1132
1134
|
// led; who has the processor is decided again at once.
|
|
1133
|
-
onProduced: () =>
|
|
1135
|
+
onProduced: () => {
|
|
1136
|
+
this.#host.encodeOrchestrator?.shareTheMachine?.();
|
|
1137
|
+
const told = atFirstPiece;
|
|
1138
|
+
atFirstPiece = null;
|
|
1139
|
+
told?.();
|
|
1140
|
+
},
|
|
1134
1141
|
indexOfName: (name) => session.segmentFormat.segmentIndexFromName(
|
|
1135
1142
|
session.segmentFormat.servedNameOf?.(name) ?? name),
|
|
1136
1143
|
// Why this encoder exists, recorded with its argument list. It used to be
|
|
@@ -1232,12 +1239,24 @@ export class EncodeRuns {
|
|
|
1232
1239
|
// It waits for this run's own stretch to be copied: that copy goes on
|
|
1233
1240
|
// while the run opens and FFmpeg's reads wait for it, and a second copy
|
|
1234
1241
|
// beside it on the same thread would slow it (torrent-tv/meta#178).
|
|
1242
|
+
//
|
|
1243
|
+
// And for this run's first piece when the rest of the run leaves the
|
|
1244
|
+
// time to prepare it in (`next-input.js`).
|
|
1235
1245
|
if (inputEnd < mayReach) {
|
|
1236
1246
|
const askNext = () => {
|
|
1237
1247
|
if (this.isLive(session) && run.isAlive) this.#host.encodeInputs?.take(session, inputEnd + 1, mayReach);
|
|
1238
1248
|
};
|
|
1239
|
-
|
|
1240
|
-
|
|
1249
|
+
const preparedInMs = this.#host.encodeInputs?.preparationMsOf?.(session) ?? null;
|
|
1250
|
+
const speed = Number(this.#host.encodeCost?.speedForOutput?.(session.outputKey));
|
|
1251
|
+
const timeline = session.timeline;
|
|
1252
|
+
const restSeconds = Number(timeline?.publishedStartOf?.(inputEnd + 1)) - Number(timeline?.publishedStartOf?.(safeIndex + 1));
|
|
1253
|
+
const atOnce = !nextInputAtFirstPiece({ preparedInMs, speed, restSeconds });
|
|
1254
|
+
let copied = false, produced = atOnce;
|
|
1255
|
+
const askWhenBoth = () => { if (copied && produced) askNext(); };
|
|
1256
|
+
if (!atOnce) atFirstPiece = () => { produced = true; askWhenBoth(); };
|
|
1257
|
+
const whenCopied = () => { copied = true; askWhenBoth(); };
|
|
1258
|
+
if (admittedInput.whole) void admittedInput.whole.then(whole => { if (whole) whenCopied(); });
|
|
1259
|
+
else whenCopied();
|
|
1241
1260
|
}
|
|
1242
1261
|
} else if (admittedInput) {
|
|
1243
1262
|
run.admittedInputKeys = new Map([[safeIndex, inputKey]]);
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* @file The speed readings of each output's runs, in the order the runs
|
|
3
3
|
* measured them, over the start forecast's horizon (torrent-tv/meta#178).
|
|
4
4
|
*
|
|
5
|
-
* A run announces a reading at each closed piece;
|
|
6
|
-
*
|
|
5
|
+
* A run announces a reading at each closed piece; the forecast summarizes the
|
|
6
|
+
* whole window on each poll. Two runs of one output can announce out of order —
|
|
7
|
+
* each reading carries the time of its own run's last progress report — so the
|
|
8
|
+
* window is kept in the order the readings were TAKEN, and two taken in the
|
|
9
|
+
* same millisecond are two readings. The forecast used to take only the newest
|
|
7
10
|
* reading of a run alive when it asked: on a cold start on Home Assistant the
|
|
8
11
|
* runs were three pieces long and ended between its polls, and of the readings
|
|
9
12
|
* 0.78, 1.80, 2.25, 2.84 and 2.69x it got 0.78 and 2.69x, and held the picture
|
|
@@ -28,18 +31,19 @@ export class SpeedReadings {
|
|
|
28
31
|
if (!output || !(reading?.speed > 0) || !Number.isFinite(reading.at)) return;
|
|
29
32
|
let readings = this.#byOutput.get(output);
|
|
30
33
|
if (!readings) this.#byOutput.set(output, readings = []);
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
let place = readings.length;
|
|
35
|
+
while (place > 0 && readings[place - 1].at > reading.at) place -= 1;
|
|
36
|
+
readings.splice(place, 0, { speed: reading.speed, at: reading.at });
|
|
37
|
+
const cutoff = readings.at(-1).at - this.#keepMs;
|
|
33
38
|
// As the forecast's own trend keeps them: the newest one before the horizon stays.
|
|
34
39
|
while (readings.length > 1 && readings[1].at <= cutoff) readings.shift();
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* @param {object} output
|
|
39
|
-
* @
|
|
40
|
-
* @returns {Array<{ speed: number, at: number }>} Oldest first.
|
|
44
|
+
* @returns {Array<{ speed: number, at: number }>} The window, in the order the readings were taken.
|
|
41
45
|
*/
|
|
42
|
-
|
|
43
|
-
return (this.#byOutput.get(output) ?? [])
|
|
46
|
+
windowOf(output) {
|
|
47
|
+
return [...(this.#byOutput.get(output) ?? [])];
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file When a run asks for the input of the run after it (torrent-tv/meta#178).
|
|
3
|
+
*
|
|
4
|
+
* Asked as soon as the run's own stretch was copied, the next input was copied
|
|
5
|
+
* while the run opened: a background run on Home Assistant started with its
|
|
6
|
+
* 108.7 MB, the next 115.6 MB were copied at once, the proxy took 1.5-2 cores,
|
|
7
|
+
* and the share left to encoders fell from 65 % to 10-16 % before either had
|
|
8
|
+
* a piece (r37). Asked at the run's first piece, it still has the rest of the
|
|
9
|
+
* run to be ready in — unless preparing an input of this output was measured
|
|
10
|
+
* to take longer than that rest at this output's speed, or the run is one
|
|
11
|
+
* piece, whose first piece is its last.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether the next input waits for this run's first piece.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} params
|
|
18
|
+
* @param {number | null} params.preparedInMs - How long this output's last input took from being asked for to being ready.
|
|
19
|
+
* @param {number} params.speed - This output's processing speed, film seconds per second; 0 when not known.
|
|
20
|
+
* @param {number} params.restSeconds - The film this run makes after its first piece.
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function nextInputAtFirstPiece({ preparedInMs, speed, restSeconds }) {
|
|
24
|
+
if (!Number.isFinite(preparedInMs) || !(speed > 0) || !(restSeconds > 0)) return false;
|
|
25
|
+
return preparedInMs < restSeconds / speed * 1000;
|
|
26
|
+
}
|
|
@@ -604,7 +604,7 @@ export class ViewerRequests {
|
|
|
604
604
|
const tracks = [];
|
|
605
605
|
for (const output of outputs) {
|
|
606
606
|
const trackProgress = output === session ? progress : this.#host.encodeRuns.progressOf(output);
|
|
607
|
-
const trackRates = this.#trackRateReadings(
|
|
607
|
+
const trackRates = this.#trackRateReadings(output, now);
|
|
608
608
|
const trackName = output === session ? "video" : "audio";
|
|
609
609
|
// The page states the offset its player applied to this track, and the
|
|
610
610
|
// init it applied it to is the session's. Until both are known, the
|
|
@@ -691,12 +691,6 @@ export class ViewerRequests {
|
|
|
691
691
|
});
|
|
692
692
|
}
|
|
693
693
|
|
|
694
|
-
const activeTrackIds = new Set(tracks.map(({ id }) => id));
|
|
695
|
-
for (const id of measurement.tracks.keys()) {
|
|
696
|
-
if (!activeTrackIds.has(id)) {
|
|
697
|
-
measurement.tracks.delete(id);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
694
|
const bufferedAheadSeconds = Number(viewerReading?.bufferedAheadSec);
|
|
701
695
|
const positionSeconds = Number.isFinite(viewerReading?.positionSeconds)
|
|
702
696
|
? viewerReading.positionSeconds
|
|
@@ -880,25 +874,23 @@ export class ViewerRequests {
|
|
|
880
874
|
const id = typeof consumerId === "string" ? consumerId : "";
|
|
881
875
|
let state = byViewer.get(id);
|
|
882
876
|
if (!state) {
|
|
883
|
-
state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds),
|
|
877
|
+
state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), sources: new Map() };
|
|
884
878
|
byViewer.set(id, state);
|
|
885
879
|
}
|
|
886
880
|
return state;
|
|
887
881
|
}
|
|
888
882
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
if (!trend.snapshot()) {
|
|
883
|
+
/**
|
|
884
|
+
* The encoder's rate for the forecast: the output's whole window of readings,
|
|
885
|
+
* summarized again on each poll. A run that ended between two polls took its
|
|
886
|
+
* readings with it when the forecast asked for the newest one alone; and two
|
|
887
|
+
* runs of one output can report out of order, so a summary kept and added to
|
|
888
|
+
* would drop the later-told, earlier-taken one (torrent-tv/meta#178).
|
|
889
|
+
*/
|
|
890
|
+
#trackRateReadings(output, now) {
|
|
891
|
+
const readings = (this.#host.encodeSpeedReadingsOf?.(output) ?? []).filter(reading => reading.at <= now);
|
|
892
|
+
const summary = readings.length > 0 ? this.#host.playbackReadiness.rateSummaryOf(readings) : null;
|
|
893
|
+
if (!summary) {
|
|
902
894
|
// ONLY UNTIL A PIECE IS MEASURED does the prediction stand for the
|
|
903
895
|
// encoder, and it is not kept among the readings. Added on every poll
|
|
904
896
|
// between runs, it put the machine's share of that moment — 1 % at a
|
|
@@ -911,7 +903,7 @@ export class ViewerRequests {
|
|
|
911
903
|
return seed.snapshot();
|
|
912
904
|
}
|
|
913
905
|
}
|
|
914
|
-
return
|
|
906
|
+
return summary;
|
|
915
907
|
}
|
|
916
908
|
|
|
917
909
|
/**
|
|
@@ -71,7 +71,7 @@ import { ViewerRequests } from "./ViewerRequests.js";
|
|
|
71
71
|
import { OutputLifecycle } from "./OutputLifecycle.js";
|
|
72
72
|
import { SegmentServing } from "./SegmentServing.js";
|
|
73
73
|
import { audioStartSecondsFor } from "../viewer/audio-start.js";
|
|
74
|
-
import { forecastRate, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
|
|
74
|
+
import { forecastRate, rateSummaryOf, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
|
|
75
75
|
import { audioRenditionName } from "../media/audio-inventory.js";
|
|
76
76
|
import { Renditions } from "../encode/Renditions.js";
|
|
77
77
|
import { CushionReport, LOOKAHEAD_PAUSE_SECONDS } from "../encode/CushionReport.js";
|
|
@@ -578,12 +578,12 @@ export function wireOutputs({
|
|
|
578
578
|
get encodeRuns() { return parts.encodeRuns; },
|
|
579
579
|
// When this viewer's playback can start and run to the end: the viewer
|
|
580
580
|
// component's forecast, which models the viewer's browser.
|
|
581
|
-
playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, RateTrend },
|
|
581
|
+
playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, rateSummaryOf, RateTrend },
|
|
582
582
|
// Whether the subtitle a viewer chose has been read where they stand — the
|
|
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
|
-
encodeSpeedReadingsOf: (output
|
|
586
|
+
encodeSpeedReadingsOf: (output) => parts.encodeRuns.speedReadingsOf(output),
|
|
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
|
|
@@ -58,6 +58,22 @@ export function forecastRate(readings, now = Date.now(), horizonSeconds = 0, wei
|
|
|
58
58
|
return horizon > 0 ? curve.workBy(horizon) / horizon : curve.rateAt(0);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The summary the forecast reads, of a window of readings taken in any order:
|
|
63
|
+
* ordered by when each was taken, readings taken in the same millisecond kept
|
|
64
|
+
* apart. Null for no reading.
|
|
65
|
+
*
|
|
66
|
+
* @param {Array<{ at: number, speed?: number, value?: number }>} readings
|
|
67
|
+
* @returns {object | null}
|
|
68
|
+
*/
|
|
69
|
+
export function rateSummaryOf(readings) {
|
|
70
|
+
const ordered = (Array.isArray(readings) ? readings : [])
|
|
71
|
+
.map(reading => ({ at: reading?.at, value: reading?.value ?? reading?.speed }))
|
|
72
|
+
.filter(reading => Number.isFinite(reading.at) && Number.isFinite(reading.value) && reading.value >= 0)
|
|
73
|
+
.sort((left, right) => left.at - right.at);
|
|
74
|
+
return ordered.length > 0 ? summarizeRate(ordered) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/**
|
|
62
78
|
* Measurements over the output's existing look-ahead horizon. The oldest
|
|
63
79
|
* boundary sample is retained until another sample replaces it, so a repeated
|
|
@@ -84,11 +100,6 @@ export class RateTrend {
|
|
|
84
100
|
return true;
|
|
85
101
|
}
|
|
86
102
|
|
|
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
|
-
|
|
92
103
|
snapshot() {
|
|
93
104
|
if (this.#readings.length === 0) {
|
|
94
105
|
return null;
|