@torrent-tv/proxy 2.99.9 → 2.99.11

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.99.9",
3
+ "version": "2.99.11",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -582,6 +582,10 @@ export class ViewerRequests {
582
582
 
583
583
  async #playbackReadinessFor(session, consumerId, progress, outputMbps) {
584
584
  const now = Date.now();
585
+ // WHERE AN ANSWER'S TIME GOES, said with each answer. On Home Assistant a
586
+ // held viewer's answer took 11.2 s while its forecast took 0.1-0.4 s and
587
+ // the answer crossed the connection in 21 ms (r50, torrent-tv/meta#178).
588
+ const parts = { enteredAt: performance.now(), statsMs: 0, workerForecastMs: 0, qualityMs: 0 };
585
589
  const viewer = consumerId ? this.#host.viewers.get(consumerId) : null;
586
590
  const viewerReading = viewer?.linkReading() ?? null;
587
591
  const measurement = this.#rateStateFor(session, consumerId);
@@ -629,12 +633,15 @@ export class ViewerRequests {
629
633
  continue;
630
634
  }
631
635
  let stats = null;
636
+ const statsAt = performance.now();
632
637
  try {
633
638
  stats = await this.#host.getSourceStats?.(output.file.sourceKey, fileIndex,
634
639
  { forecast: !sourceForecasts.has(output.file.sourceKey) }) ?? null;
635
640
  } catch {
636
641
  stats = null;
637
642
  }
643
+ parts.statsMs += performance.now() - statsAt;
644
+ parts.workerForecastMs += Number(stats?.forecastMs) || 0;
638
645
  if (!sourceForecasts.has(output.file.sourceKey)) sourceForecasts.set(output.file.sourceKey, stats?.downloadForecast ?? { ranges: [] });
639
646
  const sharedForecast = sourceForecasts.get(output.file.sourceKey);
640
647
  const downloadForecast = sharedForecast.global ? { measuredAt: sharedForecast.measuredAt,
@@ -655,9 +662,11 @@ export class ViewerRequests {
655
662
  });
656
663
  }
657
664
 
665
+ const qualityAt = performance.now();
658
666
  const observedMbps = output === session
659
667
  ? outputMbps
660
668
  : await this.#host.quality.observedStreamMbps(output);
669
+ parts.qualityMs += performance.now() - qualityAt;
661
670
  const liveRuns = this.#host.encodeRuns.liveRunsOf?.(output) ?? [];
662
671
  const processingStarted = liveRuns.some(({ progress }) => progress.processedSeconds > progress.startPositionSeconds);
663
672
  const startedAt = liveRuns.find(({ startedAt }) => startedAt > 0)?.startedAt ?? now;
