@torrent-tv/proxy 2.107.7 → 2.107.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.107.7",
3
+ "version": "2.107.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": {
@@ -905,7 +905,7 @@ export class ViewerRequests {
905
905
  const tracks = [];
906
906
  for (const output of outputs) {
907
907
  const trackProgress = output === session ? progress : this.#host.encodeRuns.progressOf(output);
908
- const trackRates = this.#trackRateReadings(output, now);
908
+ const trackRates = this.#trackRateReadings(output, now, trackProgress, measurement);
909
909
  const trackName = output === session ? "video" : "audio";
910
910
  // The page states the offset its player applied to this track, and the
911
911
  // init it applied it to is the session's. Until both are known, the
@@ -1244,7 +1244,8 @@ export class ViewerRequests {
1244
1244
  const id = typeof consumerId === "string" ? consumerId : "";
1245
1245
  let state = byViewer.get(id);
1246
1246
  if (!state) {
1247
- state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), sources: new Map() };
1247
+ state = { link: new this.#host.playbackReadiness.RateTrend(this.#host.lookaheadSeconds), sources: new Map(),
1248
+ trackProgress: new WeakMap() };
1248
1249
  byViewer.set(id, state);
1249
1250
  }
1250
1251
  return state;
@@ -1257,23 +1258,34 @@ export class ViewerRequests {
1257
1258
  * runs of one output can report out of order, so a summary kept and added to
1258
1259
  * would drop the later-told, earlier-taken one (torrent-tv/meta#178).
1259
1260
  */
1260
- #trackRateReadings(output, now) {
1261
+ #trackRateReadings(output, now, progress, measurement) {
1261
1262
  const readings = (this.#host.encodeSpeedReadingsOf?.(output) ?? []).filter(reading => reading.at <= now);
1262
1263
  const summary = readings.length > 0 ? this.#host.playbackReadiness.rateSummaryOf(readings) : null;
1263
- if (!summary) {
1264
- // ONLY UNTIL A PIECE IS MEASURED does the prediction stand for the
1265
- // encoder, and it is not kept among the readings. Added on every poll
1266
- // between runs, it put the machine's share of that moment — 1 % at a
1267
- // cold start — on the whole film, and the forecast read 0.03x
1268
- // (torrent-tv/meta#178).
1269
- const projected = Number(this.#host.predictedEncodeSpeedOf?.(output) ?? this.#host.projectedEncodeSpeedOf?.(output));
1270
- if (Number.isFinite(projected) && projected > 0) {
1271
- const seed = new this.#host.playbackReadiness.RateTrend();
1272
- seed.add(now, projected);
1273
- return seed.snapshot();
1264
+ if (summary) return summary;
1265
+ // ONLY UNTIL A PIECE IS MEASURED does the prediction stand for the
1266
+ // encoder, and it is not kept among the readings. Added on every poll
1267
+ // between runs, it put the machine's share of that moment — 1 % at a
1268
+ // cold start — on the whole film, and the forecast read 0.03x
1269
+ // (torrent-tv/meta#178).
1270
+ const projected = Number(this.#host.predictedEncodeSpeedOf?.(output) ?? this.#host.projectedEncodeSpeedOf?.(output));
1271
+ if (Number.isFinite(projected) && projected > 0) {
1272
+ const seed = new this.#host.playbackReadiness.RateTrend();
1273
+ seed.add(now, projected);
1274
+ return seed.snapshot();
1275
+ }
1276
+ // A separate soundtrack may have no calibrated codec reading and no
1277
+ // completed segment yet. Its progress is still a measured rate: retain the
1278
+ // time since the previous advance so source waits reduce, rather than
1279
+ // inflate, the speed used by the forecast.
1280
+ if (output?.spec?.carries === "audio-only") {
1281
+ let trend = measurement.trackProgress.get(output);
1282
+ if (!trend) {
1283
+ trend = new this.#host.playbackReadiness.ProgressRate(this.#host.lookaheadSeconds);
1284
+ measurement.trackProgress.set(output, trend);
1274
1285
  }
1286
+ return trend.note(now, progress?.processedSeconds, progress?.startPositionSeconds);
1275
1287
  }
1276
- return summary;
1288
+ return null;
1277
1289
  }
1278
1290
 
1279
1291
  /**
@@ -72,7 +72,7 @@ import { ViewerRequests } from "./ViewerRequests.js";
72
72
  import { OutputLifecycle } from "./OutputLifecycle.js";
73
73
  import { SegmentServing } from "./SegmentServing.js";
74
74
  import { audioStartSecondsFor } from "../viewer/audio-start.js";
75
- import { forecastRate, rateSummaryOf, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
75
+ import { forecastRate, rateSummaryOf, predictPlaybackReadiness, ProgressRate, RateTrend } from "../viewer/playback-readiness.js";
76
76
  import { audioRenditionName } from "../media/audio-inventory.js";
77
77
  import { Renditions } from "../encode/Renditions.js";
78
78
  import { CushionReport, LOOKAHEAD_PAUSE_SECONDS } from "../encode/CushionReport.js";
@@ -640,7 +640,7 @@ export function wireOutputs({
640
640
  get encodeRuns() { return parts.encodeRuns; },
641
641
  // When this viewer's playback can start and run to the end: the viewer
642
642
  // component's forecast, which models the viewer's browser.
643
- playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, rateSummaryOf, RateTrend },
643
+ playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, rateSummaryOf, RateTrend, ProgressRate },
644
644
  // Whether the subtitle a viewer chose has been read where they stand — the
645
645
  // source preparation's answer, set on these parts by the assembly.
646
646
  subtitleReadyFor: (viewer) => parts.subtitleReadyFor?.(viewer) ?? true,
@@ -108,6 +108,38 @@ export class RateTrend {
108
108
  }
109
109
  }
110
110
 
111
+ /**
112
+ * Rates the media an output actually produced between progress reports.
113
+ * Unchanged progress keeps its original time, so a source wait is included
114
+ * instead of being mistaken for encoder throughput.
115
+ */
116
+ export class ProgressRate {
117
+ #trend;
118
+ #horizonSeconds;
119
+ #previous = null;
120
+
121
+ constructor(horizonSeconds) {
122
+ this.#horizonSeconds = horizonSeconds;
123
+ this.#trend = new RateTrend(horizonSeconds);
124
+ }
125
+
126
+ note(at, processedSeconds, startPositionSeconds) {
127
+ if (!Number.isFinite(at) || !Number.isFinite(processedSeconds)) return this.#trend.snapshot();
128
+ const start = Number.isFinite(startPositionSeconds) ? startPositionSeconds : 0;
129
+ if (!this.#previous || this.#previous.start !== start || processedSeconds < this.#previous.processedSeconds) {
130
+ this.#previous = { at, processedSeconds, start };
131
+ this.#trend = new RateTrend(this.#horizonSeconds);
132
+ return this.#trend.snapshot();
133
+ }
134
+ const elapsedMs = at - this.#previous.at;
135
+ if (processedSeconds > this.#previous.processedSeconds && elapsedMs > 0) {
136
+ this.#trend.add(at, (processedSeconds - this.#previous.processedSeconds) * 1000 / elapsedMs);
137
+ this.#previous = { at, processedSeconds, start };
138
+ }
139
+ return this.#trend.snapshot();
140
+ }
141
+ }
142
+
111
143
  /**
112
144
  * @param {object} input
113
145
  * @param {number} input.positionSeconds