@torrent-tv/proxy 2.94.3 → 2.95.1
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/assets/calibration/NOTICE.md +25 -0
- package/assets/calibration/cal-av1-1080-hi.mp4 +0 -0
- package/assets/calibration/cal-av1-1080-lo.mp4 +0 -0
- package/assets/calibration/cal-av1-480-hi.mp4 +0 -0
- package/assets/calibration/cal-av1-480-lo.mp4 +0 -0
- package/assets/calibration/cal-mpeg2-1080-hi.mp4 +0 -0
- package/assets/calibration/cal-mpeg2-1080-lo.mp4 +0 -0
- package/assets/calibration/cal-mpeg2-480-hi.mp4 +0 -0
- package/assets/calibration/cal-mpeg2-480-lo.mp4 +0 -0
- package/assets/calibration/cal-mpeg4-1080-hi.mp4 +0 -0
- package/assets/calibration/cal-mpeg4-1080-lo.mp4 +0 -0
- package/assets/calibration/cal-mpeg4-480-hi.mp4 +0 -0
- package/assets/calibration/cal-mpeg4-480-lo.mp4 +0 -0
- package/package.json +1 -1
- package/server.js +0 -2
- package/services/encode/EncodeOrchestrator.js +10 -0
- package/services/encode/LocalObservations.js +11 -2
- package/services/encode/OutputOpening.js +3 -6
- package/services/encode/SoftwareEncoder.js +2 -2
- package/services/encode/available-share.js +5 -1
- package/services/encode/calibration.js +15 -2
- package/services/encode/decode-cost-fit.js +12 -0
- package/services/encode/host-load.js +34 -0
- package/services/encode/hwaccel.js +139 -125
- package/services/encode/quality/ColdStarts.js +78 -0
- package/services/encode/quality/EncodeCost.js +119 -47
- package/services/encode/quality/OutputQualityState.js +2 -0
- package/services/encode/quality/QualityController.js +9 -5
- package/services/media/playback-declarations.js +6 -1
- package/services/media/playback-planner.js +10 -20
- package/services/server/SegmentServing.js +3 -3
- package/services/server/ViewerRequests.js +1 -10
- package/services/server/wire-outputs.js +14 -19
- package/services/encode/quality/HostTimings.js +0 -249
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What this host takes to create an output and to produce its first
|
|
3
|
-
* segment: the recorded medians, the synthetic figure from the startup
|
|
4
|
-
* measurement, and the file that keeps the medians across a restart.
|
|
5
|
-
*
|
|
6
|
-
* Moved out of the session manager whole. The file it keeps is to be deleted once
|
|
7
|
-
* the synthetic figure takes every term of the first-segment time as an input.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { logger } from "../../../utils/logger.js";
|
|
14
|
-
import { TRANSCODE_FPS } from "../hwaccel.js";
|
|
15
|
-
// How many recent runs the two cold-start estimates keep. Both the
|
|
16
|
-
// session-create time and the first-segment time are reported to the browser as
|
|
17
|
-
// the median of this many samples, so it has to be long enough that one slow run
|
|
18
|
-
// does not move the figure and short enough that the estimate still follows the
|
|
19
|
-
// host: a proxy whose swarm has warmed up, or which has just picked up a second
|
|
20
|
-
// viewer, should stop quoting the numbers from ten minutes ago.
|
|
21
|
-
const FIRST_SEGMENT_SAMPLES = 20;
|
|
22
|
-
|
|
23
|
-
/** The directory of the installed proxy, where its measurements are kept when no state directory is named. */
|
|
24
|
-
export const PROXY_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
25
|
-
|
|
26
|
-
export class HostTimings {
|
|
27
|
-
/** What this reads and asks of the rest of the proxy, and nothing else. @type {object} */
|
|
28
|
-
#host;
|
|
29
|
-
|
|
30
|
-
#firstSegmentLatencies = [];
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Output → when the request that created it arrived, until its first segment
|
|
34
|
-
* is served. Taken out on that first segment, so each output is measured once
|
|
35
|
-
* and an output nobody created in this process (adopted at startup) is never
|
|
36
|
-
* measured at all.
|
|
37
|
-
*
|
|
38
|
-
* @type {WeakMap<object, number>}
|
|
39
|
-
*/
|
|
40
|
-
#createdAt = new WeakMap();
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Recent times to create a session, in ms — the second term of the browser's
|
|
44
|
-
* estimate. Measured for the same reason as the first: it is 116-843 ms
|
|
45
|
-
* depending on whether the keyframe index is already in hand, and guessing it
|
|
46
|
-
* was one of the ways the shown figure stopped describing the whole wait.
|
|
47
|
-
*
|
|
48
|
-
* @type {number[]}
|
|
49
|
-
*/
|
|
50
|
-
#sessionCreateLatencies = [];
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {object} host - `segmentDurationSec`, `softwarePresetBenchmark`, `stateDir`
|
|
54
|
-
*/
|
|
55
|
-
constructor(host) {
|
|
56
|
-
this.#host = host;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Where this host's recorded timings live: `--state-dir` when the deployment
|
|
61
|
-
* names one, otherwise beside the installed proxy, which is where they have
|
|
62
|
-
* always been kept.
|
|
63
|
-
*
|
|
64
|
-
* The default is deliberately the old location and not the working directory:
|
|
65
|
-
* measured on the addon, both are inside the container's writable layer and
|
|
66
|
-
* both are discarded when an update rebuilds it, so moving there bought
|
|
67
|
-
* nothing — while for an ordinary `npm i -g` install the working directory is
|
|
68
|
-
* wherever the operator happened to launch from, which splits the history
|
|
69
|
-
* between runs and drops a file into someone's project.
|
|
70
|
-
*
|
|
71
|
-
* A deployment that HAS a persistent directory says so: the addon passes
|
|
72
|
-
* `/data`, the one path its supervisor keeps across updates. Naming it here
|
|
73
|
-
* would put Home Assistant into proxy code, which this repo does not do.
|
|
74
|
-
*
|
|
75
|
-
* Kept so a proxy that has just restarted is not back to knowing nothing —
|
|
76
|
-
* the browser was shown an assumed rate for the whole of the first wait after
|
|
77
|
-
* every restart. It is meant to be TEMPORARY: if the synthetic figure tracks
|
|
78
|
-
* the measured one closely enough (see #compareSyntheticWithMeasured) this
|
|
79
|
-
* file can go, and every machine is then right from its first second without
|
|
80
|
-
* carrying anything between runs.
|
|
81
|
-
*
|
|
82
|
-
* @returns {string}
|
|
83
|
-
*/
|
|
84
|
-
#hostTimingsPath() {
|
|
85
|
-
const stateDir = typeof this.#host.stateDir === "string" && this.#host.stateDir.length > 0
|
|
86
|
-
? this.#host.stateDir
|
|
87
|
-
: PROXY_ROOT;
|
|
88
|
-
return path.join(stateDir, "host-timings.json");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
loadHostTimings() {
|
|
92
|
-
try {
|
|
93
|
-
const raw = JSON.parse(readFileSync(this.#hostTimingsPath(), "utf8"));
|
|
94
|
-
if (Array.isArray(raw?.firstSegment)) {
|
|
95
|
-
this.#firstSegmentLatencies = raw.firstSegment.filter((value) => Number.isFinite(value) && value > 0);
|
|
96
|
-
}
|
|
97
|
-
if (Array.isArray(raw?.sessionCreate)) {
|
|
98
|
-
this.#sessionCreateLatencies = raw.sessionCreate.filter((value) => Number.isFinite(value) && value > 0);
|
|
99
|
-
}
|
|
100
|
-
const asMs = (value) => (value === null ? "n/a" : `${value}ms`);
|
|
101
|
-
logger.info(
|
|
102
|
-
`host timings loaded from ${this.#hostTimingsPath()}: ` +
|
|
103
|
-
`first-segment ${asMs(this.expectedFirstSegmentMs())}, ` +
|
|
104
|
-
`session-create ${asMs(this.expectedSessionCreateMs())}`
|
|
105
|
-
);
|
|
106
|
-
} catch {
|
|
107
|
-
// No file yet, or it is unreadable. The synthetic figure answers instead.
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
#saveHostTimings() {
|
|
112
|
-
try {
|
|
113
|
-
writeFileSync(this.#hostTimingsPath(), JSON.stringify({
|
|
114
|
-
firstSegment: this.#firstSegmentLatencies,
|
|
115
|
-
sessionCreate: this.#sessionCreateLatencies
|
|
116
|
-
}));
|
|
117
|
-
} catch {
|
|
118
|
-
// Read-only install, no permission — not worth failing a session over.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
syntheticFirstSegmentMs(output = {}) {
|
|
123
|
-
const benchmark = this.#host.softwarePresetBenchmark;
|
|
124
|
-
if (!Array.isArray(benchmark) || benchmark.length === 0) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
const width = Number.isFinite(output.width) && output.width > 0 ? output.width : 1920;
|
|
128
|
-
const height = Number.isFinite(output.height) && output.height > 0 ? output.height : 1080;
|
|
129
|
-
const fps = Number.isFinite(output.fps) && output.fps > 0 ? output.fps : TRANSCODE_FPS;
|
|
130
|
-
// The preset actually chosen sits somewhere in the middle of the ladder;
|
|
131
|
-
// the median entry is the representative one and involves no choice.
|
|
132
|
-
const sorted = [...benchmark].sort((left, right) => left.pixelsPerSec - right.pixelsPerSec);
|
|
133
|
-
const pixelsPerSec = sorted[Math.floor(sorted.length / 2)]?.pixelsPerSec;
|
|
134
|
-
if (!Number.isFinite(pixelsPerSec) || pixelsPerSec <= 0) {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
const pixels = this.#host.segmentDurationSec * width * height * fps;
|
|
138
|
-
return (pixels / pixelsPerSec) * 1000;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Say how the synthetic figure compares with what actually happened.
|
|
143
|
-
*
|
|
144
|
-
* The point is to learn whether the startup benchmark alone can carry the
|
|
145
|
-
* estimate. If the two track each other, the recorded history can go and
|
|
146
|
-
* every machine is right from its first second; if they do not, the log says
|
|
147
|
-
* by how much and in which direction, which is the beginning of knowing why.
|
|
148
|
-
*
|
|
149
|
-
* @param {number} measuredMs
|
|
150
|
-
* @returns {void}
|
|
151
|
-
*/
|
|
152
|
-
#compareSyntheticWithMeasured(measuredMs) {
|
|
153
|
-
const synthetic = this.syntheticFirstSegmentMs();
|
|
154
|
-
if (synthetic === null) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
const ratio = measuredMs / synthetic;
|
|
158
|
-
logger.info(
|
|
159
|
-
`first-segment synthetic=${Math.round(synthetic)}ms measured=${Math.round(measuredMs)}ms ` +
|
|
160
|
-
`ratio=${ratio.toFixed(2)} (1.00 would mean the startup benchmark alone suffices)`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
rememberSessionCreateLatency(latencyMs) {
|
|
165
|
-
if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
this.#sessionCreateLatencies.push(latencyMs);
|
|
169
|
-
if (this.#sessionCreateLatencies.length > FIRST_SEGMENT_SAMPLES) {
|
|
170
|
-
this.#sessionCreateLatencies.shift();
|
|
171
|
-
}
|
|
172
|
-
this.#saveHostTimings();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* What this host typically takes to create a session, in ms — the median of
|
|
177
|
-
* recent ones, or null before any has finished.
|
|
178
|
-
*
|
|
179
|
-
* @returns {number | null}
|
|
180
|
-
*/
|
|
181
|
-
expectedSessionCreateMs() {
|
|
182
|
-
if (this.#sessionCreateLatencies.length === 0) {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
const sorted = [...this.#sessionCreateLatencies].sort((left, right) => left - right);
|
|
186
|
-
return sorted[Math.floor(sorted.length / 2)];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* An output was created by a request that arrived at `at`.
|
|
191
|
-
*
|
|
192
|
-
* @param {object} output
|
|
193
|
-
* @param {number} at
|
|
194
|
-
* @returns {void}
|
|
195
|
-
*/
|
|
196
|
-
noteOutputCreated(output, at) {
|
|
197
|
-
if (Number.isFinite(at) && at > 0) {
|
|
198
|
-
this.#createdAt.set(output, at);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* A segment of this output has just been served. The first one closes the
|
|
204
|
-
* cold-start measurement and is remembered; every later one is nothing.
|
|
205
|
-
*
|
|
206
|
-
* @param {object} output
|
|
207
|
-
* @param {number} [now]
|
|
208
|
-
* @returns {number | null} The cold-start latency when this was the first.
|
|
209
|
-
*/
|
|
210
|
-
noteSegmentServed(output, now = Date.now()) {
|
|
211
|
-
const at = this.#createdAt.get(output);
|
|
212
|
-
if (at === undefined) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
this.#createdAt.delete(output);
|
|
216
|
-
const latencyMs = now - at;
|
|
217
|
-
this.rememberFirstSegmentLatency(latencyMs);
|
|
218
|
-
return latencyMs;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
rememberFirstSegmentLatency(latencyMs) {
|
|
222
|
-
if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
this.#compareSyntheticWithMeasured(latencyMs);
|
|
226
|
-
this.#firstSegmentLatencies.push(latencyMs);
|
|
227
|
-
if (this.#firstSegmentLatencies.length > FIRST_SEGMENT_SAMPLES) {
|
|
228
|
-
this.#firstSegmentLatencies.shift();
|
|
229
|
-
}
|
|
230
|
-
this.#saveHostTimings();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* What this host typically takes to produce a session's first segment, in
|
|
235
|
-
* milliseconds — the median of recent runs, or null before any has finished.
|
|
236
|
-
*
|
|
237
|
-
* @returns {number | null}
|
|
238
|
-
*/
|
|
239
|
-
expectedFirstSegmentMs() {
|
|
240
|
-
if (this.#firstSegmentLatencies.length === 0) {
|
|
241
|
-
// Nothing recorded yet — a machine's first run, or one whose history has
|
|
242
|
-
// not been written. The startup benchmark answers without any history at
|
|
243
|
-
// all, which is why the browser was showing an assumed rate here.
|
|
244
|
-
return this.syntheticFirstSegmentMs();
|
|
245
|
-
}
|
|
246
|
-
const sorted = [...this.#firstSegmentLatencies].sort((left, right) => left - right);
|
|
247
|
-
return sorted[Math.floor(sorted.length / 2)];
|
|
248
|
-
}
|
|
249
|
-
}
|