@torrent-tv/proxy 2.98.10 → 2.98.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -28,6 +28,8 @@ export class EncodeInputs {
|
|
|
28
28
|
/** Whether an output's last resolved input was its original file. @type {WeakMap<object, boolean>} */
|
|
29
29
|
#original = new WeakMap();
|
|
30
30
|
#revision = 0;
|
|
31
|
+
/** Output → how long its last input took to be ready, ms. @type {WeakMap<object, number>} */
|
|
32
|
+
#preparedIn = new WeakMap();
|
|
31
33
|
#memoryRevision = 0;
|
|
32
34
|
#held = 0;
|
|
33
35
|
#allowed = 0;
|
|
@@ -270,6 +272,18 @@ export class EncodeInputs {
|
|
|
270
272
|
* @param {{ sourceKey: string, ranges: Array<{ fileIndex: number, start: number, end: number }> } | null} [arrived] -
|
|
271
273
|
* The bytes that arrived, inclusive, when the event names them.
|
|
272
274
|
*/
|
|
275
|
+
/**
|
|
276
|
+
* How long this output's last input took from being asked for to being
|
|
277
|
+
* ready, in milliseconds, or null before one was: the time a run gives the
|
|
278
|
+
* input after it to be prepared in (`EncodeRuns`).
|
|
279
|
+
*
|
|
280
|
+
* @param {object} output
|
|
281
|
+
* @returns {number | null}
|
|
282
|
+
*/
|
|
283
|
+
preparationMsOf(output) {
|
|
284
|
+
return this.#preparedIn.get(output) ?? null;
|
|
285
|
+
}
|
|
286
|
+
|
|
273
287
|
bytesChanged(arrived = null) {
|
|
274
288
|
this.#revision++;
|
|
275
289
|
for (const request of this.#requests.values()) {
|
|
@@ -376,6 +390,7 @@ export class EncodeInputs {
|
|
|
376
390
|
// is commanded over its prefix.
|
|
377
391
|
if (result.kind === "result" && result.to > request.to) result.to = Math.max(request.from, request.to);
|
|
378
392
|
request.result = result;
|
|
393
|
+
if (result.kind === "result") this.#preparedIn.set(request.output, this.#now() - request.timing.askedAt);
|
|
379
394
|
this.#note(request, result.kind === "result" ? `ready #${result.from}..#${result.to}, ${result.bytes ?? "?"} bytes` +
|
|
380
395
|
(Number.isFinite(result.copyMs) ? (result.deferredBytes > 0
|
|
381
396
|
? `, opening part copied in ${result.copyMs}ms, ${result.deferredBytes} bytes of the stretch copied while the run opens`
|
|
@@ -362,6 +362,26 @@ export function planEncoders({
|
|
|
362
362
|
const decided = new Map();
|
|
363
363
|
const room = Math.max(0, maxRuns - live.length);
|
|
364
364
|
const refetchPerSegment = segmentSeconds * refetchSecPerFilmSecond;
|
|
365
|
+
// WHAT "NEVER" IS, ONE FIGURE FOR EVERY ARRANGEMENT THIS PASS COMPARES: later
|
|
366
|
+
// than any encoder here can reach the last wanted number — the film's length,
|
|
367
|
+
// a start and a stop, the whole wanted stretch at the slowest rate any
|
|
368
|
+
// arrangement can run at, and every piece fetched again. Capped at the film's
|
|
369
|
+
// length instead, a slow encoder's arrival scored as never; and worked out
|
|
370
|
+
// from each arrangement's own encoders, the arrangement without an encoder
|
|
371
|
+
// had a smaller never than the one with it. Either way an encoder making the
|
|
372
|
+
// film behind the viewer at 0.57x scored no better than none, and the plan
|
|
373
|
+
// stopped it before its first piece, then started it again when the speed
|
|
374
|
+
// read 1.9x — thirteen times in three minutes on Home Assistant, each start
|
|
375
|
+
// copying 108-122 MB of input (r37, torrent-tv/meta#178).
|
|
376
|
+
const neverSec = (() => {
|
|
377
|
+
const first = Math.min(...wanted.map((span) => span.from));
|
|
378
|
+
const last = Math.max(...wanted.map((span) => span.to));
|
|
379
|
+
const film = (last + 1) * segmentSeconds;
|
|
380
|
+
const slowest = rate / contentionPenaltyFor(Math.max(maxRuns, live.length));
|
|
381
|
+
if (!(slowest > 0) || !Number.isFinite(film)) return film;
|
|
382
|
+
return film + firstByteWaitSec + killCostSec + (last - first + 2) / slowest +
|
|
383
|
+
(last - first + 1) * refetchPerSegment + segmentSeconds;
|
|
384
|
+
})();
|
|
365
385
|
|
|
366
386
|
let arrangements = [{ fill: [], used: new Set(), fresh: 0 }];
|
|
367
387
|
for (let index = 0; index < positions.length; index += 1) {
|
|
@@ -441,7 +461,7 @@ export function planEncoders({
|
|
|
441
461
|
: (piece) => finishesItsPieceIn(run, piece)
|
|
442
462
|
});
|
|
443
463
|
}
|
|
444
|
-
const scored = latenessOf(priced(encoders), coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
|
|
464
|
+
const scored = latenessOf(priced(encoders), coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds, neverSec);
|
|
445
465
|
if (bestScore === null || cheaperThan(scored, bestScore)) {
|
|
446
466
|
bestScore = scored;
|
|
447
467
|
best = arrangement;
|
|
@@ -492,7 +512,7 @@ export function planEncoders({
|
|
|
492
512
|
const scoreOf = (override) => {
|
|
493
513
|
const encoders = encodersOf(override);
|
|
494
514
|
return latenessOf(encoders, coverage, wanted, untilNeeded,
|
|
495
|
-
rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
|
|
515
|
+
rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds, neverSec);
|
|
496
516
|
};
|
|
497
517
|
for (const run of live) {
|
|
498
518
|
if (placement.has(run)) {
|
|
@@ -519,7 +539,7 @@ export function planEncoders({
|
|
|
519
539
|
const kept = encodersOf(new Map());
|
|
520
540
|
const without = encodersOf(new Map([[run, null]]));
|
|
521
541
|
const scoreOf_ = (encoders) => latenessOf(encoders, coverage, wanted, untilNeeded,
|
|
522
|
-
rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
|
|
542
|
+
rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds, neverSec);
|
|
523
543
|
return cheaperThan(scoreOf_(kept), scoreOf_(without));
|
|
524
544
|
};
|
|
525
545
|
|
|
@@ -586,7 +606,7 @@ export function planEncoders({
|
|
|
586
606
|
continue;
|
|
587
607
|
}
|
|
588
608
|
const without = latenessOf(encodersOf(new Map([[run, null]])), coverage, wanted, untilNeeded,
|
|
589
|
-
rate / contentionPenaltyFor(Math.max(0, live.length - 2)), refetchPerSegment, segmentSeconds);
|
|
609
|
+
rate / contentionPenaltyFor(Math.max(0, live.length - 2)), refetchPerSegment, segmentSeconds, neverSec);
|
|
590
610
|
if (cheapestScore === null || cheaperThan(without, cheapestScore)) {
|
|
591
611
|
cheapestScore = without;
|
|
592
612
|
cheapest = run;
|
|
@@ -751,13 +771,14 @@ function ranksOf(wanted, first, last) {
|
|
|
751
771
|
return rankAt;
|
|
752
772
|
}
|
|
753
773
|
|
|
754
|
-
function latenessOf(encoders, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds) {
|
|
774
|
+
function latenessOf(encoders, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds, neverSec = null) {
|
|
755
775
|
const first = Math.min(...wanted.map((span) => span.from));
|
|
756
776
|
const last = Math.max(...wanted.map((span) => span.to));
|
|
757
|
-
// What a number nobody reaches at all counts as
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
|
|
777
|
+
// What a number nobody reaches at all counts as: one figure for every
|
|
778
|
+
// arrangement the plan compares, later than any arrival (`neverSec` in
|
|
779
|
+
// `planEncoders`), and finite, so two hopeless arrangements are still told
|
|
780
|
+
// apart by the rest of the sum. Without it, the film's own length.
|
|
781
|
+
const never = Number.isFinite(neverSec) ? neverSec : (last + 1) * segmentSeconds;
|
|
761
782
|
|
|
762
783
|
// WHICH SIDE OF THE VIEWERS a piece is on. The map states it; nothing here
|
|
763
784
|
// works it out from positions, and nothing here knows where a viewer stands.
|
|
@@ -876,7 +897,7 @@ function latenessOf(encoders, coverage, wanted, untilNeeded, rate, refetchSecPer
|
|
|
876
897
|
// gives. It is a definition rather than a guard: it also makes the score
|
|
877
898
|
// total on a host whose startup measured nothing at all, where every arrival
|
|
878
899
|
// is beyond reckoning and every arrangement is therefore equally hopeless.
|
|
879
|
-
const when = byWhom === null ? never :
|
|
900
|
+
const when = byWhom === null || !Number.isFinite(soonest) ? never : soonest;
|
|
880
901
|
const rank = rankAt(index);
|
|
881
902
|
doneAt.set(rank, Math.max(doneAt.get(rank) ?? 0, when));
|
|
882
903
|
const deadline = untilNeeded(index);
|
|
@@ -12,6 +12,7 @@ import { spawn } from "node:child_process";
|
|
|
12
12
|
import { contentionPenalty } from "./contention.js";
|
|
13
13
|
import { LOOKAHEAD_PAUSE_SECONDS } from "./CushionReport.js";
|
|
14
14
|
import { SpeedReadings } from "./SpeedReadings.js";
|
|
15
|
+
import { nextInputAtFirstPiece } from "./next-input.js";
|
|
15
16
|
import { ENCODE_RUN_STATE, liveRunsOf, runStateOf, wireState } from "./encode-run-state.js";
|
|
16
17
|
import { ENCODE_EXIT } from "./encode-exit.js";
|
|
17
18
|
import { EncodeRun, endOfRun } from "./EncodeRun.js";
|
|
@@ -1096,6 +1097,8 @@ export class EncodeRuns {
|
|
|
1096
1097
|
// killed mid-piece leaving a partial file — is not a correctness problem
|
|
1097
1098
|
// either, because the store serves a segment only once its closure is
|
|
1098
1099
|
// proven, and it is cleared up when the run ends.
|
|
1100
|
+
// Told when this run closes its first piece; see the next stretch below.
|
|
1101
|
+
let atFirstPiece = null;
|
|
1099
1102
|
const run = new EncodeRun({
|
|
1100
1103
|
address: session.outputKey ?? session.id,
|
|
1101
1104
|
encoder: this.#host.videoEncoder,
|
|
@@ -1129,7 +1132,12 @@ export class EncodeRuns {
|
|
|
1129
1132
|
onProgress: (report) => this.#noteRunProgress(session, run, report),
|
|
1130
1133
|
// A closed piece moves this encoder forward, perhaps out of the band that
|
|
1131
1134
|
// led; who has the processor is decided again at once.
|
|
1132
|
-
onProduced: () =>
|
|
1135
|
+
onProduced: () => {
|
|
1136
|
+
this.#host.encodeOrchestrator?.shareTheMachine?.();
|
|
1137
|
+
const told = atFirstPiece;
|
|
1138
|
+
atFirstPiece = null;
|
|
1139
|
+
told?.();
|
|
1140
|
+
},
|
|
1133
1141
|
indexOfName: (name) => session.segmentFormat.segmentIndexFromName(
|
|
1134
1142
|
session.segmentFormat.servedNameOf?.(name) ?? name),
|
|
1135
1143
|
// Why this encoder exists, recorded with its argument list. It used to be
|
|
@@ -1231,12 +1239,24 @@ export class EncodeRuns {
|
|
|
1231
1239
|
// It waits for this run's own stretch to be copied: that copy goes on
|
|
1232
1240
|
// while the run opens and FFmpeg's reads wait for it, and a second copy
|
|
1233
1241
|
// beside it on the same thread would slow it (torrent-tv/meta#178).
|
|
1242
|
+
//
|
|
1243
|
+
// And for this run's first piece when the rest of the run leaves the
|
|
1244
|
+
// time to prepare it in (`next-input.js`).
|
|
1234
1245
|
if (inputEnd < mayReach) {
|
|
1235
1246
|
const askNext = () => {
|
|
1236
1247
|
if (this.isLive(session) && run.isAlive) this.#host.encodeInputs?.take(session, inputEnd + 1, mayReach);
|
|
1237
1248
|
};
|
|
1238
|
-
|
|
1239
|
-
|
|
1249
|
+
const preparedInMs = this.#host.encodeInputs?.preparationMsOf?.(session) ?? null;
|
|
1250
|
+
const speed = Number(this.#host.encodeCost?.speedForOutput?.(session.outputKey));
|
|
1251
|
+
const timeline = session.timeline;
|
|
1252
|
+
const restSeconds = Number(timeline?.publishedStartOf?.(inputEnd + 1)) - Number(timeline?.publishedStartOf?.(safeIndex + 1));
|
|
1253
|
+
const atOnce = !nextInputAtFirstPiece({ preparedInMs, speed, restSeconds });
|
|
1254
|
+
let copied = false, produced = atOnce;
|
|
1255
|
+
const askWhenBoth = () => { if (copied && produced) askNext(); };
|
|
1256
|
+
if (!atOnce) atFirstPiece = () => { produced = true; askWhenBoth(); };
|
|
1257
|
+
const whenCopied = () => { copied = true; askWhenBoth(); };
|
|
1258
|
+
if (admittedInput.whole) void admittedInput.whole.then(whole => { if (whole) whenCopied(); });
|
|
1259
|
+
else whenCopied();
|
|
1240
1260
|
}
|
|
1241
1261
|
} else if (admittedInput) {
|
|
1242
1262
|
run.admittedInputKeys = new Map([[safeIndex, inputKey]]);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file When a run asks for the input of the run after it (torrent-tv/meta#178).
|
|
3
|
+
*
|
|
4
|
+
* Asked as soon as the run's own stretch was copied, the next input was copied
|
|
5
|
+
* while the run opened: a background run on Home Assistant started with its
|
|
6
|
+
* 108.7 MB, the next 115.6 MB were copied at once, the proxy took 1.5-2 cores,
|
|
7
|
+
* and the share left to encoders fell from 65 % to 10-16 % before either had
|
|
8
|
+
* a piece (r37). Asked at the run's first piece, it still has the rest of the
|
|
9
|
+
* run to be ready in — unless preparing an input of this output was measured
|
|
10
|
+
* to take longer than that rest at this output's speed, or the run is one
|
|
11
|
+
* piece, whose first piece is its last.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether the next input waits for this run's first piece.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} params
|
|
18
|
+
* @param {number | null} params.preparedInMs - How long this output's last input took from being asked for to being ready.
|
|
19
|
+
* @param {number} params.speed - This output's processing speed, film seconds per second; 0 when not known.
|
|
20
|
+
* @param {number} params.restSeconds - The film this run makes after its first piece.
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function nextInputAtFirstPiece({ preparedInMs, speed, restSeconds }) {
|
|
24
|
+
if (!Number.isFinite(preparedInMs) || !(speed > 0) || !(restSeconds > 0)) return false;
|
|
25
|
+
return preparedInMs < restSeconds / speed * 1000;
|
|
26
|
+
}
|