@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.
Files changed (49) hide show
  1. package/AGENTS.md +338 -336
  2. package/CHANGELOG.md +12 -0
  3. package/docs/encode-architecture.md +55 -19
  4. package/docs/playback-start-forecast.md +13 -5
  5. package/docs/storage-architecture.md +6 -0
  6. package/package.json +1 -1
  7. package/routes/stream/get.js +38 -2
  8. package/server.js +748 -745
  9. package/services/encode/EncodeOrchestrator.js +24 -49
  10. package/services/encode/EncodePlan.js +5 -3
  11. package/services/encode/EncodeRun.js +80 -17
  12. package/services/encode/EncodeRuns.js +1339 -1239
  13. package/services/encode/Renditions.js +2 -1
  14. package/services/encode/RunClock.js +87 -0
  15. package/services/encode/RunProgress.js +1 -6
  16. package/services/encode/encoder-readings.js +26 -28
  17. package/services/encode/piece-completeness.js +46 -0
  18. package/services/encode/quality/EncodeCost.js +1043 -1167
  19. package/services/encode/quality/OutputQualityState.js +1 -5
  20. package/services/encode/quality/QualityController.js +213 -390
  21. package/services/encode/quality/link-budget.js +5 -17
  22. package/services/encode/run-inputs.js +13 -2
  23. package/services/server/SegmentServing.js +3 -1
  24. package/services/server/ViewerRequests.js +2 -7
  25. package/services/server/wire-outputs.js +12 -7
  26. package/services/storage/segment-store/SegmentStore.js +46 -29
  27. package/services/torrent/supply-margin.js +7 -5
  28. package/test/auto-quality-step.test.js +75 -29
  29. package/test/decode-cost.test.js +1 -1
  30. package/test/encode-cost.test.js +31 -39
  31. package/test/encode-orchestrator.test.js +15 -8
  32. package/test/encode-plan-viewers.test.js +34 -27
  33. package/test/encode-run.test.js +34 -6
  34. package/test/encoder-readings.test.js +48 -26
  35. package/test/helpers/encode-run.js +113 -9
  36. package/test/link-budget.test.js +4 -5
  37. package/test/manager-surface.test.js +1 -2
  38. package/test/orchestrator-wired.test.js +5 -4
  39. package/test/output-speed.test.js +0 -1
  40. package/test/piece-completeness.test.js +42 -0
  41. package/test/quality-offer.test.js +1 -3
  42. package/test/quality-variants.test.js +1 -1
  43. package/test/run-progress.test.js +2 -3
  44. package/test/same-height-switch.test.js +1 -2
  45. package/test/segment-store.test.js +15 -15
  46. package/test/stream-route.test.js +28 -0
  47. package/test/supply-margin.test.js +13 -0
  48. package/test/two-viewers-one-picture.test.js +1 -1
  49. 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
- // The best figure this host has: what a run here is doing now, what one was
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
- // The best figure this host has: what a run here is doing now, what one was
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 - Measured encode speed against realtime, from
69
- * ffmpeg's own progress. Zero or less means nothing has measured it yet, and
70
- * then no comparison involving its speed can be made.
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
- /** @type {number} */
154
- #speedX = 0;
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
- /** @returns {number} */
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.#speedX;
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, speed: null });
504
+ this.#reportProgress({ processedSeconds: numeric / MICROSECONDS_PER_SECOND });
464
505
  }
465
506
  } else if (key === "out_time") {
466
- this.#reportProgress({ processedSeconds: null, speed: null, outTime: value });
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
- noteSpeed(speedX) {
552
- if (Number.isFinite(speedX) && speedX > 0) {
553
- this.#speedX = speedX;
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