@torrent-tv/proxy 2.89.3 → 2.89.5

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 (50) hide show
  1. package/AGENTS.md +338 -336
  2. package/CHANGELOG.md +2319 -2303
  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 +1294 -1255
  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-closed-on-both-branches.test.js +114 -96
  46. package/test/segment-store.test.js +15 -15
  47. package/test/stream-route.test.js +28 -0
  48. package/test/supply-margin.test.js +13 -0
  49. package/test/two-viewers-one-picture.test.js +1 -1
  50. package/test/viewer-outputs.test.js +1 -1
@@ -24,21 +24,6 @@
24
24
 
25
25
  import { AUDIO_TRANSCODE_KBPS, maxrateKbpsFor, nominalKbpsFor } from "../args.js";
26
26
 
27
- /**
28
- * How much of a measured link may be spent on the stream.
29
- *
30
- * The rest is what the link does when it is not being perfect: transport
31
- * framing, retransmission, the other tabs, the moment somebody else in the
32
- * house starts something. A load sized to the whole reading stalls on the
33
- * first of those.
34
- *
35
- * Chosen, not measured, and written as a constant rather than dressed up as a
36
- * measurement. What would replace it is a reading of the transport's own
37
- * overhead and of how far a link's throughput varies over a session, which
38
- * nothing takes.
39
- */
40
- export const LINK_SAFETY = 0.8;
41
-
42
27
  /** How far a figure in a load can be trusted. */
