@torrent-tv/proxy 2.98.7 → 2.98.8

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.98.7",
3
+ "version": "2.98.8",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -60,7 +60,13 @@ export class ViewerRequests {
60
60
  #host;
61
61
  /** @type {WeakMap<object, Map<string, object>>} */
62
62
  #rateStateBySession = new WeakMap();
63
- /** The pieces of each output as the start forecast read them last; see `#segmentsOf`. */
63
+ /**
64
+ * The pieces of each output as the start forecast read them last (see
65
+ * `#segmentsOf`), and what the forecast keeps of their time geometry while
66
+ * they stand (`playback-readiness.js` `timescaleOfPieces`).
67
+ *
68
+ * @type {WeakMap<object, { pieces: object[], geometry: object }>}
69
+ */
64
70
  #forecastPieces = new WeakMap();
65
71
 
66
72
  /**
@@ -658,6 +664,7 @@ export class ViewerRequests {
658
664
  this.#host.initialOutputBitsPerMediaSecondOf?.(output) ?? 0,
659
665
  readings: trackRates,
660
666
  segments,
667
+ geometry: this.#forecastPieces.get(output)?.geometry,
661
668
  readySegmentIndices: [...inventory.keys()],
662
669
  segmentSizesBytes: inventory
663
670
  });
@@ -808,11 +815,12 @@ export class ViewerRequests {
808
815
  #segmentsOf(output, sessionInit, inventory, positionSeconds) {
809
816
  const timeline = output.timeline;
810
817
  const count = timeline?.segmentCount ?? 0;
811
- let kept = this.#forecastPieces.get(output);
812
- if (!kept) {
813
- kept = [];
814
- this.#forecastPieces.set(output, kept);
818
+ let forecast = this.#forecastPieces.get(output);
819
+ if (!forecast) {
820
+ forecast = { pieces: [], geometry: {} };
821
+ this.#forecastPieces.set(output, forecast);
815
822
  }
823
+ const kept = forecast.pieces;
816
824
  const segments = [];
817
825
  for (let index = 0; index < count; index++) {
818
826
  const endSeconds = timeline.publishedStartOf(index + 1);
@@ -139,21 +139,16 @@ export function predictPlaybackReadiness(input = {}) {
139
139
  range.end > range.start).map((range) => ({ start: fromSeconds(range.start), end: fromSeconds(range.end) }))
140
140
  : null);
141
141
  // One timescale in which every time of this forecast is a whole number of
142
- // ticks, so that adding and comparing never seek a common denominator.
142
+ // ticks, so that adding and comparing never seek a common denominator. A
143
+ // track's pieces give it the same timescale while they are the same pieces.
143
144
  const unit = commonTimescale([
144
145
  fromSeconds(finiteNonNegative(input.positionSeconds)), fromSeconds(duration),
145
146
  fromSeconds(finiteNonNegative(input.bufferedAheadSeconds)), REPORTED_TIME_ERROR, ...clocks,
146
147
  ...reported.flatMap((ranges) => (ranges ?? []).flatMap(({ start, end }) => [start, end])),
147
- ...tracks.flatMap((track) => (Array.isArray(track.segments) ? track.segments : []).flatMap((segment) => [
148
- ...(segment.mediaRanges?.timescale ? [mediaTime(0n, segment.mediaRanges.timescale)] : []),
149
- ...exactBoundsOf(segment)
150
- ]))
148
+ ...tracks.map((track) => mediaTime(0n, timescaleOfPieces(track)))
151
149
  ]);
152
150
  const zero = mediaTime(0n, unit);
153
- const segmentsOf = tracks.map((track, trackIndex) => predictedMediaSegments(
154
- (Array.isArray(track.segments) ? track.segments : []).map((segment) => ({
155
- ...segment, exact: exactTimesOf(segment), ranges: playerRanges(segment.mediaRanges, rescale(clocks[trackIndex], unit), unit)
156
- })), unit));
151
+ const segmentsOf = tracks.map((track, trackIndex) => placedPieces(track, rescale(clocks[trackIndex], unit), unit));
157
152
  const origin = latest(zero, ...segmentsOf.map((segments) =>
158
153
  segments.find(({ index }) => index === 0)?.ranges?.[0]?.start ?? zero));
159
154
  const at = latest(origin, rescale(fromSeconds(finiteNonNegative(input.positionSeconds)), unit));
@@ -645,10 +640,69 @@ function timesOf(segment) {
645
640
  return segment.exact ?? exactTimesOf(segment);
646
641
  }
647
642
 
648
- /** @returns {import("./media-time.js").MediaTime[]} The finite playlist edges of a piece, exactly. */
649
- function exactBoundsOf(segment) {
650
- const { start, end } = exactTimesOf(segment);
651
- return [start, end].filter(Boolean);
643
+ /**
644
+ * WHAT A TRACK'S PIECES GIVE THE FORECAST'S TIME, KEPT BETWEEN POLLS while
645
+ * they are the same pieces. The forecast runs on every poll of a waiting
646
+ * viewer, and gathering the edges of every remaining piece for the timescale,
647
+ * then placing each on the page's clock, was about 3 s of the proxy's main
648
+ * thread in r35's first 15 minutes on Home Assistant (torrent-tv/meta#178).
649
+ * `track.geometry` is kept by whoever keeps the pieces; without it nothing is
650
+ * kept, and the answer is the same either way: a placement is reused only for
651
+ * the same pieces, the same timescale and the same clock offset, so no time is
652
+ * rounded for the reuse.
653
+ *
654
+ * @param {{ segments?: object[], geometry?: object }} track
655
+ * @returns {bigint} The least timescale holding every edge and every served tick of its pieces.
656
+ */
657
+ function timescaleOfPieces(track) {
658
+ const segments = Array.isArray(track.segments) ? track.segments : [];
659
+ const geometry = track.geometry;
660
+ if (geometry && samePieces(geometry.pieces, segments)) return geometry.timescale;
661
+ const distinct = new Set();
662
+ for (const segment of segments) {
663
+ if (segment.mediaRanges?.timescale) distinct.add(segment.mediaRanges.timescale);
664
+ const { start, end } = exactTimesOf(segment);
665
+ if (start) distinct.add(start.timescale);
666
+ if (end) distinct.add(end.timescale);
667
+ }
668
+ const timescale = commonTimescale([...distinct].map((value) => mediaTime(0n, value)));
669
+ if (geometry) {
670
+ geometry.pieces = segments.map((segment) => ({ segment, index: segment.index, startSeconds: segment.startSeconds,
671
+ endSeconds: segment.endSeconds, mediaRanges: segment.mediaRanges }));
672
+ geometry.timescale = timescale;
673
+ geometry.placed = null;
674
+ }
675
+ return timescale;
676
+ }
677
+
678
+ /**
679
+ * A track's pieces on the page's clock in `unit`: read pieces at their served
680
+ * ranges, the others placed between them. Reused while the pieces are those
681
+ * {@link timescaleOfPieces} last took and `unit` and `clock` are the same.
682
+ *
683
+ * @param {{ segments?: object[], geometry?: object }} track
684
+ * @param {import("./media-time.js").MediaTime} clock - In `unit`.
685
+ * @param {bigint} unit
686
+ */
687
+ function placedPieces(track, clock, unit) {
688
+ const geometry = track.geometry;
689
+ if (geometry?.placed && geometry.placedUnit === unit && geometry.placedClock === clock.ticks) return geometry.placed;
690
+ const placed = predictedMediaSegments((Array.isArray(track.segments) ? track.segments : []).map((segment) => ({
691
+ ...segment, exact: exactTimesOf(segment), ranges: playerRanges(segment.mediaRanges, clock, unit)
692
+ })), unit);
693
+ if (geometry) Object.assign(geometry, { placed, placedUnit: unit, placedClock: clock.ticks });
694
+ return placed;
695
+ }
696
+
697
+ /** Whether `segments` are the pieces `kept` recorded, field by field the forecast reads. */
698
+ function samePieces(kept, segments) {
699
+ if (!Array.isArray(kept) || kept.length !== segments.length) return false;
700
+ for (let index = 0; index < segments.length; index++) {
701
+ const segment = segments[index], was = kept[index];
702
+ if (segment !== was.segment || segment.index !== was.index || segment.startSeconds !== was.startSeconds ||
703
+ segment.endSeconds !== was.endSeconds || segment.mediaRanges !== was.mediaRanges) return false;
704
+ }
705
+ return true;
652
706
  }
653
707
 
654
708
  /**
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  export class SegmentInputTable {
13
- /** `sourceKey` → output → segment → file → its ranges. @type {Map<string, Map<string, Map<number, Map<number, { joined: Array<[number, number]>, ranges: Array<{ start: number, end: number }> }>>>>} */
13
+ /** `sourceKey` → output → segment → its files' ranges, and the list `inputsFor` answers while they stand. @type {Map<string, Map<string, Map<number, { files: Map<number, { joined: Array<[number, number]>, ranges: Array<{ start: number, end: number }> }>, inputs: Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null }>>>} */
14
14
  #bySource = new Map();
15
15
 
16
16
  /**
@@ -25,15 +25,19 @@ export class SegmentInputTable {
25
25
  if (!outputs) this.#bySource.set(sourceKey, outputs = new Map());
26
26
  let segments = outputs.get(outputKey);
27
27
  if (!segments) outputs.set(outputKey, segments = new Map());
28
- let files = segments.get(index);
29
- if (!files) segments.set(index, files = new Map());
28
+ let segment = segments.get(index);
29
+ if (!segment) segments.set(index, segment = { files: new Map(), inputs: null });
30
30
  // The same reading keeps its list: whoever compares inputs compares them by it.
31
- if (files.get(fileIndex)?.joined === joined) return;
32
- files.set(fileIndex, { joined, ranges: joined.map(([start, end]) => ({ start, end: end + 1 })) });
31
+ if (segment.files.get(fileIndex)?.joined === joined) return;
32
+ segment.files.set(fileIndex, { joined, ranges: joined.map(([start, end]) => ({ start, end: end + 1 })) });
33
+ segment.inputs = null;
33
34
  }
34
35
 
35
36
  /**
36
- * The bytes one segment reads, by file, or null where none are known.
37
+ * The bytes one segment reads, by file, or null where none are known. The
38
+ * same list while they stand: the start forecast asks for every segment of
39
+ * the film on each poll, and keeps what it derived from a segment whose
40
+ * inputs are the same list.
37
41
  *
38
42
  * @param {string} sourceKey
39
43
  * @param {string} outputKey
@@ -41,9 +45,10 @@ export class SegmentInputTable {
41
45
  * @returns {Array<{ sourceId: string, ranges: Array<{ start: number, end: number }> }> | null}
42
46
  */
43
47
  inputsFor(sourceKey, outputKey, index) {
44
- const files = this.#bySource.get(sourceKey)?.get(outputKey)?.get(index);
45
- if (!files?.size) return null;
46
- return [...files].map(([fileIndex, { ranges }]) => ({ sourceId: `${sourceKey}:${fileIndex}`, ranges }));
48
+ const segment = this.#bySource.get(sourceKey)?.get(outputKey)?.get(index);
49
+ if (!segment?.files.size) return null;
50
+ segment.inputs ??= [...segment.files].map(([fileIndex, { ranges }]) => ({ sourceId: `${sourceKey}:${fileIndex}`, ranges }));
51
+ return segment.inputs;
47
52
  }
48
53
 
49
54
  /** @param {string} sourceKey */