@torrent-tv/proxy 2.80.4 → 2.80.6
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/CHANGELOG.md +36 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +42 -22
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +55 -3
- package/services/encode/open-piece.js +47 -24
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +123 -94
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +152 -0
- package/test/output-speed.test.js +86 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
- package/test/flushed-piece.test.js +0 -108
|
@@ -1,500 +1,555 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What encoding this file costs THIS machine, and which heights follow.
|
|
3
|
-
*
|
|
4
|
-
* One subject: seconds of work per second of video. Everything here is that
|
|
5
|
-
* question asked about something — a picture being re-encoded, a soundtrack, a
|
|
6
|
-
* copy, everything running beside the rung being judged — and the last method
|
|
7
|
-
* turns the answers into the list of heights the machine can hold.
|
|
8
|
-
*
|
|
9
|
-
* It decides what a viewer is offered, so being wrong in either direction costs
|
|
10
|
-
* them: too generous and they are given a rung that runs below realtime, which
|
|
11
|
-
* is a slideshow; too mean and they are refused quality the host could hold.
|
|
12
|
-
*
|
|
13
|
-
* Nothing here is chosen. Every figure is a measurement — the startup
|
|
14
|
-
* benchmarks, what an encoder has since been seen doing on this very file, what
|
|
15
|
-
* a second job costs on this host, what share of the machine is free — and where
|
|
16
|
-
* a term has not been measured it contributes nothing rather than a guess.
|
|
17
|
-
*
|
|
18
|
-
* What it is given, and why each is passed rather than reached for: which
|
|
19
|
-
* sessions belong to one file (`liveOutputs`), the host's own readings, which
|
|
20
|
-
* key names a soundtrack, how many encoders are running, and what the file costs
|
|
21
|
-
* merely by being fetched. The learned costs it holds itself: they are what an
|
|
22
|
-
* encoder taught it, and it is their only reader.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { correctForAvailability } from "../available-share.js";
|
|
26
|
-
import { contentionPenalty } from "../contention.js";
|
|
27
|
-
import { TRANSCODE_FPS } from "../encode/args.js";
|
|
28
|
-
import { processCanBeSignalled, runStateOf } from "../encode/encode-run-state.js";
|
|
29
|
-
import { canSustainOutput, speedBar } from "../hwaccel.js";
|
|
30
|
-
import { logger } from "../../utils/logger.js";
|
|
31
|
-
|
|
32
|
-
export class EncodeCost {
|
|
33
|
-
/**
|
|
34
|
-
* What a soundtrack encoder has been seen to cost, by the key naming that
|
|
35
|
-
* track. Written by whoever learns from a run; read here and nowhere else.
|
|
36
|
-
*
|
|
37
|
-
* Public for now because the three that learn are still methods of the
|
|
38
|
-
* session manager. Moving them here is the next step, and until then two
|
|
39
|
-
* objects must not each keep a copy of the same reading.
|
|
40
|
-
*
|
|
41
|
-
* @type {Map<string, { costSec: number, readings: number[], version: number }>}
|
|
42
|
-
*/
|
|
43
|
-
audioCost = new Map();
|
|
44
|
-
|
|
45
|
-
/** What copying a file's picture has been seen to cost. @type {Map<string, { costSec: number, readings: number[], version: number }>} */
|
|
46
|
-
copyCost = new Map();
|
|
47
|
-
|
|
48
|
-
/** What decoding a file has been seen to cost. @type {Map<string, { costSec: number, readings: number[], version: number }>} */
|
|
49
|
-
decodeCost = new Map();
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* What each height was last predicted to do, kept so a session started at
|
|
53
|
-
* that height can be compared against the prediction once it runs.
|
|
54
|
-
*
|
|
55
|
-
* @type {Map<number, number | null> | null}
|
|
56
|
-
*/
|
|
57
|
-
lastPredictedByHeight = null;
|
|
58
|
-
|
|
59
|
-
// The last refusal printed. The offer is recomputed on the path that serves
|
|
60
|
-
// every playlist, init and segment, and the figures behind it move every few
|
|
61
|
-
// seconds — so the line is written when the ANSWER changes, not when it is
|
|
62
|
-
// asked again.
|
|
63
|
-
#lastOfferLine = "";
|
|
64
|
-
|
|
65
|
-
#liveOutputs;
|
|
66
|
-
#host;
|
|
67
|
-
#audioCostKey;
|
|
68
|
-
#runningEncoders;
|
|
69
|
-
#encodersRunningNow;
|
|
70
|
-
#torrentCostSecFor;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {{
|
|
74
|
-
* liveOutputs: import("../output/LiveOutputs.js").LiveOutputs,
|
|
75
|
-
* host: () => { benchmark: object[] | null, decodeModel: object | null, contentionPenalties: object | null, availability: { known: boolean, share: number } | null },
|
|
76
|
-
* audioCostKey: (session: object) => string,
|
|
77
|
-
* runningEncoders: () => number,
|
|
78
|
-
* encodersRunningNow: () => number,
|
|
79
|
-
* torrentCostSecFor: (session: object) => number
|
|
80
|
-
* }} deps
|
|
81
|
-
*/
|
|
82
|
-
constructor({ liveOutputs, host, audioCostKey, runningEncoders, encodersRunningNow, torrentCostSecFor }) {
|
|
83
|
-
this.#liveOutputs = liveOutputs;
|
|
84
|
-
// Asked at the moment of the question, not copied: the share of the machine
|
|
85
|
-
// that is free is re-read every few seconds, and a copy taken when this was
|
|
86
|
-
// built would price every later rung against a machine that has gone.
|
|
87
|
-
this.#host = host;
|
|
88
|
-
this.#audioCostKey = audioCostKey;
|
|
89
|
-
this.#runningEncoders = runningEncoders;
|
|
90
|
-
this.#encodersRunningNow = encodersRunningNow;
|
|
91
|
-
this.#torrentCostSecFor = torrentCostSecFor;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* What a running re-encode of the picture costs, in seconds of work per
|
|
96
|
-
* second of video.
|
|
97
|
-
*
|
|
98
|
-
* Measured first: `lastAloneSpeed` is what this very rung did with the
|
|
99
|
-
* machine to itself. Failing that, the encode model that decides every rung —
|
|
100
|
-
* the same benchmark, the same decode term — applied to this rung's own pixel
|
|
101
|
-
* rate. There is no third answer: a rung whose cost cannot be derived at all
|
|
102
|
-
* contributes nothing rather than a number somebody invented.
|
|
103
|
-
*
|
|
104
|
-
* @param {HlsSession} session
|
|
105
|
-
* @returns {number}
|
|
106
|
-
*/
|
|
107
|
-
#pictureCostOf(session) {
|
|
108
|
-
if (Number.isFinite(session.lastAloneSpeed) && session.lastAloneSpeed > 0) {
|
|
109
|
-
return 1 / session.lastAloneSpeed;
|
|
110
|
-
}
|
|
111
|
-
const benchmark = this.#host().benchmark;
|
|
112
|
-
const width = Number(session.output.encodeWidth) || 0;
|
|
113
|
-
const height = Number(session.output.encodeHeight) || 0;
|
|
114
|
-
const fps = Number(session.output.outputFps) || TRANSCODE_FPS;
|
|
115
|
-
if (!Array.isArray(benchmark) || benchmark.length === 0 || width <= 0 || height <= 0) {
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
const { speed } = canSustainOutput({
|
|
119
|
-
benchmark,
|
|
120
|
-
decodeModel: this.#host().decodeModel,
|
|
121
|
-
source: session.file.decode ?? null,
|
|
122
|
-
outputPixelsPerSec: width * height * fps,
|
|
123
|
-
observedDecodeCostSec: null,
|
|
124
|
-
concurrentCostSec: 0
|
|
125
|
-
});
|
|
126
|
-
return Number.isFinite(speed) && speed > 0 ? 1 / speed : 0;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (member
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
this.#
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file What encoding this file costs THIS machine, and which heights follow.
|
|
3
|
+
*
|
|
4
|
+
* One subject: seconds of work per second of video. Everything here is that
|
|
5
|
+
* question asked about something — a picture being re-encoded, a soundtrack, a
|
|
6
|
+
* copy, everything running beside the rung being judged — and the last method
|
|
7
|
+
* turns the answers into the list of heights the machine can hold.
|
|
8
|
+
*
|
|
9
|
+
* It decides what a viewer is offered, so being wrong in either direction costs
|
|
10
|
+
* them: too generous and they are given a rung that runs below realtime, which
|
|
11
|
+
* is a slideshow; too mean and they are refused quality the host could hold.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is chosen. Every figure is a measurement — the startup
|
|
14
|
+
* benchmarks, what an encoder has since been seen doing on this very file, what
|
|
15
|
+
* a second job costs on this host, what share of the machine is free — and where
|
|
16
|
+
* a term has not been measured it contributes nothing rather than a guess.
|
|
17
|
+
*
|
|
18
|
+
* What it is given, and why each is passed rather than reached for: which
|
|
19
|
+
* sessions belong to one file (`liveOutputs`), the host's own readings, which
|
|
20
|
+
* key names a soundtrack, how many encoders are running, and what the file costs
|
|
21
|
+
* merely by being fetched. The learned costs it holds itself: they are what an
|
|
22
|
+
* encoder taught it, and it is their only reader.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { correctForAvailability } from "../available-share.js";
|
|
26
|
+
import { contentionPenalty } from "../contention.js";
|
|
27
|
+
import { TRANSCODE_FPS } from "../encode/args.js";
|
|
28
|
+
import { processCanBeSignalled, runStateOf } from "../encode/encode-run-state.js";
|
|
29
|
+
import { canSustainOutput, speedBar } from "../hwaccel.js";
|
|
30
|
+
import { logger } from "../../utils/logger.js";
|
|
31
|
+
|
|
32
|
+
export class EncodeCost {
|
|
33
|
+
/**
|
|
34
|
+
* What a soundtrack encoder has been seen to cost, by the key naming that
|
|
35
|
+
* track. Written by whoever learns from a run; read here and nowhere else.
|
|
36
|
+
*
|
|
37
|
+
* Public for now because the three that learn are still methods of the
|
|
38
|
+
* session manager. Moving them here is the next step, and until then two
|
|
39
|
+
* objects must not each keep a copy of the same reading.
|
|
40
|
+
*
|
|
41
|
+
* @type {Map<string, { costSec: number, readings: number[], version: number }>}
|
|
42
|
+
*/
|
|
43
|
+
audioCost = new Map();
|
|
44
|
+
|
|
45
|
+
/** What copying a file's picture has been seen to cost. @type {Map<string, { costSec: number, readings: number[], version: number }>} */
|
|
46
|
+
copyCost = new Map();
|
|
47
|
+
|
|
48
|
+
/** What decoding a file has been seen to cost. @type {Map<string, { costSec: number, readings: number[], version: number }>} */
|
|
49
|
+
decodeCost = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What each height was last predicted to do, kept so a session started at
|
|
53
|
+
* that height can be compared against the prediction once it runs.
|
|
54
|
+
*
|
|
55
|
+
* @type {Map<number, number | null> | null}
|
|
56
|
+
*/
|
|
57
|
+
lastPredictedByHeight = null;
|
|
58
|
+
|
|
59
|
+
// The last refusal printed. The offer is recomputed on the path that serves
|
|
60
|
+
// every playlist, init and segment, and the figures behind it move every few
|
|
61
|
+
// seconds — so the line is written when the ANSWER changes, not when it is
|
|
62
|
+
// asked again.
|
|
63
|
+
#lastOfferLine = "";
|
|
64
|
+
|
|
65
|
+
#liveOutputs;
|
|
66
|
+
#host;
|
|
67
|
+
#audioCostKey;
|
|
68
|
+
#runningEncoders;
|
|
69
|
+
#encodersRunningNow;
|
|
70
|
+
#torrentCostSecFor;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {{
|
|
74
|
+
* liveOutputs: import("../output/LiveOutputs.js").LiveOutputs,
|
|
75
|
+
* host: () => { benchmark: object[] | null, decodeModel: object | null, contentionPenalties: object | null, copySpeedX: number | null, availability: { known: boolean, share: number } | null },
|
|
76
|
+
* audioCostKey: (session: object) => string,
|
|
77
|
+
* runningEncoders: () => number,
|
|
78
|
+
* encodersRunningNow: () => number,
|
|
79
|
+
* torrentCostSecFor: (session: object) => number
|
|
80
|
+
* }} deps
|
|
81
|
+
*/
|
|
82
|
+
constructor({ liveOutputs, host, audioCostKey, runningEncoders, encodersRunningNow, torrentCostSecFor }) {
|
|
83
|
+
this.#liveOutputs = liveOutputs;
|
|
84
|
+
// Asked at the moment of the question, not copied: the share of the machine
|
|
85
|
+
// that is free is re-read every few seconds, and a copy taken when this was
|
|
86
|
+
// built would price every later rung against a machine that has gone.
|
|
87
|
+
this.#host = host;
|
|
88
|
+
this.#audioCostKey = audioCostKey;
|
|
89
|
+
this.#runningEncoders = runningEncoders;
|
|
90
|
+
this.#encodersRunningNow = encodersRunningNow;
|
|
91
|
+
this.#torrentCostSecFor = torrentCostSecFor;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What a running re-encode of the picture costs, in seconds of work per
|
|
96
|
+
* second of video.
|
|
97
|
+
*
|
|
98
|
+
* Measured first: `lastAloneSpeed` is what this very rung did with the
|
|
99
|
+
* machine to itself. Failing that, the encode model that decides every rung —
|
|
100
|
+
* the same benchmark, the same decode term — applied to this rung's own pixel
|
|
101
|
+
* rate. There is no third answer: a rung whose cost cannot be derived at all
|
|
102
|
+
* contributes nothing rather than a number somebody invented.
|
|
103
|
+
*
|
|
104
|
+
* @param {HlsSession} session
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
#pictureCostOf(session) {
|
|
108
|
+
if (Number.isFinite(session.lastAloneSpeed) && session.lastAloneSpeed > 0) {
|
|
109
|
+
return 1 / session.lastAloneSpeed;
|
|
110
|
+
}
|
|
111
|
+
const benchmark = this.#host().benchmark;
|
|
112
|
+
const width = Number(session.output.encodeWidth) || 0;
|
|
113
|
+
const height = Number(session.output.encodeHeight) || 0;
|
|
114
|
+
const fps = Number(session.output.outputFps) || TRANSCODE_FPS;
|
|
115
|
+
if (!Array.isArray(benchmark) || benchmark.length === 0 || width <= 0 || height <= 0) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const { speed } = canSustainOutput({
|
|
119
|
+
benchmark,
|
|
120
|
+
decodeModel: this.#host().decodeModel,
|
|
121
|
+
source: session.file.decode ?? null,
|
|
122
|
+
outputPixelsPerSec: width * height * fps,
|
|
123
|
+
observedDecodeCostSec: null,
|
|
124
|
+
concurrentCostSec: 0
|
|
125
|
+
});
|
|
126
|
+
return Number.isFinite(speed) && speed > 0 ? 1 / speed : 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How fast this machine produces ONE output, in seconds of film per second.
|
|
131
|
+
*
|
|
132
|
+
* THERE IS ALWAYS AN ANSWER, and that is the point of this method. Every
|
|
133
|
+
* decision in the encoding layer is made from arrivals — when would this
|
|
134
|
+
* encoder reach that piece — and an arrival cannot be computed without a
|
|
135
|
+
* speed. A speed that is missing is therefore not a smaller answer, it is no
|
|
136
|
+
* answer at all: the plan then cannot tell a viewer who will be served from
|
|
137
|
+
* one who will be left waiting, and the moment it happens is the cold start,
|
|
138
|
+
* which is when the question matters most.
|
|
139
|
+
*
|
|
140
|
+
* Three sources, most specific first, and every one of them measured:
|
|
141
|
+
*
|
|
142
|
+
* 1. what a run on THIS output has been seen doing. It is this machine, this
|
|
143
|
+
* material and these settings, so nothing beats it;
|
|
144
|
+
* 2. the startup benchmark, for an output whose picture is re-encoded: the
|
|
145
|
+
* preset readings and the decode model, applied to this output's own pixel
|
|
146
|
+
* rate. It exists before any viewer;
|
|
147
|
+
* 3. the startup copy measurement, for an output whose picture is copied.
|
|
148
|
+
* Copying neither decodes nor encodes, so neither of the above describes
|
|
149
|
+
* it, and until it was measured this branch had no figure at all.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} address - The output, as the encoding layer names it.
|
|
152
|
+
* @returns {number} Seconds of film per second. Zero only where the host
|
|
153
|
+
* measured nothing at all, which is a broken startup rather than a state to
|
|
154
|
+
* plan around.
|
|
155
|
+
*/
|
|
156
|
+
speedForOutput(address) {
|
|
157
|
+
const sessions = this.#liveOutputs.sessionsOn(address);
|
|
158
|
+
let measured = 0;
|
|
159
|
+
for (const session of sessions) {
|
|
160
|
+
const speed = Number(session.lastAloneSpeed);
|
|
161
|
+
if (Number.isFinite(speed) && speed > measured) {
|
|
162
|
+
measured = speed;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (measured > 0) {
|
|
166
|
+
return measured;
|
|
167
|
+
}
|
|
168
|
+
for (const session of sessions) {
|
|
169
|
+
if (session.transcodeVideo === true) {
|
|
170
|
+
const cost = this.#pictureCostOf(session);
|
|
171
|
+
if (cost > 0) {
|
|
172
|
+
return 1 / cost;
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const copying = Number(this.#host().copySpeedX);
|
|
177
|
+
if (Number.isFinite(copying) && copying > 0) {
|
|
178
|
+
return copying;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* What everything OTHER than this session is costing right now, or null when
|
|
186
|
+
* any of it is unpriced.
|
|
187
|
+
*
|
|
188
|
+
* Used to recover a soundtrack's own share from a reading taken beside the
|
|
189
|
+
* picture — the only kind of reading a rendition ever gives, since it runs
|
|
190
|
+
* exactly as long as the picture does. Refusing to answer when something
|
|
191
|
+
* running has no price is the point: unpriced work would otherwise be
|
|
192
|
+
* attributed to the soundtrack, and an overpriced soundtrack refuses quality
|
|
193
|
+
* steps the host could actually hold.
|
|
194
|
+
*
|
|
195
|
+
* @param {HlsSession} session
|
|
196
|
+
* @returns {number | null}
|
|
197
|
+
*/
|
|
198
|
+
pricedConcurrentCost(session) {
|
|
199
|
+
let cost = 0;
|
|
200
|
+
for (const member of this.#liveOutputs.familyOf(session)) {
|
|
201
|
+
if (member === session || !processCanBeSignalled(runStateOf(member))) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (member.audioOnly === true) {
|
|
205
|
+
const audio = this.audioCost.get(this.#audioCostKey(member));
|
|
206
|
+
if (!audio || !(audio.costSec > 0)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
cost += audio.costSec;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (member.transcodeVideo !== true) {
|
|
213
|
+
const copy = this.copyCost.get(member.file.key);
|
|
214
|
+
if (!copy || !(copy.costSec > 0)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
cost += copy.costSec;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const picture = this.#pictureCostOf(member);
|
|
221
|
+
if (!(picture > 0)) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
cost += picture;
|
|
225
|
+
}
|
|
226
|
+
// Encoders outside this family are counted by number only — there is no
|
|
227
|
+
// price to look up for another film's session — so a reading taken while
|
|
228
|
+
// one is running cannot be attributed either.
|
|
229
|
+
return this.#runningEncoders() > this.#liveOutputs.familyOf(session).filter(
|
|
230
|
+
(member) => processCanBeSignalled(runStateOf(member))
|
|
231
|
+
).length
|
|
232
|
+
? null
|
|
233
|
+
: cost;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* What each height of this family is costing RIGHT NOW, for the heights an
|
|
238
|
+
* encoder is actually running at.
|
|
239
|
+
*
|
|
240
|
+
* Exists so a height can be judged against what the machine spends on
|
|
241
|
+
* everything else — a step being warmed is running while it is judged, and
|
|
242
|
+
* charged its own cost it refuses itself.
|
|
243
|
+
*
|
|
244
|
+
* @param {HlsSession} session
|
|
245
|
+
* @returns {Map<number, number>}
|
|
246
|
+
*/
|
|
247
|
+
runningCostByHeight(session) {
|
|
248
|
+
/** @type {Map<number, number>} */
|
|
249
|
+
const byHeight = new Map();
|
|
250
|
+
for (const member of this.#liveOutputs.familyOf(session)) {
|
|
251
|
+
if (member.audioOnly === true || member.transcodeVideo !== true) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!processCanBeSignalled(runStateOf(member))) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const height = this.#liveOutputs.variantHeightOf(member);
|
|
258
|
+
if (height > 0) {
|
|
259
|
+
byHeight.set(height, (byHeight.get(height) ?? 0) + this.#pictureCostOf(member));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return byHeight;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Seconds of work per second of video this family is ALREADY committed to,
|
|
267
|
+
* beside any rung being considered.
|
|
268
|
+
*
|
|
269
|
+
* Every encoder of the family that is actually running: the picture, whether
|
|
270
|
+
* it is copied or re-encoded, and each audio rendition. The rung the viewer
|
|
271
|
+
* is watching and the source's own copied height are never withdrawn by the
|
|
272
|
+
* caller, so charging for the encoder that serves them cannot strand anyone —
|
|
273
|
+
* what it does is stop the NEXT rung being offered as though the machine were
|
|
274
|
+
* idle, which is what the field disproved on 2026-08-15.
|
|
275
|
+
*
|
|
276
|
+
* Anything whose cost is neither measured nor derivable contributes nothing.
|
|
277
|
+
* A guess here would refuse rungs on arithmetic nobody performed.
|
|
278
|
+
*
|
|
279
|
+
* @param {HlsSession} session
|
|
280
|
+
* @returns {number}
|
|
281
|
+
*/
|
|
282
|
+
committedCostOf(session) {
|
|
283
|
+
let cost = 0;
|
|
284
|
+
for (const member of this.#liveOutputs.familyOf(session)) {
|
|
285
|
+
// Only what still HAS an encoder. A quality step the viewer left keeps
|
|
286
|
+
// its session and its segments but not a process, and it produces nothing
|
|
287
|
+
// for anybody — charging the machine for it would refuse steps on work
|
|
288
|
+
// nobody is doing.
|
|
289
|
+
//
|
|
290
|
+
// A SUSPENDED encoder is charged, deliberately, and this is not the same
|
|
291
|
+
// question. The unit here is seconds of work per second of VIDEO, not per
|
|
292
|
+
// second of wall clock: a copy running at 8x costs 0.125 s/s whether it
|
|
293
|
+
// is producing right now or parked by the look-ahead cap, because over an
|
|
294
|
+
// hour of watching it still produces an hour of video. Suspension is how
|
|
295
|
+
// that cost is spread, not a discount on it — and pricing a parked
|
|
296
|
+
// encoder at zero would offer a step on the strength of a pause that ends
|
|
297
|
+
// the moment the viewer catches up.
|
|
298
|
+
if (!processCanBeSignalled(runStateOf(member))) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (member.audioOnly === true) {
|
|
302
|
+
// A soundtrack encoder, priced from its own measured speed. Nothing is
|
|
303
|
+
// charged for a track nobody has measured: a guess here refuses rungs
|
|
304
|
+
// on arithmetic no one performed.
|
|
305
|
+
const audio = this.audioCost.get(this.#audioCostKey(member));
|
|
306
|
+
cost += audio && audio.costSec > 0 ? audio.costSec : 0;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (member.transcodeVideo !== true) {
|
|
310
|
+
const observed = this.copyCost.get(member.file.key);
|
|
311
|
+
cost += observed && observed.costSec > 0 ? observed.costSec : 0;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// A picture being RE-ENCODED beside the rung being judged — the warm-up
|
|
315
|
+
// that makes a quality switch seamless is two encoders by design, and
|
|
316
|
+
// that overlap is exactly where the field measured 0.504x on a rung
|
|
317
|
+
// predicted at 1.58x (2026-08-15). Priced by what it has been SEEN doing
|
|
318
|
+
// when it had the machine to itself, and otherwise by the same model that
|
|
319
|
+
// judges every rung — which is a prediction, not a guess.
|
|
320
|
+
cost += this.#pictureCostOf(member);
|
|
321
|
+
}
|
|
322
|
+
// And what the FILE costs simply by being fetched and delivered while it is
|
|
323
|
+
// watched: a viewer consumes it at its own byte rate, and every one of
|
|
324
|
+
// those bytes is downloaded, verified and pushed by this process. Priced
|
|
325
|
+
// per megabyte from readings taken while nothing was encoding, so the two
|
|
326
|
+
// measurements do not contain each other.
|
|
327
|
+
cost += this.#torrentCostSecFor(session);
|
|
328
|
+
return cost;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The speed each rung of this family was last seen running at, when it was
|
|
333
|
+
* running alone.
|
|
334
|
+
*
|
|
335
|
+
* A rung that has been watched failing is refused on that evidence; a rung
|
|
336
|
+
* nobody has run says nothing about itself and is judged by the startup
|
|
337
|
+
* measurement like any other.
|
|
338
|
+
*
|
|
339
|
+
* @param {HlsSession} base
|
|
340
|
+
* @returns {Map<number, number>}
|
|
341
|
+
*/
|
|
342
|
+
measuredRungSpeeds(base) {
|
|
343
|
+
/** @type {Map<number, number>} */
|
|
344
|
+
const speeds = new Map();
|
|
345
|
+
for (const session of this.#liveOutputs.familyOf(base)) {
|
|
346
|
+
if (session.transcodeVideo !== true || !Number.isFinite(session.lastAloneSpeed)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const height = this.#liveOutputs.variantHeightOf(session);
|
|
350
|
+
if (height > 0) {
|
|
351
|
+
speeds.set(height, session.lastAloneSpeed);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return speeds;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Drop the rungs this host cannot hold at realtime.
|
|
359
|
+
*
|
|
360
|
+
* Every rung below the source height is a full re-encode — decode the whole
|
|
361
|
+
* source, encode a smaller picture — and on a weak host that is dearer than
|
|
362
|
+
* the copy it replaces. Measured 2026-08-14: 1080p was copied at 7.8-8.9x
|
|
363
|
+
* while the offered 240p rung ran at 0.388-0.947x, its first segment took
|
|
364
|
+
* 30 s and later ones were held 22 s, so choosing a LOWER quality is what
|
|
365
|
+
* broke playback. A rung that cannot be produced faster than it is watched
|
|
366
|
+
* must not be offered at all.
|
|
367
|
+
*
|
|
368
|
+
* The session's OWN height always stays: an encoder is already producing it,
|
|
369
|
+
* and removing it would point the player at a rung nobody is encoding.
|
|
370
|
+
*
|
|
371
|
+
* @param {{ heights: number[], ownHeight: number, sourceWidth: number, sourceHeight: number, fps: number, source: { megapixelsPerSecond: number, megabitsPerSecond: number } | null, transcodeVideo: boolean }} params
|
|
372
|
+
* @returns {number[]}
|
|
373
|
+
*/
|
|
374
|
+
sustainableHeights({
|
|
375
|
+
heights,
|
|
376
|
+
ownHeight,
|
|
377
|
+
// Every height a viewer has on screen, not one: two viewers of one picture
|
|
378
|
+
// can be on two rungs, and a rung is never withdrawn while somebody is
|
|
379
|
+
// watching it — their next segment would 404 on a stream that is playing.
|
|
380
|
+
playingHeights = new Set(),
|
|
381
|
+
sourceWidth,
|
|
382
|
+
sourceHeight,
|
|
383
|
+
fps,
|
|
384
|
+
source,
|
|
385
|
+
transcodeVideo,
|
|
386
|
+
observedDecodeCostSec = null,
|
|
387
|
+
concurrentCostSec = 0,
|
|
388
|
+
runningCostByHeight = null,
|
|
389
|
+
measuredHeights = null,
|
|
390
|
+
requiredSpeed = null
|
|
391
|
+
}) {
|
|
392
|
+
// What this file's own supply demands, measured by its reader — and
|
|
393
|
+
// realtime while it has not been measured. Read once here so the line that
|
|
394
|
+
// reports a refusal names the figure it refused against.
|
|
395
|
+
const bar = speedBar(requiredSpeed);
|
|
396
|
+
const benchmark = this.#host().benchmark;
|
|
397
|
+
if (!Array.isArray(benchmark) || benchmark.length === 0 || sourceHeight <= 0 || sourceWidth <= 0) {
|
|
398
|
+
// Nothing to predict WITH, so nothing is predicted. What has been SEEN
|
|
399
|
+
// still counts: a rung measured running below realtime is withdrawn here
|
|
400
|
+
// too, because the evidence for it does not come from the benchmark. This
|
|
401
|
+
// return used to hand back every height including one measured at 0.4x —
|
|
402
|
+
// found by a check written when this moved out of the session manager,
|
|
403
|
+
// 2026-09-05.
|
|
404
|
+
// The rung on screen is not exempt, for the same reason it is not exempt
|
|
405
|
+
// below: keeping one measured at 0.007x stalls the viewer with no path to
|
|
406
|
+
// a faster rung, which is what the field showed on 2026-08-31.
|
|
407
|
+
return heights.filter((height) => {
|
|
408
|
+
const measured = measuredHeights?.get(height) ?? null;
|
|
409
|
+
return measured === null || measured >= 1;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/** @type {number[]} */
|
|
413
|
+
const kept = [];
|
|
414
|
+
/** @type {string[]} */
|
|
415
|
+
const dropped = [];
|
|
416
|
+
// What each height was predicted to do on THIS machine, kept so a session
|
|
417
|
+
// started at that height can be compared against it once it runs. The
|
|
418
|
+
// manager holds the last answer, because the offer is computed on the path
|
|
419
|
+
// that serves every request while a session is created elsewhere.
|
|
420
|
+
/** @type {Map<number, number | null>} */
|
|
421
|
+
const predictedByHeight = new Map();
|
|
422
|
+
for (const height of heights) {
|
|
423
|
+
// A rung this session has actually been seen running below realtime is
|
|
424
|
+
// withdrawn on that evidence, whatever the prediction says. This is the
|
|
425
|
+
// one thing a live reading is authority on: itself. It is asked before
|
|
426
|
+
// any exemption so a rung measured failing while on screen does not stay
|
|
427
|
+
// offered because it was on screen when measured — otherwise a step
|
|
428
|
+
// would ask for the one rung this machine has been measured failing at,
|
|
429
|
+
// then fail again, then step down, for ever. A copied source height
|
|
430
|
+
// cannot reach this: `#measuredRungSpeeds` records only sessions that
|
|
431
|
+
// re-encode, so a copy has no reading to be withdrawn on, which is right
|
|
432
|
+
// — it costs no encoder.
|
|
433
|
+
const measured = measuredHeights?.get(height) ?? null;
|
|
434
|
+
if (measured !== null && measured < 1) {
|
|
435
|
+
// Even the rung on screen is withdrawn on measured failure: keeping it
|
|
436
|
+
// would 404 the next segment, but keeping a rung measured at 0.007x
|
|
437
|
+
// (field 2026-08-31, 4K HEVC on CM4) stalls the viewer for minutes with
|
|
438
|
+
// 0.04s buffered and no way to downgrade because every other rung is
|
|
439
|
+
// also dropped. Withdrawing it lets the offer become empty, which the
|
|
440
|
+
// caller turns into an error the viewer can act on (try another proxy
|
|
441
|
+
// or a lower source) instead of an endless spinner.
|
|
442
|
+
dropped.push(`${height}p=${measured.toFixed(2)}x measured`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// The rung ON SCREEN is kept only when it has not been measured failing
|
|
446
|
+
// above. Keeping a rung measured at 0.007x would stall the viewer with
|
|
447
|
+
// no path to a faster rung, which is what the field showed.
|
|
448
|
+
if (playingHeights.has(height)) {
|
|
449
|
+
kept.push(height);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
// The height an encoder is ALREADY producing, and the source's own height
|
|
453
|
+
// when the FAMILY serves it by copy — neither has to be predicted,
|
|
454
|
+
// because it is happening. A copied rung costs no encoder at all, so no
|
|
455
|
+
// measurement of this host can ever be a reason to withdraw it, and the
|
|
456
|
+
// whole point of it is that it is where a viewer on a rung the machine
|
|
457
|
+
// cannot hold goes back to. `transcodeVideo` here is the base's, not the
|
|
458
|
+
// asking session's: a 240p rung re-encodes, and reading its own flag is
|
|
459
|
+
// what withdrew a copied 1080p in the field on 2026-08-15.
|
|
460
|
+
//
|
|
461
|
+
// A source height that would have to be RE-ENCODED is a prediction like
|
|
462
|
+
// any other: on a session whose budget stepped down to 480p, the source's
|
|
463
|
+
// 1080p is neither copied nor being produced, and keeping it unpriced
|
|
464
|
+
// would offer exactly the kind of rung this refuses. Likewise, a rung
|
|
465
|
+
// this session is already producing at 0.007x (field 2026-08-31, 4K HEVC
|
|
466
|
+
// on CM4, 0.1x at 23:45 and 0.007x at 06:57) is not sustainable just
|
|
467
|
+
// because it is running — keeping it offered no path to a faster rung
|
|
468
|
+
// and left the viewer at 0.04s buffered with no downgrade.
|
|
469
|
+
if (
|
|
470
|
+
(height === ownHeight && !transcodeVideo) ||
|
|
471
|
+
(height === sourceHeight && !transcodeVideo)
|
|
472
|
+
) {
|
|
473
|
+
kept.push(height);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const width = Math.round(((sourceWidth / sourceHeight) * height) / 2) * 2;
|
|
477
|
+
// What the machine is spending on everything EXCEPT this height. A step
|
|
478
|
+
// being warmed for a switch is already running while it is judged, so its
|
|
479
|
+
// own cost is inside the committed total — and charged against itself it
|
|
480
|
+
// is counted twice. Measured against the field figures of 2026-08-15
|
|
481
|
+
// that is 1.83x against 1.03x: below the margin, so the step the viewer
|
|
482
|
+
// had just asked for was dropped from the offer by the act of warming it,
|
|
483
|
+
// and its next segment answered 404 on a stream that was playing.
|
|
484
|
+
const concurrentBesideThis = Math.max(
|
|
485
|
+
0,
|
|
486
|
+
concurrentCostSec - (runningCostByHeight?.get(height) ?? 0)
|
|
487
|
+
);
|
|
488
|
+
const { speed } = canSustainOutput({
|
|
489
|
+
benchmark,
|
|
490
|
+
decodeModel: this.#host().decodeModel,
|
|
491
|
+
source,
|
|
492
|
+
outputPixelsPerSec: width * height * fps,
|
|
493
|
+
observedDecodeCostSec,
|
|
494
|
+
concurrentCostSec: concurrentBesideThis
|
|
495
|
+
});
|
|
496
|
+
// The benchmark behind that figure was taken on a QUIET host — one
|
|
497
|
+
// ffmpeg and nothing else. The machine a step will actually run on is
|
|
498
|
+
// also running the kernel, the container and whatever else its owner
|
|
499
|
+
// does, and on the addon host that was measured at 99 % busy with a
|
|
500
|
+
// quarter of it unattributed. Only the unattributed part is charged
|
|
501
|
+
// here: our own encoders are already in `concurrentBesideThis` and the
|
|
502
|
+
// proxy's own work is already priced per megabyte moved.
|
|
503
|
+
// Two corrections, and they are different facts about the machine. The
|
|
504
|
+
// availability share removes work nobody has been charged for; the
|
|
505
|
+
// contention penalty says what OUR OWN second job costs, because the
|
|
506
|
+
// budget adds independent prices and this host does not behave that way
|
|
507
|
+
// — the same work measured 2.6× dearer beside one encoder and 3.7×
|
|
508
|
+
// beside two (2026-08-18). `concurrentBesideThis` already counts what is
|
|
509
|
+
// committed; this multiplies by how badly running at all together goes.
|
|
510
|
+
const othersRunning = concurrentBesideThis > 0 ? this.#encodersRunningNow() : 0;
|
|
511
|
+
const { penalty } = contentionPenalty(othersRunning, this.#host().contentionPenalties);
|
|
512
|
+
const onThisMachine = correctForAvailability(
|
|
513
|
+
speed === null ? null : speed / penalty,
|
|
514
|
+
this.#host().availability
|
|
515
|
+
);
|
|
516
|
+
// Kept against the step's own session, so that when it runs the field
|
|
517
|
+
// says what the prediction was worth. Without this the only comparison
|
|
518
|
+
// available is between two figures written minutes apart in different
|
|
519
|
+
// lines of the log.
|
|
520
|
+
predictedByHeight.set(height, onThisMachine);
|
|
521
|
+
if (onThisMachine !== null && onThisMachine >= bar) {
|
|
522
|
+
kept.push(height);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
dropped.push(`${height}p=${onThisMachine === null ? "n/a" : `${onThisMachine.toFixed(2)}x`}`);
|
|
526
|
+
}
|
|
527
|
+
// Written when the ANSWER changes, not when the answer is recomputed. This
|
|
528
|
+
// is asked on the path that serves every playlist, init and segment, and
|
|
529
|
+
// the figures behind it move every five seconds — so an unconditional line
|
|
530
|
+
// here is roughly seven hundred identical lines an hour into a forwarder
|
|
531
|
+
// that holds five hundred, which buries whatever is worth reading.
|
|
532
|
+
if (dropped.length > 0) {
|
|
533
|
+
const line =
|
|
534
|
+
`transcode: not offering ${dropped.join(" ")} — below ${bar.toFixed(2)}x ` +
|
|
535
|
+
(Number.isFinite(requiredSpeed) && requiredSpeed > 1
|
|
536
|
+
? "(the speed this file's own interruptions demand) "
|
|
537
|
+
: "(realtime, this file's supply not measured yet) ") +
|
|
538
|
+
// Said with the figures, because a step refused on a busy machine and
|
|
539
|
+
// one refused on an idle machine are different facts about the host.
|
|
540
|
+
(this.#host().availability?.known
|
|
541
|
+
? `on a machine with ${Math.round(this.#host().availability.share * 100)}% to spare `
|
|
542
|
+
: "") +
|
|
543
|
+
`(offering ${kept.map((height) => `${height}p`).join(" ")})`;
|
|
544
|
+
if (line !== this.#lastOfferLine) {
|
|
545
|
+
this.#lastOfferLine = line;
|
|
546
|
+
logger.info(line);
|
|
547
|
+
}
|
|
548
|
+
this.lastPredictedByHeight = predictedByHeight;
|
|
549
|
+
} else {
|
|
550
|
+
this.lastPredictedByHeight = predictedByHeight;
|
|
551
|
+
this.#lastOfferLine = "";
|
|
552
|
+
}
|
|
553
|
+
return kept;
|
|
554
|
+
}
|
|
555
|
+
}
|