43
28
  export const PEAK_CLASS = Object.freeze({
44
29
  KNOWN: "known",
@@ -279,7 +264,11 @@ export function linkCouldCarry(linkMbps, load) {
279
264
  if (load.peakClass === PEAK_CLASS.UNKNOWN) {
280
265
  return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
281
266
  }
282
- if (linkMbps * LINK_SAFETY < load.totalMbps) {
267
+ // The measured link against the whole load, with no share held back. The
268
+ // reading is what the link carried while it carried a piece, and the stream
269
+ // needs that on average; what a link does when it is not perfect — a pause,
270
+ // another tab — is what the viewer's buffer and `drain-threat.js` measure.
271
+ if (linkMbps < load.totalMbps) {
283
272
  return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
284
273
  }
285
274
  return load.peakClass === PEAK_CLASS.KNOWN
@@ -338,7 +327,6 @@ export function linkAnswerFigures(answer) {
338
327
  return {
339
328
  verdict: answer.verdict,
340
329
  linkMbps: answer.linkMbps,
341
- linkSafety: LINK_SAFETY,
342
330
  videoMbps: answer.load.video?.mbps ?? null,
343
331
  videoClass: answer.load.video?.peakClass ?? null,
344
332
  audioMbps: answer.load.audio?.mbps ?? null,
@@ -11,6 +11,9 @@
11
11
  * sized in seconds of playback by whoever knows the file's byte rate.
12
12
  */
13
13
 
14
+ /** The query parameter naming which run of an output a read feeds. */
15
+ const RUN_PARAMETER = "run";
16
+
14
17
  /**
15
18
  * @param {object} params
16
19
  * @param {{ fileIndex: number, streamUrl: (baseUrl: string, options?: { sessionId?: string }) => URL }} params.picture
@@ -20,15 +23,23 @@
20
23
  * @param {"video-only" | "audio-only" | "muxed" | "empty"} params.carries
21
24
  * @param {boolean} params.audioSeparate - The picture's sound travels as its own output.
22
25
  * @param {string} params.sessionId - Which output this read feeds.
26
+ * @param {number} [params.runToken] - Which run of that output the read feeds,
27
+ * so the time its input waits is charged to that run and not to another one
28
+ * working on the same output.
23
29
  * @param {number} params.readWindowBytes - Zero when no window was measured.
24
30
  * @param {string} params.baseUrl
25
31
  * @returns {{ inputFile: object, audioFile: object, inputUrl: string, audioInputUrl: string }}
26
32
  */
27
- export function encoderInputs({ picture, soundtrack, carries, audioSeparate, sessionId, readWindowBytes, baseUrl }) {
33
+ export function encoderInputs({ picture, soundtrack, carries, audioSeparate, sessionId, runToken, readWindowBytes, baseUrl }) {
28
34
  // A soundtrack shipped as its own file is encoded FROM that file, so a
29
35
  // rendition of it reads nothing else.
30
36
  const inputFile = carries === "audio-only" && soundtrack !== picture ? soundtrack : picture;
31
37
  const input = inputFile.streamUrl(baseUrl, { sessionId });
38
+ const forRun = (url) => {
39
+ if (Number.isInteger(runToken)) url.searchParams.set(RUN_PARAMETER, String(runToken));
40
+ return url;
41
+ };
42
+ forRun(input);
32
43
  if (Number.isFinite(readWindowBytes) && readWindowBytes > 0) {
33
44
  input.searchParams.set("windowBytes", String(readWindowBytes));
34
45
  input.searchParams.set("reader", "playback");
@@ -36,7 +47,7 @@ export function encoderInputs({ picture, soundtrack, carries, audioSeparate, ses
36
47
  // The second input exists only for a picture that carries its sound inside
37
48
  // itself when that sound is in another file.
38
49
  const audioInputUrl = carries !== "audio-only" && !audioSeparate && soundtrack !== inputFile
39
- ? soundtrack.streamUrl(baseUrl, { sessionId }).toString()
50
+ ? forRun(soundtrack.streamUrl(baseUrl, { sessionId })).toString()
40
51
  : "";
41
52
  return { inputFile, audioFile: soundtrack, inputUrl: input.toString(), audioInputUrl };
42
53
  }
@@ -607,7 +607,9 @@ export class SegmentServing {
607
607
  : this.#host.runStartTimeFor(session, this.#host.encodeRuns.earliestStartOf(session) ?? 0);
608
608
  const position = Number(progress?.processedSeconds);
609
609
  const produced = Number.isFinite(position) ? position - runStartSeconds : null;
610
- const speed = progress?.speed || "n/a";
610
+ const measured = this.#host.encodeRuns.liveRunsOf(session)
611
+ .reduce((best, run) => Math.max(best, run.speedX || 0), 0);
612
+ const speed = measured > 0 ? `${measured.toFixed(2)}x` : "n/a";
611
613
  logger.warn(
612
614
  `transcode ${session.id} holding ${fileName}: ${reason} ` +
613
615
  `(runs from #${this.#host.encodeRuns.earliestStartOf(session) ?? "?"}, viewer at #${this.#host.outputTimes.segmentIndexForTime(session, this.#host.viewerSecondsOn(session))}, ` +
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { logger } from "../../utils/logger.js";
11
- import { isOutputName, SEGMENT_CUT_TIME_DELTA_SECONDS } from "../encode/output/index.js";
11
+ import { isOutputName } from "../encode/output/index.js";
12
12
 
13
13
  function isWarmupTimeoutError(error) {
14
14
  if (!(error instanceof Error)) {
@@ -484,7 +484,6 @@ export class ViewerRequests {
484
484
  // segment (the only thing it waits for before playback starts) instead
485
485
  // of a percentage of the whole-file transcode.
486
486
  segmentDurationSec: this.#host.segmentDurationSec,
487
- speed: progress.speed,
488
487
  outputMbps,
489
488
  playbackReadiness,
490
489
  // The height the viewer is WATCHING right now, which is what the menu
@@ -566,11 +565,7 @@ export class ViewerRequests {
566
565
  const timeline = output.timeline;
567
566
  const trackName = output === session ? "video" : "audio";
568
567
  const nativeRanges = Array.from({ length: timeline?.segmentCount ?? 0 }, (_, index) =>
569
- this.#host.segmentStore.mediaRangesOf(output.outputKey, index, {
570
- startSeconds: timeline.publishedStartOf(index),
571
- endSeconds: index < timeline.segmentCount - 1 ?
572
- timeline.publishedStartOf(index + 1) - SEGMENT_CUT_TIME_DELTA_SECONDS : undefined
573
- }));
568
+ this.#host.encodeRuns.finishedMediaRangesOf(output, index));
574
569
  // The page states the offset its player applied to this track, and the
575
570
  // init it applied it to is the session's. Until both are known, the
576
571
  // pieces stay on their own presentation timeline: there is nothing yet
@@ -247,7 +247,10 @@ export function wireOutputs({
247
247
  pathFor: (address) => parts.segmentStore.pathFor(address),
248
248
  initOf: (address) => parts.segmentStore.initOf(address),
249
249
  directoryFor: (address) => parts.segmentStore.directoryFor(address),
250
- publish: (address, makingName, format, coverage) => parts.segmentStore.publish(address, makingName, format, coverage)
250
+ publish: (address, makingName, format, read) => parts.segmentStore.publish(address, makingName, format, read),
251
+ closedBytesOf: (address, makingName) => parts.segmentStore.closedBytesOf(address, makingName),
252
+ mediaRangesOf: (address, index, where) => parts.segmentStore.mediaRangesOf(address, index, where),
253
+ remove: (address, index, because) => parts.segmentStore.remove(address, index, because)
251
254
  };
252
255
  const segmentOutputFiles = {
253
256
  addresses: () => parts.segmentStore.addresses(),
@@ -257,7 +260,8 @@ export function wireOutputs({
257
260
  useFormat: (address, format) => parts.segmentStore.useFormat(address, format)
258
261
  };
259
262
  const segmentPaths = {
260
- pathOf: (address, index) => parts.segmentStore.pathOf(address, index)
263
+ pathOf: (address, index) => parts.segmentStore.pathOf(address, index),
264
+ sizesOf: (address) => parts.segmentStore.sizesOf(address)
261
265
  };
262
266
  /**
263
267
  * Whether everything this output will ever serve is made: every segment of
@@ -303,6 +307,11 @@ export function wireOutputs({
303
307
  // The encoders of this proxy: built where the plan places them, followed while they run, accounted when they end.
304
308
  parts.encodeRuns = new EncodeRuns({
305
309
  logger,
310
+ // What this host is doing with an encode is learned each time a run has
311
+ // measured its processing speed, on every host.
312
+ noteRunSpeedMeasured: (session, run) => {
313
+ void parts.encodeCost.learnFrom(session, run);
314
+ },
306
315
  // What an admitted encode was seen to do, filed under its configuration.
307
316
  observeEncodeEnded: (session) => {
308
317
  if (!parts.localObservations || !session?.spec?.video?.encode) {
@@ -317,7 +326,6 @@ export function wireOutputs({
317
326
  });
318
327
  },
319
328
  viewerSecondsOn: (output, consumerId, now) => viewerSecondsOn(parts.viewers, output, consumerId, now),
320
- noteRunStarting: (output) => parts.quality.noteRunStarting(output),
321
329
  inputOf: (...args) => parts.renditions.inputOf(...args),
322
330
  producedNumbers: (...args) => parts.serving.producedNumbers(...args),
323
331
  servesAudioSeparately: (...args) => parts.renditions.servesAudioSeparately(...args),
@@ -809,11 +817,8 @@ export function wireOutputs({
809
817
  runningEncoders: () => parts.encodeOrchestrator.runningCount(),
810
818
  encodersRunningNow: () => parts.encodeOrchestrator.runningCount(),
811
819
  torrentCostSecFor: (session) => parts.hostLoad.torrentCostSecFor(session),
812
- boundBy: (session) => parts.quality.classifyTranscodeBound(session),
813
820
  runsFor: (session) => parts.encodeRuns.runsOf(session),
814
- stateFor: (session) => parts.encodeRuns.runStateOf(session),
815
- // The run's OWN progress: a speed is a pair of readings of one process.
816
- progressFor: (_session, run) => run?.progress?.snapshot?.() ?? null
821
+ stateFor: (session) => parts.encodeRuns.runStateOf(session)
817
822
  });
818
823
  // WHICH HEIGHTS ARE ON THE MENU, which is the arithmetic above plus three
819
824
  // things that are nothing to do with it: whose answer it is, what may never