@@ -734,7 +743,11 @@ export class ViewerRequests {
734
743
  viewer.subtitle.fileIndex === session.file.fileIndex ? viewer.subtitle : null;
735
744
  const startedAt = performance.now();
736
745
  const predicted = this.#host.playbackReadiness.predict(input);
737
- this.#noteForecast(performance.now() - startedAt, tracks.map(track => track.geometry?.reason ?? "none"));
746
+ const predictMs = performance.now() - startedAt;
747
+ this.#noteForecast(predictMs, tracks.map(track => track.geometry?.reason ?? "none"));
748
+ logger.info(`readiness parts ${session.id} consumer=${consumerId}: source stats ${Math.round(parts.statsMs)}ms ` +
749
+ `(the torrent thread's arrival forecast ${Math.round(parts.workerForecastMs)}ms), stream rate ${Math.round(parts.qualityMs)}ms, ` +
750
+ `start forecast ${Math.round(predictMs)}ms, in all ${Math.round(performance.now() - parts.enteredAt)}ms`);
738
751
  const forecast = withSubtitleReadiness(predicted, subtitle,
739
752
  subtitle ? this.#host.subtitleReadyFor?.(viewer) !== false : true);
740
753
  forecast.operations = outputs.flatMap((output, index) => {
@@ -30,19 +30,44 @@ let publishingSelections = false;
30
30
  /** What the last reconciliation told the swarm to fetch first, for the pool to say. */
31
31
  let firstClassStated = "none";
32
32
 
33
- /** Concurrent file reports share one snapshot of every live download map. */
33
+ /** The last forecast that finished on the maps it was begun on, and how to tell it still describes them. */
34
+ let latestForecast = null;
35
+
36
+ /**
37
+ * The arrival forecast of every live download map, shared by concurrent file
38
+ * reports.
39
+ *
40
+ * ANSWERED WITH THE LAST ONE FINISHED. Each forecast states when it was made
41
+ * (`measuredAt`), and a newer one is worked out beside the answer whenever the
42
+ * maps or the held pieces have changed since, one at a time. Awaited afresh on
43
+ * every report, it held a waiting viewer's answer 3-11.6 s on Home Assistant
44
+ * while it took about a quarter of a second of work, the machine 83-94 % busy:
45
+ * the page asks again only once answered, so the hold after a seek to 100 s
46
+ * lasted 52.5 s (r51, torrent-tv/meta#178). Only the first report waits, when
47
+ * there is no forecast yet. A forecast whose maps change while it is worked out
48
+ * is not kept.
49
+ *
50
+ * @returns {Promise<Map<object, { measuredAt: number, arrivals: Map<number, number | null> }>>}
51
+ */
34
52
  export function forecastDownloads() {
35
- if (!futurePending) {
53
+ if (!futurePending && !latestForecast?.isCurrent()) {
36
54
  const entries = [...live];
37
55
  const revisions = entries.map(entry => entry.register.revision);
38
56
  const withdrawals = entries.map(entry => entry.withdrawalRevision);
57
+ const verified = entries.map(entry => entry.verifiedRevision);
39
58
  const isCurrent = () => entries.length === live.size && !entries.some((entry, index) =>
40
59
  !live.has(entry) || entry.register.revision !== revisions[index] ||
41
60
  entry.withdrawalRevision !== withdrawals[index]);
42
- futurePending = futureDownload(entries, { isCurrent }).then(result =>
43
- isCurrent() ? result : new Map()).finally(() => { futurePending = null; });
61
+ // Kept while no piece has arrived either: a piece forecast to come has
62
+ // either come, or its time is past and it must be forecast again.
63
+ const describes = () => isCurrent() && entries.every((entry, index) => entry.verifiedRevision === verified[index]);
64
+ futurePending = futureDownload(entries, { isCurrent }).then(result => {
65
+ if (!isCurrent()) return new Map();
66
+ latestForecast = { result, isCurrent: describes };
67
+ return result;
68
+ }).finally(() => { futurePending = null; });
44
69
  }
45
- return futurePending;
70
+ return latestForecast ? Promise.resolve(latestForecast.result) : futurePending;
46
71
  }
47
72
 
48
73
  /**
@@ -58,10 +83,11 @@ export function demandFor(torrent) {
58
83
  }
59
84
  const register = new DemandRegister();
60
85
  const selection = new SwarmSelection({ torrent, register });
61
- const entry = { torrent, register, selection, limit: new SelectionLimit({ torrent, register, selection }), withdrawalRevision: 0 };
86
+ const entry = { torrent, register, selection, limit: new SelectionLimit({ torrent, register, selection }), withdrawalRevision: 0,
87
+ verifiedRevision: 0 };
62
88
  byTorrent.set(torrent, entry);
63
89
  live.add(entry);
64
- entry.onBytesChanged = () => reconcileAll();
90
+ entry.onBytesChanged = () => { entry.verifiedRevision++; reconcileAll(); };
65
91
  entry.onWithdrawn = () => { entry.withdrawalRevision++; reconcileAll(); };
66
92
  torrent.on?.("verified", entry.onBytesChanged);
67
93
  torrent.on?.("piece-withdrawn", entry.onWithdrawn);
@@ -472,7 +472,9 @@ async function runCommand(command, params, id) {
472
472
  // second interruption has been seen: one wait shows no interval, and an
473
473
  // interval invented from one point is exactly what this work removes.
474
474
  const file = Array.isArray(torrent?.files) ? torrent.files[params.fileIndex] : null;
475
+ const forecastAt = performance.now();
475
476
  const forecast = file && params.forecast === true ? (await forecastDownloads()).get(torrent) : null;
477
+ const forecastMs = params.forecast === true ? performance.now() - forecastAt : 0;
476
478
  const stats = pool.getFileStats(torrent, params.fileIndex);
477
479
  const ranges = [];
478
480
  if (forecast) for (const [piece, availableAt] of forecast.arrivals) {
@@ -485,7 +487,10 @@ async function runCommand(command, params, id) {
485
487
  ...(params.forecast === true ? { downloadForecast: {
486
488
  measuredAt: forecast?.measuredAt ?? Date.now(), global: true, ranges
487
489
  } } : {}),
488
- supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4)
490
+ supply: supplyFiguresFor(torrent?.infoHash, file?.name, params.segmentSeconds ?? 4),
491
+ // How long this answer waited for the arrival forecast, which the
492
+ // caller says beside its own time (ViewerRequests, torrent-tv/meta#178).
493
+ forecastMs
489
494
  };
490
495
  }
491
496