@torrent-tv/proxy 2.98.9 → 2.98.10

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.9",
3
+ "version": "2.98.10",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -497,15 +497,14 @@ export class EncodeRuns {
497
497
  }
498
498
 
499
499
  /**
500
- * The speed readings of this output's runs measured after `after`, oldest
501
- * first, within the forecast's horizon.
500
+ * The speed readings of this output's runs within the forecast's horizon,
501
+ * in the order they were taken.
502
502
  *
503
503
  * @param {object} output
504
- * @param {number} [after] - The time of the last reading the asker has.
505
504
  * @returns {Array<{ speed: number, at: number }>}
506
505
  */
507
- speedReadingsOf(output, after) {
508
- return this.#speedReadings.after(output, after);
506
+ speedReadingsOf(output) {
507
+ return this.#speedReadings.windowOf(output);
509
508
  }
510
509
 
511
510
  /**
@@ -2,8 +2,11 @@
2
2
  * @file The speed readings of each output's runs, in the order the runs
3
3
  * measured them, over the start forecast's horizon (torrent-tv/meta#178).
4
4
  *
5
- * A run announces a reading at each closed piece; whoever forecasts takes every
6
- * reading after the last one it has. The forecast used to take only the newest
5
+ * A run announces a reading at each closed piece; the forecast summarizes the
6
+ * whole window on each poll. Two runs of one output can announce out of order —
7
+ * each reading carries the time of its own run's last progress report — so the
8
+ * window is kept in the order the readings were TAKEN, and two taken in the
9
+ * same millisecond are two readings. The forecast used to take only the newest
7
10
  * reading of a run alive when it asked: on a cold start on Home Assistant the
8
11
  * runs were three pieces long and ended between its polls, and of the readings
9
12
  * 0.78, 1.80, 2.25, 2.84 and 2.69x it got 0.78 and 2.69x, and held the picture
@@ -28,18 +31,19 @@ export class SpeedReadings {
28
31
  if (!output || !(reading?.speed > 0) || !Number.isFinite(reading.at)) return;
29
32
  let readings = this.#byOutput.get(output);
30
33
  if (!readings) this.#byOutput.set(output, readings = []);
31
- readings.push({ speed: reading.speed, at: reading.at });
32
- const cutoff = reading.at - this.#keepMs;
34
+ let place = readings.length;
35
+ while (place > 0 && readings[place - 1].at > reading.at) place -= 1;
36
+ readings.splice(place, 0, { speed: reading.speed, at: reading.at });
37
+ const cutoff = readings.at(-1).at - this.#keepMs;
33
38
  // As the forecast's own trend keeps them: the newest one before the horizon stays.
34
39
  while (readings.length > 1 && readings[1].at <= cutoff) readings.shift();
35
40
  }
36
41
 
37
42
  /**
38
43
  * @param {object} output
39
- * @param {number} [after] - The time of the last reading the asker has.
40
- * @returns {Array<{ speed: number, at: number }>} Oldest first.
44
+ * @returns {Array<{ speed: number, at: number }>} The window, in the order the readings were taken.
41
45
  */
42
- after(output, after = Number.NEGATIVE_INFINITY) {
43
- return (this.#byOutput.get(output) ?? []).filter(reading => reading.at > after);
46
+ windowOf(output) {
47
+ return [...(this.#byOutput.get(output) ?? [])];
44
48
  }
45
49
  }
@@ -604,7 +604,7 @@ export class ViewerRequests {
604
604
  const tracks = [];
605
605
  for (const output of outputs) {
606
606
  const trackProgress = output === session ? progress : this.#host.encodeRuns.progressOf(output);
607
- const trackRates = this.#trackRateReadings(measurement, output, now);
607
+ const trackRates = this.#trackRateReadings(output, now);
608
608
  const trackName = output === session ? "video" : "audio";
609
609
  // The page states the offset its player applied to this track, and the
610
610
  // init it applied it to is the session's. Until both are known, the
@@ -691,12 +691,6 @@ export class ViewerRequests {
691
691
  });
692
692
  }
693
693
 
694
- const activeTrackIds = new Set(tracks.map(({ id }) => id));
695
- for (const id of measurement.tracks.keys()) {
696
- if (!activeTrackIds.has(id)) {
697
- measurement.tracks.delete(id);
698
- }
699
- }
700
694
  const bufferedAheadSeconds = Number(viewerReading?.bufferedAheadSec);
701
695
  const positionSeconds = Number.isFinite(viewerReading?.positionSeconds)
702
696
  ? viewerReading.positionSeconds
@@ -880,25 +874,23 @@ export class ViewerRequests {
880
874
  const id = typeof consumerId === "string" ? consumerId : "";
881
875
  let state = byViewer.get(id);
882
876
  if (!state) {
883
- state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), tracks: new Map(), sources: new Map() };
877
+ state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), sources: new Map() };
884
878
  byViewer.set(id, state);
885
879
  }
886
880
  return state;
887
881
  }
888
882
 
