@torrent-tv/proxy 2.89.3 → 2.89.4
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/AGENTS.md +338 -336
- package/CHANGELOG.md +12 -0
- package/docs/encode-architecture.md +55 -19
- package/docs/playback-start-forecast.md +13 -5
- package/docs/storage-architecture.md +6 -0
- package/package.json +1 -1
- package/routes/stream/get.js +38 -2
- package/server.js +748 -745
- package/services/encode/EncodeOrchestrator.js +24 -49
- package/services/encode/EncodePlan.js +5 -3
- package/services/encode/EncodeRun.js +80 -17
- package/services/encode/EncodeRuns.js +1339 -1239
- package/services/encode/Renditions.js +2 -1
- package/services/encode/RunClock.js +87 -0
- package/services/encode/RunProgress.js +1 -6
- package/services/encode/encoder-readings.js +26 -28
- package/services/encode/piece-completeness.js +46 -0
- package/services/encode/quality/EncodeCost.js +1043 -1167
- package/services/encode/quality/OutputQualityState.js +1 -5
- package/services/encode/quality/QualityController.js +213 -390
- package/services/encode/quality/link-budget.js +5 -17
- package/services/encode/run-inputs.js +13 -2
- package/services/server/SegmentServing.js +3 -1
- package/services/server/ViewerRequests.js +2 -7
- package/services/server/wire-outputs.js +12 -7
- package/services/storage/segment-store/SegmentStore.js +46 -29
- package/services/torrent/supply-margin.js +7 -5
- package/test/auto-quality-step.test.js +75 -29
- package/test/decode-cost.test.js +1 -1
- package/test/encode-cost.test.js +31 -39
- package/test/encode-orchestrator.test.js +15 -8
- package/test/encode-plan-viewers.test.js +34 -27
- package/test/encode-run.test.js +34 -6
- package/test/encoder-readings.test.js +48 -26
- package/test/helpers/encode-run.js +113 -9
- package/test/link-budget.test.js +4 -5
- package/test/manager-surface.test.js +1 -2
- package/test/orchestrator-wired.test.js +5 -4
- package/test/output-speed.test.js +0 -1
- package/test/piece-completeness.test.js +42 -0
- package/test/quality-offer.test.js +1 -3
- package/test/quality-variants.test.js +1 -1
- package/test/run-progress.test.js +2 -3
- package/test/same-height-switch.test.js +1 -2
- package/test/segment-store.test.js +15 -15
- package/test/stream-route.test.js +28 -0
- package/test/supply-margin.test.js +13 -0
- package/test/two-viewers-one-picture.test.js +1 -1
- package/test/viewer-outputs.test.js +1 -1
|
@@ -84,9 +84,6 @@ export class EncodeOrchestrator {
|
|
|
84
84
|
/** Consecutive fast failures at one position, per output. @type {Map<string, { at: number, count: number }>} */
|
|
85
85
|
#failedStarts = new Map();
|
|
86
86
|
|
|
87
|
-
/** The fastest speed measured on one output, kept across restarts. @type {Map<string, number>} */
|
|
88
|
-
#lastSpeed = new Map();
|
|
89
|
-
|
|
90
87
|
/** How runs have ended, by cause. @type {Map<string, number>} */
|
|
91
88
|
#endings = new Map();
|
|
92
89
|
|
|
@@ -567,27 +564,6 @@ export class EncodeOrchestrator {
|
|
|
567
564
|
}
|
|
568
565
|
}
|
|
569
566
|
|
|
570
|
-
/**
|
|
571
|
-
* @param {string} address
|
|
572
|
-
* @param {object} run
|
|
573
|
-
* @param {number} speedX
|
|
574
|
-
*/
|
|
575
|
-
noteSpeed(address, wanted, speedX) {
|
|
576
|
-
for (const run of this.runsOn(address)) {
|
|
577
|
-
if (run === wanted) {
|
|
578
|
-
run.noteSpeed(speedX);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// HOW FAST THIS MACHINE ENCODES THIS OUTPUT is a property of the machine and
|
|
582
|
-
// the material, not of one process. Read off `run.speedX` alone it was lost
|
|
583
|
-
// at every restart: a moved encoder is a new object that has measured
|
|
584
|
-
// nothing, so the plan fell back to "nothing is known" and stopped comparing
|
|
585
|
-
// arrivals at all — which is every decision in this layer.
|
|
586
|
-
if (speedX > 0 && speedX > (this.#lastSpeed.get(address) ?? 0)) {
|
|
587
|
-
this.#lastSpeed.set(address, speedX);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
567
|
/**
|
|
592
568
|
* Decide and act, for every output anybody wants anything of and every output
|
|
593
569
|
* that still has an encoder on it.
|
|
@@ -680,14 +656,7 @@ export class EncodeOrchestrator {
|
|
|
680
656
|
// the answer the plan had given.
|
|
681
657
|
const costs = this.#costs.seconds();
|
|
682
658
|
const refetchSecPerFilmSecond = this.refetchSecPerFilmSecond(address);
|
|
683
|
-
|
|
684
|
-
// last measured doing, or what the startup benchmark predicted. The first
|
|
685
|
-
// two are this output's own; the third exists before either.
|
|
686
|
-
const speedX = Math.max(
|
|
687
|
-
live.reduce((best, run) => Math.max(best, run.speedX || 0), 0),
|
|
688
|
-
this.#lastSpeed.get(address) ?? 0,
|
|
689
|
-
this.startingSpeedFor(address) || 0
|
|
690
|
-
);
|
|
659
|
+
const speedX = this.#speedOn(address, live);
|
|
691
660
|
const actions = planEncoders({
|
|
692
661
|
coverage,
|
|
693
662
|
windows,
|
|
@@ -853,14 +822,6 @@ export class EncodeOrchestrator {
|
|
|
853
822
|
this.logger.warn(`encode: no encoder could be made for #${from}..#${to} of ${address}`);
|
|
854
823
|
return;
|
|
855
824
|
}
|
|
856
|
-
// What this machine has been measured to do on this output, carried over.
|
|
857
|
-
// A restart does not make the machine slower, and without this every moved
|
|
858
|
-
// encoder began as one whose speed nothing had measured — which the plan
|
|
859
|
-
// reads as "no arrival can be computed" and answers by comparing nothing.
|
|
860
|
-
const known = this.#lastSpeed.get(address) ?? 0;
|
|
861
|
-
if (known > 0) {
|
|
862
|
-
run.noteSpeed(known);
|
|
863
|
-
}
|
|
864
825
|
const onThisOutput = this.#runs.get(address) ?? [];
|
|
865
826
|
onThisOutput.push(run);
|
|
866
827
|
this.#runs.set(address, onThisOutput);
|
|
@@ -876,6 +837,28 @@ export class EncodeOrchestrator {
|
|
|
876
837
|
this.coverageOf(address).claim(run, from, endOfRun({ from, to }));
|
|
877
838
|
}
|
|
878
839
|
|
|
840
|
+
/**
|
|
841
|
+
* The best figure this host has for one encoder on this output: the
|
|
842
|
+
* processing speed a run here has measured (`EncodeRun.speedX`), or what this
|
|
843
|
+
* output was last measured doing and the startup measurements predict
|
|
844
|
+
* (`startingSpeedFor`), which exists before any run has closed two pieces and
|
|
845
|
+
* after a restart. The run owns its speed; nothing here keeps a copy.
|
|
846
|
+
*
|
|
847
|
+
* It is the MACHINE's speed: input waits for the swarm are not in it. What
|
|
848
|
+
* the swarm delivers is the separate supply term
|
|
849
|
+
* (`refetchSecPerFilmSecond`).
|
|
850
|
+
*
|
|
851
|
+
* @param {string} address
|
|
852
|
+
* @param {{ speedX: number }[]} live
|
|
853
|
+
* @returns {number}
|
|
854
|
+
*/
|
|
855
|
+
#speedOn(address, live) {
|
|
856
|
+
return Math.max(
|
|
857
|
+
live.reduce((best, run) => Math.max(best, run.speedX || 0), 0),
|
|
858
|
+
this.startingSpeedFor(address) || 0
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
879
862
|
/**
|
|
880
863
|
* How many encoders may run on this output, from every limit at once.
|
|
881
864
|
*
|
|
@@ -894,15 +877,7 @@ export class EncodeOrchestrator {
|
|
|
894
877
|
*/
|
|
895
878
|
#affordableOn(address, live) {
|
|
896
879
|
const byProcessor = Math.max(0, this.maxRunsFor(address));
|
|
897
|
-
|
|
898
|
-
// last measured doing, or what the startup benchmark predicted. The first
|
|
899
|
-
// two are this output's own; the third exists before either, so the budget
|
|
900
|
-
// is never asked to price encoders at a speed of zero.
|
|
901
|
-
const fastest = Math.max(
|
|
902
|
-
live.reduce((best, run) => Math.max(best, run.speedX || 0), 0),
|
|
903
|
-
this.#lastSpeed.get(address) ?? 0,
|
|
904
|
-
this.startingSpeedFor(address) || 0
|
|
905
|
-
);
|
|
880
|
+
const fastest = this.#speedOn(address, live);
|
|
906
881
|
const budget = affordableRuns({
|
|
907
882
|
byProcessor,
|
|
908
883
|
speedX: fastest,
|
|
@@ -65,9 +65,11 @@
|
|
|
65
65
|
* @property {number} from - The first number it was given.
|
|
66
66
|
* @property {number} to - The last number it was given, inclusive.
|
|
67
67
|
* @property {number} head - The next number it will produce. Its position.
|
|
68
|
-
* @property {number} speedX -
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* @property {number} speedX - The run's processing speed against realtime:
|
|
69
|
+
* film made between closed pieces over its own working time
|
|
70
|
+
* (`EncodeRun.speedX`), with input waits and stops taken out. Zero or less
|
|
71
|
+
* means nothing has measured it yet, and then no comparison involving its
|
|
72
|
+
* speed can be made.
|
|
71
73
|
*/
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
|
|
40
40
|
import { ENCODE_RUN_EVENT, ENCODE_RUN_STATE, INITIAL_RUN_STATE, nextState } from "./encode-run-state.js";
|
|
41
41
|
import { classifyEncodeExit, ENCODE_EXIT } from "./encode-exit.js";
|
|
42
|
+
import { speedFromWork } from "./encoder-readings.js";
|
|
43
|
+
import { RunClock } from "./RunClock.js";
|
|
42
44
|
import { RunProgress } from "./RunProgress.js";
|
|
43
45
|
|
|
44
46
|
/** Microseconds in a second, as ffmpeg's `out_time_ms` counts them. */
|
|
@@ -150,8 +152,14 @@ export class EncodeRun {
|
|
|
150
152
|
*/
|
|
151
153
|
#firstOutputAt = 0;
|
|
152
154
|
|
|
153
|
-
/**
|
|
154
|
-
#
|
|
155
|
+
/** The film made and the run's own working time, as of the last progress report. */
|
|
156
|
+
#workSample = null;
|
|
157
|
+
|
|
158
|
+
/** The work sample taken when this run last measured its speed. */
|
|
159
|
+
#pieceSample = null;
|
|
160
|
+
|
|
161
|
+
/** @type {{ speed: number, at: number } | null} */
|
|
162
|
+
#speedReading = null;
|
|
155
163
|
|
|
156
164
|
/** @type {boolean} */
|
|
157
165
|
#stopping = false;
|
|
@@ -186,6 +194,9 @@ export class EncodeRun {
|
|
|
186
194
|
* @param {{ info: (line: string) => void, warn: (line: string) => void, error?: (line: string) => void }} params.logger
|
|
187
195
|
* @param {() => number} [params.now]
|
|
188
196
|
* @param {(ended: RunEnded) => void} [params.onEnded]
|
|
197
|
+
* @param {(reading: { speed: number, at: number }) => void} [params.onSpeedMeasured] -
|
|
198
|
+
* Told each time this run has measured its processing speed, once per
|
|
199
|
+
* reading.
|
|
189
200
|
* @param {(name: string) => string | null} [params.onClosed] - Called with the
|
|
190
201
|
* WORKING name of every piece the encoder has finished writing, as the
|
|
191
202
|
* encoder itself names it on its own channel, and answers with the name that
|
|
@@ -222,6 +233,7 @@ export class EncodeRun {
|
|
|
222
233
|
logger,
|
|
223
234
|
now,
|
|
224
235
|
onEnded,
|
|
236
|
+
onSpeedMeasured,
|
|
225
237
|
onClosed,
|
|
226
238
|
onProgress,
|
|
227
239
|
lastSegmentIndex,
|
|
@@ -242,8 +254,11 @@ export class EncodeRun {
|
|
|
242
254
|
this.logger = logger;
|
|
243
255
|
this.now = typeof now === "function" ? now : Date.now;
|
|
244
256
|
this.progress = new RunProgress({ startSeconds, totalSeconds, now: this.now });
|
|
257
|
+
/** Where this run's time went: its own work, or waiting, or stopped. */
|
|
258
|
+
this.clock = new RunClock({ now: this.now });
|
|
245
259
|
this.onEnded = typeof onEnded === "function" ? onEnded : () => {};
|
|
246
260
|
this.onProgress = typeof onProgress === "function" ? onProgress : () => {};
|
|
261
|
+
this.onSpeedMeasured = typeof onSpeedMeasured === "function" ? onSpeedMeasured : () => {};
|
|
247
262
|
// Told the NAME of every piece the encoder has closed. The name is the
|
|
248
263
|
// proof it is whole; nothing else here can prove that.
|
|
249
264
|
this.onClosed = typeof onClosed === "function" ? onClosed : (name) => name;
|
|
@@ -264,9 +279,25 @@ export class EncodeRun {
|
|
|
264
279
|
return this.#state;
|
|
265
280
|
}
|
|
266
281
|
|
|
267
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* This run's processing speed: film made over its own working time between
|
|
284
|
+
* two closed pieces, with the time its input waited and the time it was
|
|
285
|
+
* stopped taken out (`RunClock`). Zero until two pieces have closed.
|
|
286
|
+
*
|
|
287
|
+
* @returns {number}
|
|
288
|
+
*/
|
|
268
289
|
get speedX() {
|
|
269
|
-
return this.#
|
|
290
|
+
return this.#speedReading?.speed ?? 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The same speed with the moment it was measured, or null before a second
|
|
295
|
+
* piece has closed.
|
|
296
|
+
*
|
|
297
|
+
* @returns {{ speed: number, at: number } | null}
|
|
298
|
+
*/
|
|
299
|
+
get speedReading() {
|
|
300
|
+
return this.#speedReading === null ? null : { ...this.#speedReading };
|
|
270
301
|
}
|
|
271
302
|
|
|
272
303
|
/** @returns {number} When it was spawned, or 0 before that. */
|
|
@@ -355,6 +386,16 @@ export class EncodeRun {
|
|
|
355
386
|
return this.#stopping && !this.#ended;
|
|
356
387
|
}
|
|
357
388
|
|
|
389
|
+
/** An input read of this run starts waiting for bytes; see {@link RunClock}. */
|
|
390
|
+
inputWaitBegins() {
|
|
391
|
+
this.clock.inputWaitBegins();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** An input read of this run has its bytes. */
|
|
395
|
+
inputWaitEnds() {
|
|
396
|
+
this.clock.inputWaitEnds();
|
|
397
|
+
}
|
|
398
|
+
|
|
358
399
|
/** @returns {boolean} Whether it is stopped where it stands, producing nothing. */
|
|
359
400
|
get isSuspended() {
|
|
360
401
|
return this.#state === ENCODE_RUN_STATE.SUSPENDED;
|
|
@@ -460,19 +501,24 @@ export class EncodeRun {
|
|
|
460
501
|
if (key === "out_time_ms") {
|
|
461
502
|
const numeric = Number(value);
|
|
462
503
|
if (Number.isFinite(numeric) && numeric >= 0) {
|
|
463
|
-
this.#reportProgress({ processedSeconds: numeric / MICROSECONDS_PER_SECOND
|
|
504
|
+
this.#reportProgress({ processedSeconds: numeric / MICROSECONDS_PER_SECOND });
|
|
464
505
|
}
|
|
465
506
|
} else if (key === "out_time") {
|
|
466
|
-
this.#reportProgress({ processedSeconds: null,
|
|
467
|
-
} else if (key === "speed") {
|
|
468
|
-
this.noteSpeed(Number.parseFloat(value));
|
|
469
|
-
this.#reportProgress({ processedSeconds: null, speed: value });
|
|
507
|
+
this.#reportProgress({ processedSeconds: null, outTime: value });
|
|
470
508
|
}
|
|
471
509
|
}
|
|
472
510
|
}
|
|
473
511
|
|
|
474
512
|
#reportProgress(report) {
|
|
475
513
|
this.progress.note(report);
|
|
514
|
+
if (Number.isFinite(report.processedSeconds) || typeof report.outTime === "string") {
|
|
515
|
+
const at = this.now();
|
|
516
|
+
this.#workSample = {
|
|
517
|
+
at,
|
|
518
|
+
producedSeconds: Math.max(0, this.progress.processedSeconds - this.progress.startPositionSeconds),
|
|
519
|
+
workingMs: this.clock.workingMs(at)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
476
522
|
this.onProgress(this.progress.snapshot());
|
|
477
523
|
}
|
|
478
524
|
|
|
@@ -493,11 +539,6 @@ export class EncodeRun {
|
|
|
493
539
|
}
|
|
494
540
|
}
|
|
495
541
|
|
|
496
|
-
/**
|
|
497
|
-
* What ffmpeg says about its own speed.
|
|
498
|
-
*
|
|
499
|
-
* @param {number} speedX - Times realtime.
|
|
500
|
-
*/
|
|
501
542
|
/**
|
|
502
543
|
* Names of finished pieces, as the encoder writes them.
|
|
503
544
|
*
|
|
@@ -524,6 +565,7 @@ export class EncodeRun {
|
|
|
524
565
|
}
|
|
525
566
|
|
|
526
567
|
#publishClosed(name) {
|
|
568
|
+
this.#measureSpeed();
|
|
527
569
|
// ITS SERVED NAME, which is what whoever owns the disk gives it in answer.
|
|
528
570
|
// ffmpeg writes a piece under a working name and reports that; the piece
|
|
529
571
|
// becomes servable by being renamed, and everything below works in the
|
|
@@ -548,9 +590,28 @@ export class EncodeRun {
|
|
|
548
590
|
return true;
|
|
549
591
|
}
|
|
550
592
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
593
|
+
/**
|
|
594
|
+
* A closed piece is the moment this run's speed is read: what it made since
|
|
595
|
+
* the last reading, over its own working time. Measured from the first
|
|
596
|
+
* closed piece rather than from the spawn, so the process start and the wait
|
|
597
|
+
* for its first input bytes, which are charged once as startup, are not
|
|
598
|
+
* counted as processing. A stretch with no progress keeps the earlier sample,
|
|
599
|
+
* and the next reading spans both.
|
|
600
|
+
*/
|
|
601
|
+
#measureSpeed() {
|
|
602
|
+
const sample = this.#workSample;
|
|
603
|
+
if (sample === null) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (this.#pieceSample === null) {
|
|
607
|
+
this.#pieceSample = sample;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const speed = speedFromWork(this.#pieceSample, sample);
|
|
611
|
+
if (speed !== null) {
|
|
612
|
+
this.#speedReading = { speed, at: sample.at };
|
|
613
|
+
this.#pieceSample = sample;
|
|
614
|
+
this.onSpeedMeasured(this.speedReading);
|
|
554
615
|
}
|
|
555
616
|
}
|
|
556
617
|
|
|
@@ -611,6 +672,7 @@ export class EncodeRun {
|
|
|
611
672
|
return false;
|
|
612
673
|
}
|
|
613
674
|
this.#transition(ENCODE_RUN_EVENT.SUSPEND_ORDERED);
|
|
675
|
+
this.clock.stopped();
|
|
614
676
|
this.logger.info(`encode-run #${this.from}..#${this.to} suspended — ${reason}`);
|
|
615
677
|
return true;
|
|
616
678
|
}
|
|
@@ -656,6 +718,7 @@ export class EncodeRun {
|
|
|
656
718
|
continued = false;
|
|
657
719
|
}
|
|
658
720
|
this.#transition(ENCODE_RUN_EVENT.RESUMED);
|
|
721
|
+
this.clock.continued();
|
|
659
722
|
return continued;
|
|
660
723
|
}
|
|
661
724
|
|