@torrent-tv/proxy 2.76.4 → 2.76.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 +11 -0
- package/bin/cli.js +14 -2
- package/biome.json +32 -0
- package/package.json +1 -1
- package/server.js +5 -1
- package/services/encode/encode-run-state.js +67 -0
- package/services/hls-session-manager.js +77 -539
- package/services/quality/EncodeCost.js +500 -0
- package/services/torrent-worker/client.js +13 -0
- package/services/torrent-worker/pool-adapter.js +384 -370
- package/services/torrent-worker/protocol.js +2 -0
- package/services/torrent-worker/worker.js +23 -0
- package/services/tunnel-client.js +18 -2
- package/test/encode-cost.test.js +94 -0
|
@@ -20,7 +20,7 @@ import { logger } from "../utils/logger.js";
|
|
|
20
20
|
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
21
21
|
import { readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "./host-load.js";
|
|
22
22
|
import { speedFromReadings } from "./encoder-readings.js";
|
|
23
|
-
import { availableShareFrom
|
|
23
|
+
import { availableShareFrom } from "./available-share.js";
|
|
24
24
|
import { contentionPenalty } from "./contention.js";
|
|
25
25
|
import { minimumBufferFrom } from "./supply-margin.js";
|
|
26
26
|
import { baseDrawFrom, costPerMegabyteFrom } from "./torrent-cost.js";
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
ENCODE_RUN_EVENT,
|
|
30
30
|
ENCODE_RUN_STATE,
|
|
31
31
|
INITIAL_RUN_STATE,
|
|
32
|
+
liveRunsOf,
|
|
33
|
+
runStateOf,
|
|
32
34
|
processCanBeSignalled,
|
|
33
35
|
wireState
|
|
34
36
|
} from "./encode/encode-run-state.js";
|
|
@@ -42,7 +44,6 @@ import {
|
|
|
42
44
|
chooseSoftwareEncodeSettings,
|
|
43
45
|
pickSoftwarePreset,
|
|
44
46
|
canSustainOutput,
|
|
45
|
-
speedBar,
|
|
46
47
|
maxrateKbpsFor,
|
|
47
48
|
nominalKbpsForHeight,
|
|
48
49
|
nominalKbpsForMaxrate,
|
|
@@ -68,6 +69,7 @@ import { masterPlaylistText, mediaPlaylistText, segmentIndexForTime } from "./ou
|
|
|
68
69
|
import { SourceFiles, sourceDecodeCharacteristics } from "./source/SourceFile.js";
|
|
69
70
|
import { ProducedIndex } from "./produced-index.js";
|
|
70
71
|
import { SegmentStore } from "./encode/SegmentStore.js";
|
|
72
|
+
import { EncodeCost } from "./quality/EncodeCost.js";
|
|
71
73
|
import { endOfRun } from "./encode/EncodeRun.js";
|
|
72
74
|
import {
|
|
73
75
|
buildRunCommand,
|
|
@@ -1339,72 +1341,6 @@ export function describeGridDrift(published, live) {
|
|
|
1339
1341
|
|
|
1340
1342
|
|
|
1341
1343
|
|
|
1342
|
-
/**
|
|
1343
|
-
* Every run this session has going, earliest first.
|
|
1344
|
-
*
|
|
1345
|
-
* A session holds as many as the machine affords, because one output can be
|
|
1346
|
-
* watched from more than one place: a viewer who jumps back gets a run of their
|
|
1347
|
-
* own rather than dragging the picture away from a viewer watching ahead.
|
|
1348
|
-
*
|
|
1349
|
-
* @param {{ runs?: Set<object> }} session
|
|
1350
|
-
* @returns {object[]}
|
|
1351
|
-
*/
|
|
1352
|
-
function runsOf(session) {
|
|
1353
|
-
const runs = session?.runs instanceof Set ? [...session.runs] : [];
|
|
1354
|
-
return runs.sort((left, right) => left.from - right.from);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
/**
|
|
1358
|
-
* The runs of this session that still have a process.
|
|
1359
|
-
*
|
|
1360
|
-
* @param {{ runs?: Set<object> }} session
|
|
1361
|
-
* @returns {object[]}
|
|
1362
|
-
*/
|
|
1363
|
-
function liveRunsOf(session) {
|
|
1364
|
-
return runsOf(session).filter((run) => run.isAlive);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
/**
|
|
1368
|
-
* What this session's encoding is doing, as one state.
|
|
1369
|
-
*
|
|
1370
|
-
* The most advanced state among its live runs: a session with anything
|
|
1371
|
-
* producing IS producing, whatever else it has going. A session that has never
|
|
1372
|
-
* started a run, or whose runs have all ended, is at the table's own initial
|
|
1373
|
-
* state — which is what "nothing is encoding" means.
|
|
1374
|
-
*
|
|
1375
|
-
* @param {{ runs?: Set<object> }} session
|
|
1376
|
-
* @returns {string}
|
|
1377
|
-
*/
|
|
1378
|
-
function runStateOf(session) {
|
|
1379
|
-
let answer = INITIAL_RUN_STATE;
|
|
1380
|
-
let rank = -1;
|
|
1381
|
-
for (const run of runsOf(session)) {
|
|
1382
|
-
const at = RUN_STATE_ORDER.indexOf(run.state);
|
|
1383
|
-
if (at > rank) {
|
|
1384
|
-
rank = at;
|
|
1385
|
-
answer = run.state;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
return answer;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
/**
|
|
1392
|
-
* How advanced a run state is, for reducing several runs to one answer.
|
|
1393
|
-
*
|
|
1394
|
-
* Producing outranks starting, and both outrank a run that has ended: what a
|
|
1395
|
-
* caller asking "what is this session's encoding doing" wants to know is
|
|
1396
|
-
* whether anything is being made, not what the quietest of them is up to.
|
|
1397
|
-
*/
|
|
1398
|
-
const RUN_STATE_ORDER = [
|
|
1399
|
-
ENCODE_RUN_STATE.ENDED_FAILED,
|
|
1400
|
-
ENCODE_RUN_STATE.ENDED_COMPLETE,
|
|
1401
|
-
ENCODE_RUN_STATE.STOPPED,
|
|
1402
|
-
ENCODE_RUN_STATE.IDLE,
|
|
1403
|
-
ENCODE_RUN_STATE.RETRY_WAIT,
|
|
1404
|
-
ENCODE_RUN_STATE.SUSPENDED,
|
|
1405
|
-
ENCODE_RUN_STATE.STARTING,
|
|
1406
|
-
ENCODE_RUN_STATE.PRODUCING
|
|
1407
|
-
];
|
|
1408
1344
|
|
|
1409
1345
|
/**
|
|
1410
1346
|
* Whether this output is cut at times we hand the muxer, rather than at a
|
|
@@ -1570,51 +1506,12 @@ export class HlsSessionManager {
|
|
|
1570
1506
|
*/
|
|
1571
1507
|
#sessionCreateLatencies = [];
|
|
1572
1508
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
* is not, so their model is a first approximation. This is the file itself,
|
|
1580
|
-
* measured by the encoder that is running on it, and it replaces the model
|
|
1581
|
-
* for that file as soon as it exists. Held for the life of the process: it
|
|
1582
|
-
* describes a source, and the same source is commonly opened again.
|
|
1583
|
-
*
|
|
1584
|
-
* @type {Map<string, { costSec: number, version: number }>}
|
|
1585
|
-
*/
|
|
1586
|
-
#observedDecodeCost = new Map();
|
|
1587
|
-
/**
|
|
1588
|
-
* What copying costs, per source file, learned the same way: `key ->
|
|
1589
|
-
* { costSec, readings, version }`. A copy is what runs BESIDE a rung being
|
|
1590
|
-
* warmed, and pricing it at nothing is what let a host be told it had a whole
|
|
1591
|
-
* machine for the rung.
|
|
1592
|
-
*
|
|
1593
|
-
* @type {Map<string, { costSec: number, readings: number[], version: number }>}
|
|
1594
|
-
*/
|
|
1595
|
-
#observedCopyCost = new Map();
|
|
1596
|
-
|
|
1597
|
-
/**
|
|
1598
|
-
* What encoding one audio TRACK of one file costs this host, in seconds of
|
|
1599
|
-
* work per second of video. Keyed by source, file and track, because two
|
|
1600
|
-
* tracks of one film are not the same encode: a 5.1 AC-3 dub and a stereo
|
|
1601
|
-
* AAC original decode and mix differently.
|
|
1602
|
-
*
|
|
1603
|
-
* Measured exactly as the copy's price is — from an audio-only session's own
|
|
1604
|
-
* reported speed, past its start, alone on the machine, and never while the
|
|
1605
|
-
* torrent is what is short.
|
|
1606
|
-
*
|
|
1607
|
-
* @type {Map<string, { costSec: number, readings: number[], version: number }>}
|
|
1608
|
-
*/
|
|
1609
|
-
#observedAudioCost = new Map();
|
|
1610
|
-
|
|
1611
|
-
/**
|
|
1612
|
-
* The last "not offering" line written, so the same one is not written again.
|
|
1613
|
-
* See the end of {@link HlsSessionManager##sustainableHeights}.
|
|
1614
|
-
*
|
|
1615
|
-
* @type {string}
|
|
1616
|
-
*/
|
|
1617
|
-
#lastOfferLine = "";
|
|
1509
|
+
// What an encoder taught this host — the cost of decoding a file, of
|
|
1510
|
+
// copying its picture, of each of its soundtracks — is held by the object
|
|
1511
|
+
// that reads it (`quality/EncodeCost.js`), together with the last refusal
|
|
1512
|
+
// it printed. The three methods that LEARN those costs are still here and
|
|
1513
|
+
// write into it; moving them is the next step, and until then there must
|
|
1514
|
+
// not be two copies of one reading.
|
|
1618
1515
|
/** The previous reading of the machine, to compare the next one against. */
|
|
1619
1516
|
#hostLoadSample = null;
|
|
1620
1517
|
/** The previous reading taken while nothing was encoding, for the torrent's own cost. */
|
|
@@ -1803,6 +1700,24 @@ export class HlsSessionManager {
|
|
|
1803
1700
|
// What encoders there should be on each output, and where. Given the two
|
|
1804
1701
|
// things only this class can answer: how many this machine can afford, and
|
|
1805
1702
|
// how to make one.
|
|
1703
|
+
// What encoding costs this machine, and which heights follow from it. Given
|
|
1704
|
+
// what it cannot work out for itself: which sessions belong to one file, the
|
|
1705
|
+
// host's own readings AT THE MOMENT OF THE QUESTION rather than copied, how
|
|
1706
|
+
// a soundtrack is keyed, how many encoders are running, and what a file
|
|
1707
|
+
// costs merely by being fetched.
|
|
1708
|
+
this.encodeCost = new EncodeCost({
|
|
1709
|
+
liveOutputs: this.liveOutputs,
|
|
1710
|
+
host: () => ({
|
|
1711
|
+
benchmark: this.softwarePresetBenchmark,
|
|
1712
|
+
decodeModel: this.decodeCostModel,
|
|
1713
|
+
contentionPenalties: this.contentionPenalties,
|
|
1714
|
+
availability: this.hostAvailability
|
|
1715
|
+
}),
|
|
1716
|
+
audioCostKey: (session) => this.#audioCostKey(session),
|
|
1717
|
+
runningEncoders: () => this.#runningEncoders(),
|
|
1718
|
+
encodersRunningNow: () => this.#encodersRunningNow(),
|
|
1719
|
+
torrentCostSecFor: (session) => this.#torrentCostSecFor(session)
|
|
1720
|
+
});
|
|
1806
1721
|
this.encodeOrchestrator = new EncodeOrchestrator({
|
|
1807
1722
|
maxRunsFor: (address) => this.maxRunsForOutput(address),
|
|
1808
1723
|
makeRun: ({ address, from }) => this.#makeRunAt(address, from),
|
|
@@ -2449,7 +2364,7 @@ export class HlsSessionManager {
|
|
|
2449
2364
|
? startAtLadderTop(chosenBudget, outputFps, this.softwarePresetBenchmark, {
|
|
2450
2365
|
decodeModel: this.decodeCostModel,
|
|
2451
2366
|
source: sourceDecode,
|
|
2452
|
-
observedDecodeCostSec: this
|
|
2367
|
+
observedDecodeCostSec: this.encodeCost.decodeCost.get(SourceFiles.keyFor(sourceKey, fileIndex))?.costSec ?? null,
|
|
2453
2368
|
requiredSpeed: this.#requiredSpeedFor(sourceKey, fileIndex)
|
|
2454
2369
|
})
|
|
2455
2370
|
: chosenBudget;
|
|
@@ -2586,7 +2501,7 @@ export class HlsSessionManager {
|
|
|
2586
2501
|
// field can say what the prediction was worth once the step runs. Null
|
|
2587
2502
|
// when the step was never judged — a copied stream needs no encoder and
|
|
2588
2503
|
// is never predicted.
|
|
2589
|
-
predictedSpeedWhenOffered: this.lastPredictedByHeight?.get(output.encodeHeight) ?? null,
|
|
2504
|
+
predictedSpeedWhenOffered: this.encodeCost.lastPredictedByHeight?.get(output.encodeHeight) ?? null,
|
|
2590
2505
|
lastPredictionRatio: null,
|
|
2591
2506
|
// The NAME of this rung, fixed at the height that was asked for. It is
|
|
2592
2507
|
// deliberately not the height being encoded: a viewer who picked 480p on
|
|
@@ -7262,7 +7177,7 @@ export class HlsSessionManager {
|
|
|
7262
7177
|
// outlived the rung: a rung the host cannot hold went on being offered, and
|
|
7263
7178
|
// went on passing every route guard, after the viewer had left it.
|
|
7264
7179
|
// Everything else is fixed for the session's life.
|
|
7265
|
-
const observed = this
|
|
7180
|
+
const observed = this.encodeCost.decodeCost.get(owner.file.key) ?? null;
|
|
7266
7181
|
// Every rung a live viewer has on screen. One answer was enough while a
|
|
7267
7182
|
// picture had one viewer; two of them can be on two rungs, and withdrawing
|
|
7268
7183
|
// either is withdrawing a stream that is playing.
|
|
@@ -7278,14 +7193,14 @@ export class HlsSessionManager {
|
|
|
7278
7193
|
// the menu would keep the answer computed before either was measured — on
|
|
7279
7194
|
// a copied picture, which is the case they exist for, the decode version
|
|
7280
7195
|
// never moves at all, so the cache would never be recomputed.
|
|
7281
|
-
const copyVersion = this
|
|
7196
|
+
const copyVersion = this.encodeCost.copyCost.get(owner.file.key)?.version ?? 0;
|
|
7282
7197
|
const torrentCost = this.#observedTorrentCostPerMegabyte ?? 0;
|
|
7283
7198
|
// The soundtrack's price is an input too, and so is how many encoders of
|
|
7284
7199
|
// this family are running: both move the answer, and an answer cached
|
|
7285
7200
|
// across them is the stale menu this key exists to prevent.
|
|
7286
7201
|
const audioVersion = [...this.liveOutputs.familyOf(owner)]
|
|
7287
7202
|
.filter((member) => member.audioOnly === true)
|
|
7288
|
-
.map((member) => this
|
|
7203
|
+
.map((member) => this.encodeCost.audioCost.get(this.#audioCostKey(member))?.version ?? 0)
|
|
7289
7204
|
.reduce((total, one) => total + one, 0);
|
|
7290
7205
|
const running = [...this.liveOutputs.familyOf(owner)]
|
|
7291
7206
|
.filter((member) => processCanBeSignalled(runStateOf(member))).length;
|
|
@@ -7347,13 +7262,13 @@ export class HlsSessionManager {
|
|
|
7347
7262
|
// the rung that taught the lesson would be the first to be dropped, and
|
|
7348
7263
|
// every route guard reads this list: its next segment would 404 on a stream
|
|
7349
7264
|
// that is playing, with its own encoder still running.
|
|
7350
|
-
const answer = this
|
|
7265
|
+
const answer = this.encodeCost.sustainableHeights({
|
|
7351
7266
|
heights: ordered,
|
|
7352
7267
|
ownHeight: own,
|
|
7353
7268
|
playingHeights,
|
|
7354
7269
|
// What each rung was actually seen doing in this session, which is the
|
|
7355
7270
|
// only thing a live reading may speak for.
|
|
7356
|
-
measuredHeights: this
|
|
7271
|
+
measuredHeights: this.encodeCost.measuredRungSpeeds(owner),
|
|
7357
7272
|
// The speed this file's supply demands, measured by its own reader on
|
|
7358
7273
|
// this swarm. A well-seeded film and a thin one ask different speeds of
|
|
7359
7274
|
// the same machine, so the bar belongs to the pair, not to the host.
|
|
@@ -7361,10 +7276,10 @@ export class HlsSessionManager {
|
|
|
7361
7276
|
// What the family is already spending while a rung is considered. The
|
|
7362
7277
|
// picture being COPIED is the common case and used to be priced at
|
|
7363
7278
|
// nothing; measured, it is about an eighth of the machine.
|
|
7364
|
-
concurrentCostSec: this
|
|
7279
|
+
concurrentCostSec: this.encodeCost.committedCostOf(owner),
|
|
7365
7280
|
// So a height already being produced is not charged for itself when it is
|
|
7366
|
-
// judged. See the subtraction in #sustainableHeights.
|
|
7367
|
-
runningCostByHeight: this
|
|
7281
|
+
// judged. See the subtraction in EncodeCost#sustainableHeights.
|
|
7282
|
+
runningCostByHeight: this.encodeCost.runningCostByHeight(owner),
|
|
7368
7283
|
sourceWidth: Number(owner.file.width) || 0,
|
|
7369
7284
|
sourceHeight: Math.round(Number(owner.file.height) || 0),
|
|
7370
7285
|
fps: Number(owner.output.outputFps) || TRANSCODE_FPS,
|
|
@@ -7404,7 +7319,7 @@ export class HlsSessionManager {
|
|
|
7404
7319
|
* @returns {number | null}
|
|
7405
7320
|
*/
|
|
7406
7321
|
#observedDecodeCostFor(session) {
|
|
7407
|
-
const entry = this
|
|
7322
|
+
const entry = this.encodeCost.decodeCost.get(session.file.key);
|
|
7408
7323
|
return entry ? entry.costSec : null;
|
|
7409
7324
|
}
|
|
7410
7325
|
|
|
@@ -7552,7 +7467,7 @@ export class HlsSessionManager {
|
|
|
7552
7467
|
if (kind !== "audio") {
|
|
7553
7468
|
return;
|
|
7554
7469
|
}
|
|
7555
|
-
const others = this
|
|
7470
|
+
const others = this.encodeCost.pricedConcurrentCost(session);
|
|
7556
7471
|
if (others === null) {
|
|
7557
7472
|
return; // something running has no price; nothing can be attributed
|
|
7558
7473
|
}
|
|
@@ -7641,14 +7556,14 @@ export class HlsSessionManager {
|
|
|
7641
7556
|
return;
|
|
7642
7557
|
}
|
|
7643
7558
|
const key = session.file.key;
|
|
7644
|
-
const known = this
|
|
7559
|
+
const known = this.encodeCost.copyCost.get(key);
|
|
7645
7560
|
const readings = [...(known?.readings ?? []), costSec].slice(-DECODE_LEARNING_READINGS);
|
|
7646
7561
|
const median = medianOf(readings);
|
|
7647
7562
|
if (!movedBeyondScatter(known?.costSec ?? null, median, readings)) {
|
|
7648
|
-
this
|
|
7563
|
+
this.encodeCost.copyCost.set(key, { ...known, readings });
|
|
7649
7564
|
return;
|
|
7650
7565
|
}
|
|
7651
|
-
this
|
|
7566
|
+
this.encodeCost.copyCost.set(key, { costSec: median, readings, version: (known?.version ?? 0) + 1 });
|
|
7652
7567
|
logger.info(
|
|
7653
7568
|
`transcode: ${session.file.name} copies at ${(1 / median).toFixed(2)}x on this host ` +
|
|
7654
7569
|
`(median of ${readings.length}, latest ${speed.toFixed(2)}x)`
|
|
@@ -7682,20 +7597,46 @@ export class HlsSessionManager {
|
|
|
7682
7597
|
return;
|
|
7683
7598
|
}
|
|
7684
7599
|
const key = this.#audioCostKey(session);
|
|
7685
|
-
const known = this
|
|
7600
|
+
const known = this.encodeCost.audioCost.get(key);
|
|
7686
7601
|
const readings = [...(known?.readings ?? []), costSec].slice(-DECODE_LEARNING_READINGS);
|
|
7687
7602
|
const median = medianOf(readings);
|
|
7688
7603
|
if (!movedBeyondScatter(known?.costSec ?? null, median, readings)) {
|
|
7689
|
-
this
|
|
7604
|
+
this.encodeCost.audioCost.set(key, { ...known, readings });
|
|
7690
7605
|
return;
|
|
7691
7606
|
}
|
|
7692
|
-
this
|
|
7607
|
+
this.encodeCost.audioCost.set(key, { costSec: median, readings, version: (known?.version ?? 0) + 1 });
|
|
7693
7608
|
logger.info(
|
|
7694
7609
|
`transcode: ${session.file.name} encodes audio track ${session.audioTrackIndex ?? 0} at ` +
|
|
7695
7610
|
`${(1 / median).toFixed(2)}x on this host (median of ${readings.length}, latest ${speed.toFixed(2)}x)`
|
|
7696
7611
|
);
|
|
7697
7612
|
}
|
|
7698
7613
|
|
|
7614
|
+
/**
|
|
7615
|
+
* What this file costs the machine merely by being fetched and delivered
|
|
7616
|
+
* while it is watched, in seconds of work per second of video.
|
|
7617
|
+
*
|
|
7618
|
+
* A viewer consumes the file at its own byte rate, and every one of those
|
|
7619
|
+
* bytes is downloaded, verified and pushed by this process. Priced per
|
|
7620
|
+
* megabyte from readings taken while nothing was encoding, so the two
|
|
7621
|
+
* measurements do not contain each other. Zero while either term is unmeasured
|
|
7622
|
+
* — a guess here would refuse rungs on arithmetic nobody performed.
|
|
7623
|
+
*
|
|
7624
|
+
* @param {HlsSession} session
|
|
7625
|
+
* @returns {number}
|
|
7626
|
+
*/
|
|
7627
|
+
#torrentCostSecFor(session) {
|
|
7628
|
+
const perMegabyte = this.#observedTorrentCostPerMegabyte;
|
|
7629
|
+
const megabytesPerSecond = this.#torrentMegabytesPerSecond(
|
|
7630
|
+
session.file.sourceKey,
|
|
7631
|
+
session.file.fileIndex,
|
|
7632
|
+
this.#fileLengthByKey.get(session.file.key) ?? null,
|
|
7633
|
+
session.file.durationSeconds
|
|
7634
|
+
);
|
|
7635
|
+
return perMegabyte !== null && megabytesPerSecond !== null
|
|
7636
|
+
? perMegabyte * megabytesPerSecond
|
|
7637
|
+
: 0;
|
|
7638
|
+
}
|
|
7639
|
+
|
|
7699
7640
|
/**
|
|
7700
7641
|
* @param {HlsSession} session
|
|
7701
7642
|
* @returns {string}
|
|
@@ -7742,7 +7683,7 @@ export class HlsSessionManager {
|
|
|
7742
7683
|
return;
|
|
7743
7684
|
}
|
|
7744
7685
|
const key = session.file.key;
|
|
7745
|
-
const known = this
|
|
7686
|
+
const known = this.encodeCost.decodeCost.get(key);
|
|
7746
7687
|
const readings = [...(known?.readings ?? []), decodeCostSec].slice(-DECODE_LEARNING_READINGS);
|
|
7747
7688
|
const costSec = medianOf(readings);
|
|
7748
7689
|
if (!movedBeyondScatter(known?.costSec ?? null, costSec, readings)) {
|
|
@@ -7750,10 +7691,10 @@ export class HlsSessionManager {
|
|
|
7750
7691
|
// would bump the version and make every session recompute its offer,
|
|
7751
7692
|
// which is asked for on the path that serves every playlist, init and
|
|
7752
7693
|
// segment.
|
|
7753
|
-
this
|
|
7694
|
+
this.encodeCost.decodeCost.set(key, { ...known, readings });
|
|
7754
7695
|
return;
|
|
7755
7696
|
}
|
|
7756
|
-
this
|
|
7697
|
+
this.encodeCost.decodeCost.set(key, { costSec, readings, version: (known?.version ?? 0) + 1 });
|
|
7757
7698
|
logger.info(
|
|
7758
7699
|
`transcode: ${session.file.name} decodes at ${(1 / costSec).toFixed(2)}x on this host ` +
|
|
7759
7700
|
`(median of ${readings.length}, latest ${(1 / decodeCostSec).toFixed(2)}x from ${height}p ` +
|
|
@@ -7813,7 +7754,7 @@ export class HlsSessionManager {
|
|
|
7813
7754
|
// has run before. Without it a second open of a file answers from the
|
|
7814
7755
|
// startup clips again, undoing the correction the first playback earned.
|
|
7815
7756
|
const observedDecodeCostSec = mediaInfo?.sourceKey !== undefined
|
|
7816
|
-
? (this
|
|
7757
|
+
? (this.encodeCost.decodeCost.get(`${mediaInfo.sourceKey}:${mediaInfo.fileIndex}`)?.costSec ?? null)
|
|
7817
7758
|
: null;
|
|
7818
7759
|
// What this file costs the machine merely by being fetched and delivered is
|
|
7819
7760
|
// known before any session exists, so the FIRST offer — the one the viewer
|
|
@@ -7831,7 +7772,7 @@ export class HlsSessionManager {
|
|
|
7831
7772
|
? this.#observedTorrentCostPerMegabyte * movingMegabytesPerSec
|
|
7832
7773
|
: 0;
|
|
7833
7774
|
const forBranch = (transcodeVideo) =>
|
|
7834
|
-
this
|
|
7775
|
+
this.encodeCost.sustainableHeights({
|
|
7835
7776
|
heights,
|
|
7836
7777
|
concurrentCostSec: torrentCostSec,
|
|
7837
7778
|
// What this file's swarm demanded the last time it was read. Absent on
|
|
@@ -7852,409 +7793,6 @@ export class HlsSessionManager {
|
|
|
7852
7793
|
return { copy: forBranch(false), transcode: forBranch(true) };
|
|
7853
7794
|
}
|
|
7854
7795
|
|
|
7855
|
-
/**
|
|
7856
|
-
* Drop the rungs this host cannot hold at realtime.
|
|
7857
|
-
*
|
|
7858
|
-
* Every rung below the source height is a full re-encode — decode the whole
|
|
7859
|
-
* source, encode a smaller picture — and on a weak host that is dearer than
|
|
7860
|
-
* the copy it replaces. Measured 2026-08-14: 1080p was copied at 7.8-8.9x
|
|
7861
|
-
* while the offered 240p rung ran at 0.388-0.947x, its first segment took
|
|
7862
|
-
* 30 s and later ones were held 22 s, so choosing a LOWER quality is what
|
|
7863
|
-
* broke playback. A rung that cannot be produced faster than it is watched
|
|
7864
|
-
* must not be offered at all.
|
|
7865
|
-
*
|
|
7866
|
-
* The session's OWN height always stays: an encoder is already producing it,
|
|
7867
|
-
* and removing it would point the player at a rung nobody is encoding.
|
|
7868
|
-
*
|
|
7869
|
-
* @param {{ heights: number[], ownHeight: number, sourceWidth: number, sourceHeight: number, fps: number, source: { megapixelsPerSecond: number, megabitsPerSecond: number } | null, transcodeVideo: boolean }} params
|
|
7870
|
-
* @returns {number[]}
|
|
7871
|
-
*/
|
|
7872
|
-
/**
|
|
7873
|
-
* What a running re-encode of the picture costs, in seconds of work per
|
|
7874
|
-
* second of video.
|
|
7875
|
-
*
|
|
7876
|
-
* Measured first: `lastAloneSpeed` is what this very rung did with the
|
|
7877
|
-
* machine to itself. Failing that, the encode model that decides every rung —
|
|
7878
|
-
* the same benchmark, the same decode term — applied to this rung's own pixel
|
|
7879
|
-
* rate. There is no third answer: a rung whose cost cannot be derived at all
|
|
7880
|
-
* contributes nothing rather than a number somebody invented.
|
|
7881
|
-
*
|
|
7882
|
-
* @param {HlsSession} session
|
|
7883
|
-
* @returns {number}
|
|
7884
|
-
*/
|
|
7885
|
-
#pictureCostOf(session) {
|
|
7886
|
-
if (Number.isFinite(session.lastAloneSpeed) && session.lastAloneSpeed > 0) {
|
|
7887
|
-
return 1 / session.lastAloneSpeed;
|
|
7888
|
-
}
|
|
7889
|
-
const benchmark = this.softwarePresetBenchmark;
|
|
7890
|
-
const width = Number(session.output.encodeWidth) || 0;
|
|
7891
|
-
const height = Number(session.output.encodeHeight) || 0;
|
|
7892
|
-
const fps = Number(session.output.outputFps) || TRANSCODE_FPS;
|
|
7893
|
-
if (!Array.isArray(benchmark) || benchmark.length === 0 || width <= 0 || height <= 0) {
|
|
7894
|
-
return 0;
|
|
7895
|
-
}
|
|
7896
|
-
const { speed } = canSustainOutput({
|
|
7897
|
-
benchmark,
|
|
7898
|
-
decodeModel: this.decodeCostModel,
|
|
7899
|
-
source: session.file.decode ?? null,
|
|
7900
|
-
outputPixelsPerSec: width * height * fps,
|
|
7901
|
-
observedDecodeCostSec: null,
|
|
7902
|
-
concurrentCostSec: 0
|
|
7903
|
-
});
|
|
7904
|
-
return Number.isFinite(speed) && speed > 0 ? 1 / speed : 0;
|
|
7905
|
-
}
|
|
7906
|
-
|
|
7907
|
-
/**
|
|
7908
|
-
* What everything OTHER than this session is costing right now, or null when
|
|
7909
|
-
* any of it is unpriced.
|
|
7910
|
-
*
|
|
7911
|
-
* Used to recover a soundtrack's own share from a reading taken beside the
|
|
7912
|
-
* picture — the only kind of reading a rendition ever gives, since it runs
|
|
7913
|
-
* exactly as long as the picture does. Refusing to answer when something
|
|
7914
|
-
* running has no price is the point: unpriced work would otherwise be
|
|
7915
|
-
* attributed to the soundtrack, and an overpriced soundtrack refuses quality
|
|
7916
|
-
* steps the host could actually hold.
|
|
7917
|
-
*
|
|
7918
|
-
* @param {HlsSession} session
|
|
7919
|
-
* @returns {number | null}
|
|
7920
|
-
*/
|
|
7921
|
-
#pricedConcurrentCost(session) {
|
|
7922
|
-
let cost = 0;
|
|
7923
|
-
for (const member of this.liveOutputs.familyOf(session)) {
|
|
7924
|
-
if (member === session || !processCanBeSignalled(runStateOf(member))) {
|
|
7925
|
-
continue;
|
|
7926
|
-
}
|
|
7927
|
-
if (member.audioOnly === true) {
|
|
7928
|
-
const audio = this.#observedAudioCost.get(this.#audioCostKey(member));
|
|
7929
|
-
if (!audio || !(audio.costSec > 0)) {
|
|
7930
|
-
return null;
|
|
7931
|
-
}
|
|
7932
|
-
cost += audio.costSec;
|
|
7933
|
-
continue;
|
|
7934
|
-
}
|
|
7935
|
-
if (member.transcodeVideo !== true) {
|
|
7936
|
-
const copy = this.#observedCopyCost.get(member.file.key);
|
|
7937
|
-
if (!copy || !(copy.costSec > 0)) {
|
|
7938
|
-
return null;
|
|
7939
|
-
}
|
|
7940
|
-
cost += copy.costSec;
|
|
7941
|
-
continue;
|
|
7942
|
-
}
|
|
7943
|
-
const picture = this.#pictureCostOf(member);
|
|
7944
|
-
if (!(picture > 0)) {
|
|
7945
|
-
return null;
|
|
7946
|
-
}
|
|
7947
|
-
cost += picture;
|
|
7948
|
-
}
|
|
7949
|
-
// Encoders outside this family are counted by number only — there is no
|
|
7950
|
-
// price to look up for another film's session — so a reading taken while
|
|
7951
|
-
// one is running cannot be attributed either.
|
|
7952
|
-
return this.#runningEncoders() > this.liveOutputs.familyOf(session).filter(
|
|
7953
|
-
(member) => processCanBeSignalled(runStateOf(member))
|
|
7954
|
-
).length
|
|
7955
|
-
? null
|
|
7956
|
-
: cost;
|
|
7957
|
-
}
|
|
7958
|
-
|
|
7959
|
-
/**
|
|
7960
|
-
* What each height of this family is costing RIGHT NOW, for the heights an
|
|
7961
|
-
* encoder is actually running at.
|
|
7962
|
-
*
|
|
7963
|
-
* Exists so a height can be judged against what the machine spends on
|
|
7964
|
-
* everything else — a step being warmed is running while it is judged, and
|
|
7965
|
-
* charged its own cost it refuses itself.
|
|
7966
|
-
*
|
|
7967
|
-
* @param {HlsSession} session
|
|
7968
|
-
* @returns {Map<number, number>}
|
|
7969
|
-
*/
|
|
7970
|
-
#runningCostByHeight(session) {
|
|
7971
|
-
/** @type {Map<number, number>} */
|
|
7972
|
-
const byHeight = new Map();
|
|
7973
|
-
for (const member of this.liveOutputs.familyOf(session)) {
|
|
7974
|
-
if (member.audioOnly === true || member.transcodeVideo !== true) {
|
|
7975
|
-
continue;
|
|
7976
|
-
}
|
|
7977
|
-
if (!processCanBeSignalled(runStateOf(member))) {
|
|
7978
|
-
continue;
|
|
7979
|
-
}
|
|
7980
|
-
const height = this.liveOutputs.variantHeightOf(member);
|
|
7981
|
-
if (height > 0) {
|
|
7982
|
-
byHeight.set(height, (byHeight.get(height) ?? 0) + this.#pictureCostOf(member));
|
|
7983
|
-
}
|
|
7984
|
-
}
|
|
7985
|
-
return byHeight;
|
|
7986
|
-
}
|
|
7987
|
-
|
|
7988
|
-
/**
|
|
7989
|
-
* Seconds of work per second of video this family is ALREADY committed to,
|
|
7990
|
-
* beside any rung being considered.
|
|
7991
|
-
*
|
|
7992
|
-
* Every encoder of the family that is actually running: the picture, whether
|
|
7993
|
-
* it is copied or re-encoded, and each audio rendition. The rung the viewer
|
|
7994
|
-
* is watching and the source's own copied height are never withdrawn by the
|
|
7995
|
-
* caller, so charging for the encoder that serves them cannot strand anyone —
|
|
7996
|
-
* what it does is stop the NEXT rung being offered as though the machine were
|
|
7997
|
-
* idle, which is what the field disproved on 2026-08-15.
|
|
7998
|
-
*
|
|
7999
|
-
* Anything whose cost is neither measured nor derivable contributes nothing.
|
|
8000
|
-
* A guess here would refuse rungs on arithmetic nobody performed.
|
|
8001
|
-
*
|
|
8002
|
-
* @param {HlsSession} session
|
|
8003
|
-
* @returns {number}
|
|
8004
|
-
*/
|
|
8005
|
-
#committedCostOf(session) {
|
|
8006
|
-
let cost = 0;
|
|
8007
|
-
for (const member of this.liveOutputs.familyOf(session)) {
|
|
8008
|
-
// Only what still HAS an encoder. A quality step the viewer left keeps
|
|
8009
|
-
// its session and its segments but not a process, and it produces nothing
|
|
8010
|
-
// for anybody — charging the machine for it would refuse steps on work
|
|
8011
|
-
// nobody is doing.
|
|
8012
|
-
//
|
|
8013
|
-
// A SUSPENDED encoder is charged, deliberately, and this is not the same
|
|
8014
|
-
// question. The unit here is seconds of work per second of VIDEO, not per
|
|
8015
|
-
// second of wall clock: a copy running at 8x costs 0.125 s/s whether it
|
|
8016
|
-
// is producing right now or parked by the look-ahead cap, because over an
|
|
8017
|
-
// hour of watching it still produces an hour of video. Suspension is how
|
|
8018
|
-
// that cost is spread, not a discount on it — and pricing a parked
|
|
8019
|
-
// encoder at zero would offer a step on the strength of a pause that ends
|
|
8020
|
-
// the moment the viewer catches up.
|
|
8021
|
-
if (!processCanBeSignalled(runStateOf(member))) {
|
|
8022
|
-
continue;
|
|
8023
|
-
}
|
|
8024
|
-
if (member.audioOnly === true) {
|
|
8025
|
-
// A soundtrack encoder, priced from its own measured speed. Nothing is
|
|
8026
|
-
// charged for a track nobody has measured: a guess here refuses rungs
|
|
8027
|
-
// on arithmetic no one performed.
|
|
8028
|
-
const audio = this.#observedAudioCost.get(this.#audioCostKey(member));
|
|
8029
|
-
cost += audio && audio.costSec > 0 ? audio.costSec : 0;
|
|
8030
|
-
continue;
|
|
8031
|
-
}
|
|
8032
|
-
if (member.transcodeVideo !== true) {
|
|
8033
|
-
const observed = this.#observedCopyCost.get(member.file.key);
|
|
8034
|
-
cost += observed && observed.costSec > 0 ? observed.costSec : 0;
|
|
8035
|
-
continue;
|
|
8036
|
-
}
|
|
8037
|
-
// A picture being RE-ENCODED beside the rung being judged — the warm-up
|
|
8038
|
-
// that makes a quality switch seamless is two encoders by design, and
|
|
8039
|
-
// that overlap is exactly where the field measured 0.504x on a rung
|
|
8040
|
-
// predicted at 1.58x (2026-08-15). Priced by what it has been SEEN doing
|
|
8041
|
-
// when it had the machine to itself, and otherwise by the same model that
|
|
8042
|
-
// judges every rung — which is a prediction, not a guess.
|
|
8043
|
-
cost += this.#pictureCostOf(member);
|
|
8044
|
-
}
|
|
8045
|
-
// And what the FILE costs simply by being fetched and delivered while it is
|
|
8046
|
-
// watched: a viewer consumes it at its own byte rate, and every one of
|
|
8047
|
-
// those bytes is downloaded, verified and pushed by this process. Priced
|
|
8048
|
-
// per megabyte from readings taken while nothing was encoding, so the two
|
|
8049
|
-
// measurements do not contain each other.
|
|
8050
|
-
const perMegabyte = this.#observedTorrentCostPerMegabyte;
|
|
8051
|
-
const megabytesPerSecond = this.#torrentMegabytesPerSecond(
|
|
8052
|
-
session.file.sourceKey,
|
|
8053
|
-
session.file.fileIndex,
|
|
8054
|
-
this.#fileLengthByKey.get(session.file.key) ?? null,
|
|
8055
|
-
session.file.durationSeconds
|
|
8056
|
-
);
|
|
8057
|
-
if (perMegabyte !== null && megabytesPerSecond !== null) {
|
|
8058
|
-
cost += perMegabyte * megabytesPerSecond;
|
|
8059
|
-
}
|
|
8060
|
-
return cost;
|
|
8061
|
-
}
|
|
8062
|
-
|
|
8063
|
-
/**
|
|
8064
|
-
* The speed each rung of this family was last seen running at, when it was
|
|
8065
|
-
* running alone.
|
|
8066
|
-
*
|
|
8067
|
-
* A rung that has been watched failing is refused on that evidence; a rung
|
|
8068
|
-
* nobody has run says nothing about itself and is judged by the startup
|
|
8069
|
-
* measurement like any other.
|
|
8070
|
-
*
|
|
8071
|
-
* @param {HlsSession} base
|
|
8072
|
-
* @returns {Map<number, number>}
|
|
8073
|
-
*/
|
|
8074
|
-
#measuredRungSpeeds(base) {
|
|
8075
|
-
/** @type {Map<number, number>} */
|
|
8076
|
-
const speeds = new Map();
|
|
8077
|
-
for (const session of this.liveOutputs.familyOf(base)) {
|
|
8078
|
-
if (session.transcodeVideo !== true || !Number.isFinite(session.lastAloneSpeed)) {
|
|
8079
|
-
continue;
|
|
8080
|
-
}
|
|
8081
|
-
const height = this.liveOutputs.variantHeightOf(session);
|
|
8082
|
-
if (height > 0) {
|
|
8083
|
-
speeds.set(height, session.lastAloneSpeed);
|
|
8084
|
-
}
|
|
8085
|
-
}
|
|
8086
|
-
return speeds;
|
|
8087
|
-
}
|
|
8088
|
-
|
|
8089
|
-
#sustainableHeights({
|
|
8090
|
-
heights,
|
|
8091
|
-
ownHeight,
|
|
8092
|
-
// Every height a viewer has on screen, not one: two viewers of one picture
|
|
8093
|
-
// can be on two rungs, and a rung is never withdrawn while somebody is
|
|
8094
|
-
// watching it — their next segment would 404 on a stream that is playing.
|
|
8095
|
-
playingHeights = new Set(),
|
|
8096
|
-
sourceWidth,
|
|
8097
|
-
sourceHeight,
|
|
8098
|
-
fps,
|
|
8099
|
-
source,
|
|
8100
|
-
transcodeVideo,
|
|
8101
|
-
observedDecodeCostSec = null,
|
|
8102
|
-
concurrentCostSec = 0,
|
|
8103
|
-
runningCostByHeight = null,
|
|
8104
|
-
measuredHeights = null,
|
|
8105
|
-
requiredSpeed = null
|
|
8106
|
-
}) {
|
|
8107
|
-
// What this file's own supply demands, measured by its reader — and
|
|
8108
|
-
// realtime while it has not been measured. Read once here so the line that
|
|
8109
|
-
// reports a refusal names the figure it refused against.
|
|
8110
|
-
const bar = speedBar(requiredSpeed);
|
|
8111
|
-
const benchmark = this.softwarePresetBenchmark;
|
|
8112
|
-
if (!Array.isArray(benchmark) || benchmark.length === 0 || sourceHeight <= 0 || sourceWidth <= 0) {
|
|
8113
|
-
return heights;
|
|
8114
|
-
}
|
|
8115
|
-
/** @type {number[]} */
|
|
8116
|
-
const kept = [];
|
|
8117
|
-
/** @type {string[]} */
|
|
8118
|
-
const dropped = [];
|
|
8119
|
-
// What each height was predicted to do on THIS machine, kept so a session
|
|
8120
|
-
// started at that height can be compared against it once it runs. The
|
|
8121
|
-
// manager holds the last answer, because the offer is computed on the path
|
|
8122
|
-
// that serves every request while a session is created elsewhere.
|
|
8123
|
-
/** @type {Map<number, number | null>} */
|
|
8124
|
-
const predictedByHeight = new Map();
|
|
8125
|
-
for (const height of heights) {
|
|
8126
|
-
// A rung this session has actually been seen running below realtime is
|
|
8127
|
-
// withdrawn on that evidence, whatever the prediction says. This is the
|
|
8128
|
-
// one thing a live reading is authority on: itself. It is asked before
|
|
8129
|
-
// any exemption so a rung measured failing while on screen does not stay
|
|
8130
|
-
// offered because it was on screen when measured — otherwise a step
|
|
8131
|
-
// would ask for the one rung this machine has been measured failing at,
|
|
8132
|
-
// then fail again, then step down, for ever. A copied source height
|
|
8133
|
-
// cannot reach this: `#measuredRungSpeeds` records only sessions that
|
|
8134
|
-
// re-encode, so a copy has no reading to be withdrawn on, which is right
|
|
8135
|
-
// — it costs no encoder.
|
|
8136
|
-
const measured = measuredHeights?.get(height) ?? null;
|
|
8137
|
-
if (measured !== null && measured < 1) {
|
|
8138
|
-
// Even the rung on screen is withdrawn on measured failure: keeping it
|
|
8139
|
-
// would 404 the next segment, but keeping a rung measured at 0.007x
|
|
8140
|
-
// (field 2026-08-31, 4K HEVC on CM4) stalls the viewer for minutes with
|
|
8141
|
-
// 0.04s buffered and no way to downgrade because every other rung is
|
|
8142
|
-
// also dropped. Withdrawing it lets the offer become empty, which the
|
|
8143
|
-
// caller turns into an error the viewer can act on (try another proxy
|
|
8144
|
-
// or a lower source) instead of an endless spinner.
|
|
8145
|
-
dropped.push(`${height}p=${measured.toFixed(2)}x measured`);
|
|
8146
|
-
continue;
|
|
8147
|
-
}
|
|
8148
|
-
// The rung ON SCREEN is kept only when it has not been measured failing
|
|
8149
|
-
// above. Keeping a rung measured at 0.007x would stall the viewer with
|
|
8150
|
-
// no path to a faster rung, which is what the field showed.
|
|
8151
|
-
if (playingHeights.has(height)) {
|
|
8152
|
-
kept.push(height);
|
|
8153
|
-
continue;
|
|
8154
|
-
}
|
|
8155
|
-
// The height an encoder is ALREADY producing, and the source's own height
|
|
8156
|
-
// when the FAMILY serves it by copy — neither has to be predicted,
|
|
8157
|
-
// because it is happening. A copied rung costs no encoder at all, so no
|
|
8158
|
-
// measurement of this host can ever be a reason to withdraw it, and the
|
|
8159
|
-
// whole point of it is that it is where a viewer on a rung the machine
|
|
8160
|
-
// cannot hold goes back to. `transcodeVideo` here is the base's, not the
|
|
8161
|
-
// asking session's: a 240p rung re-encodes, and reading its own flag is
|
|
8162
|
-
// what withdrew a copied 1080p in the field on 2026-08-15.
|
|
8163
|
-
//
|
|
8164
|
-
// A source height that would have to be RE-ENCODED is a prediction like
|
|
8165
|
-
// any other: on a session whose budget stepped down to 480p, the source's
|
|
8166
|
-
// 1080p is neither copied nor being produced, and keeping it unpriced
|
|
8167
|
-
// would offer exactly the kind of rung this refuses. Likewise, a rung
|
|
8168
|
-
// this session is already producing at 0.007x (field 2026-08-31, 4K HEVC
|
|
8169
|
-
// on CM4, 0.1x at 23:45 and 0.007x at 06:57) is not sustainable just
|
|
8170
|
-
// because it is running — keeping it offered no path to a faster rung
|
|
8171
|
-
// and left the viewer at 0.04s buffered with no downgrade.
|
|
8172
|
-
if (
|
|
8173
|
-
(height === ownHeight && !transcodeVideo) ||
|
|
8174
|
-
(height === sourceHeight && !transcodeVideo)
|
|
8175
|
-
) {
|
|
8176
|
-
kept.push(height);
|
|
8177
|
-
continue;
|
|
8178
|
-
}
|
|
8179
|
-
const width = Math.round(((sourceWidth / sourceHeight) * height) / 2) * 2;
|
|
8180
|
-
// What the machine is spending on everything EXCEPT this height. A step
|
|
8181
|
-
// being warmed for a switch is already running while it is judged, so its
|
|
8182
|
-
// own cost is inside the committed total — and charged against itself it
|
|
8183
|
-
// is counted twice. Measured against the field figures of 2026-08-15
|
|
8184
|
-
// that is 1.83x against 1.03x: below the margin, so the step the viewer
|
|
8185
|
-
// had just asked for was dropped from the offer by the act of warming it,
|
|
8186
|
-
// and its next segment answered 404 on a stream that was playing.
|
|
8187
|
-
const concurrentBesideThis = Math.max(
|
|
8188
|
-
0,
|
|
8189
|
-
concurrentCostSec - (runningCostByHeight?.get(height) ?? 0)
|
|
8190
|
-
);
|
|
8191
|
-
const { speed } = canSustainOutput({
|
|
8192
|
-
benchmark,
|
|
8193
|
-
decodeModel: this.decodeCostModel,
|
|
8194
|
-
source,
|
|
8195
|
-
outputPixelsPerSec: width * height * fps,
|
|
8196
|
-
observedDecodeCostSec,
|
|
8197
|
-
concurrentCostSec: concurrentBesideThis
|
|
8198
|
-
});
|
|
8199
|
-
// The benchmark behind that figure was taken on a QUIET host — one
|
|
8200
|
-
// ffmpeg and nothing else. The machine a step will actually run on is
|
|
8201
|
-
// also running the kernel, the container and whatever else its owner
|
|
8202
|
-
// does, and on the addon host that was measured at 99 % busy with a
|
|
8203
|
-
// quarter of it unattributed. Only the unattributed part is charged
|
|
8204
|
-
// here: our own encoders are already in `concurrentBesideThis` and the
|
|
8205
|
-
// proxy's own work is already priced per megabyte moved.
|
|
8206
|
-
// Two corrections, and they are different facts about the machine. The
|
|
8207
|
-
// availability share removes work nobody has been charged for; the
|
|
8208
|
-
// contention penalty says what OUR OWN second job costs, because the
|
|
8209
|
-
// budget adds independent prices and this host does not behave that way
|
|
8210
|
-
// — the same work measured 2.6× dearer beside one encoder and 3.7×
|
|
8211
|
-
// beside two (2026-08-18). `concurrentBesideThis` already counts what is
|
|
8212
|
-
// committed; this multiplies by how badly running at all together goes.
|
|
8213
|
-
const othersRunning = concurrentBesideThis > 0 ? this.#encodersRunningNow() : 0;
|
|
8214
|
-
const { penalty } = contentionPenalty(othersRunning, this.contentionPenalties);
|
|
8215
|
-
const onThisMachine = correctForAvailability(
|
|
8216
|
-
speed === null ? null : speed / penalty,
|
|
8217
|
-
this.hostAvailability
|
|
8218
|
-
);
|
|
8219
|
-
// Kept against the step's own session, so that when it runs the field
|
|
8220
|
-
// says what the prediction was worth. Without this the only comparison
|
|
8221
|
-
// available is between two figures written minutes apart in different
|
|
8222
|
-
// lines of the log.
|
|
8223
|
-
predictedByHeight.set(height, onThisMachine);
|
|
8224
|
-
if (onThisMachine !== null && onThisMachine >= bar) {
|
|
8225
|
-
kept.push(height);
|
|
8226
|
-
continue;
|
|
8227
|
-
}
|
|
8228
|
-
dropped.push(`${height}p=${onThisMachine === null ? "n/a" : `${onThisMachine.toFixed(2)}x`}`);
|
|
8229
|
-
}
|
|
8230
|
-
// Written when the ANSWER changes, not when the answer is recomputed. This
|
|
8231
|
-
// is asked on the path that serves every playlist, init and segment, and
|
|
8232
|
-
// the figures behind it move every five seconds — so an unconditional line
|
|
8233
|
-
// here is roughly seven hundred identical lines an hour into a forwarder
|
|
8234
|
-
// that holds five hundred, which buries whatever is worth reading.
|
|
8235
|
-
if (dropped.length > 0) {
|
|
8236
|
-
const line =
|
|
8237
|
-
`transcode: not offering ${dropped.join(" ")} — below ${bar.toFixed(2)}x ` +
|
|
8238
|
-
(Number.isFinite(requiredSpeed) && requiredSpeed > 1
|
|
8239
|
-
? "(the speed this file's own interruptions demand) "
|
|
8240
|
-
: "(realtime, this file's supply not measured yet) ") +
|
|
8241
|
-
// Said with the figures, because a step refused on a busy machine and
|
|
8242
|
-
// one refused on an idle machine are different facts about the host.
|
|
8243
|
-
(this.hostAvailability?.known
|
|
8244
|
-
? `on a machine with ${Math.round(this.hostAvailability.share * 100)}% to spare `
|
|
8245
|
-
: "") +
|
|
8246
|
-
`(offering ${kept.map((height) => `${height}p`).join(" ")})`;
|
|
8247
|
-
if (line !== this.#lastOfferLine) {
|
|
8248
|
-
this.#lastOfferLine = line;
|
|
8249
|
-
logger.info(line);
|
|
8250
|
-
}
|
|
8251
|
-
this.lastPredictedByHeight = predictedByHeight;
|
|
8252
|
-
} else {
|
|
8253
|
-
this.lastPredictedByHeight = predictedByHeight;
|
|
8254
|
-
this.#lastOfferLine = "";
|
|
8255
|
-
}
|
|
8256
|
-
return kept;
|
|
8257
|
-
}
|
|
8258
7796
|
|
|
8259
7797
|
/**
|
|
8260
7798
|
* The height this proxy is asking the player to move to, or 0.
|