889
- #trackRateReadings(state, output, now) {
890
- const id = output.outputKey;
891
- let trend = state.tracks.get(id);
892
- if (!trend) {
893
- trend = new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds);
894
- state.tracks.set(id, trend);
895
- }
896
- // Every reading since this trend's last, not the newest one at the poll:
897
- // a run that ended between two polls took its readings with it.
898
- for (const reading of this.#host.encodeSpeedReadingsOf?.(output, trend.lastAt) ?? []) {
899
- if (reading.at <= now) trend.add(reading.at, reading.speed);
900
- }
901
- if (!trend.snapshot()) {
883
+ /**
884
+ * The encoder's rate for the forecast: the output's whole window of readings,
885
+ * summarized again on each poll. A run that ended between two polls took its
886
+ * readings with it when the forecast asked for the newest one alone; and two
887
+ * runs of one output can report out of order, so a summary kept and added to
888
+ * would drop the later-told, earlier-taken one (torrent-tv/meta#178).
889
+ */
890
+ #trackRateReadings(output, now) {
891
+ const readings = (this.#host.encodeSpeedReadingsOf?.(output) ?? []).filter(reading => reading.at <= now);
892
+ const summary = readings.length > 0 ? this.#host.playbackReadiness.rateSummaryOf(readings) : null;
893
+ if (!summary) {
902
894
  // ONLY UNTIL A PIECE IS MEASURED does the prediction stand for the
903
895
  // encoder, and it is not kept among the readings. Added on every poll
904
896
  // between runs, it put the machine's share of that moment — 1 % at a
@@ -911,7 +903,7 @@ export class ViewerRequests {
911
903
  return seed.snapshot();
912
904
  }
913
905
  }
914
- return trend.snapshot();
906
+ return summary;
915
907
  }
916
908
 
917
909
  /**
@@ -71,7 +71,7 @@ import { ViewerRequests } from "./ViewerRequests.js";
71
71
  import { OutputLifecycle } from "./OutputLifecycle.js";
72
72
  import { SegmentServing } from "./SegmentServing.js";
73
73
  import { audioStartSecondsFor } from "../viewer/audio-start.js";
74
- import { forecastRate, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
74
+ import { forecastRate, rateSummaryOf, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
75
75
  import { audioRenditionName } from "../media/audio-inventory.js";
76
76
  import { Renditions } from "../encode/Renditions.js";
77
77
  import { CushionReport, LOOKAHEAD_PAUSE_SECONDS } from "../encode/CushionReport.js";
@@ -578,12 +578,12 @@ export function wireOutputs({
578
578
  get encodeRuns() { return parts.encodeRuns; },
579
579
  // When this viewer's playback can start and run to the end: the viewer
580
580
  // component's forecast, which models the viewer's browser.
581
- playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, RateTrend },
581
+ playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, rateSummaryOf, RateTrend },
582
582
  // Whether the subtitle a viewer chose has been read where they stand — the
583
583
  // source preparation's answer, set on these parts by the assembly.
584
584
  subtitleReadyFor: (viewer) => parts.subtitleReadyFor?.(viewer) ?? true,
585
585
  sourceInputsFor: (output, index) => parts.sourceInputsFor?.(output, index),
586
- encodeSpeedReadingsOf: (output, after) => parts.encodeRuns.speedReadingsOf(output, after),
586
+ encodeSpeedReadingsOf: (output) => parts.encodeRuns.speedReadingsOf(output),
587
587
  projectedEncodeSpeedOf: (output) => parts.encodeCost.projectedSpeedOf(output),
588
588
  // The speed the offer predicted for this output when it chose its mode: a
589
589
  // forecast with no measured piece yet stands on it rather than on the
@@ -58,6 +58,22 @@ export function forecastRate(readings, now = Date.now(), horizonSeconds = 0, wei
58
58
  return horizon > 0 ? curve.workBy(horizon) / horizon : curve.rateAt(0);
59
59
  }
60
60
 
61
+ /**
62
+ * The summary the forecast reads, of a window of readings taken in any order:
63
+ * ordered by when each was taken, readings taken in the same millisecond kept
64
+ * apart. Null for no reading.
65
+ *
66
+ * @param {Array<{ at: number, speed?: number, value?: number }>} readings
67
+ * @returns {object | null}
68
+ */
69
+ export function rateSummaryOf(readings) {
70
+ const ordered = (Array.isArray(readings) ? readings : [])
71
+ .map(reading => ({ at: reading?.at, value: reading?.value ?? reading?.speed }))
72
+ .filter(reading => Number.isFinite(reading.at) && Number.isFinite(reading.value) && reading.value >= 0)
73
+ .sort((left, right) => left.at - right.at);
74
+ return ordered.length > 0 ? summarizeRate(ordered) : null;
75
+ }
76
+
61
77
  /**
62
78
  * Measurements over the output's existing look-ahead horizon. The oldest
63
79
  * boundary sample is retained until another sample replaces it, so a repeated
@@ -84,11 +100,6 @@ export class RateTrend {
84
100
  return true;
85
101
  }
86
102
 
87
- /** The time of the newest reading kept, or -Infinity. */
88
- get lastAt() {
89
- return this.#readings.at(-1)?.at ?? Number.NEGATIVE_INFINITY;
90
- }
91
-
92
103
  snapshot() {
93
104
  if (this.#readings.length === 0) {
94
105
  return null;