@torrent-tv/proxy 2.102.1 → 2.102.2

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.102.1",
3
+ "version": "2.102.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -305,6 +305,16 @@ export class LocalObservations {
305
305
  * @returns {number | null}
306
306
  */
307
307
  firstFrameSeconds(features) {
308
+ return this.firstFrameEstimate(features)?.seconds ?? null;
309
+ }
310
+
311
+ /**
312
+ * Return the current stage-median forecast and the measurements that formed it.
313
+ *
314
+ * @param {object} features
315
+ * @returns {{ seconds: number, stagesMs: object, observations: number } | null}
316
+ */
317
+ firstFrameEstimate(features) {
308
318
  if (!features || typeof features !== "object") return null;
309
319
  const stages = ["transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
310
320
  const candidates = [];
@@ -319,7 +329,11 @@ export class LocalObservations {
319
329
  const prices = stages.map(stage => median(candidates.map(reading => reading[stage])
320
330
  .filter(Number.isFinite)));
321
331
  if (prices.some(value => value === null)) return null;
322
- return prices.reduce((sum, value) => sum + value, 0) / 1000;
332
+ return {
333
+ seconds: prices.reduce((sum, value) => sum + value, 0) / 1000,
334
+ stagesMs: Object.fromEntries(stages.map((stage, index) => [stage, prices[index]])),
335
+ observations: candidates.length
336
+ };
323
337
  }
324
338
 
325
339
  #firstFrameKey() {
@@ -125,11 +125,13 @@ export class ViewerRequests {
125
125
  const changed = this.#recordEarlyWaitSnapshot(context, context.features);
126
126
  if (changed) context.revision += 1;
127
127
  this.#pruneEarlyWaits();
128
- const totalSeconds = Number.isFinite(context.activeTotalSeconds)
129
- ? context.activeTotalSeconds
130
- : this.#host.firstFrameSecondsOf?.(context.features) ?? null;
131
- const seconds = earlyWaitRemainingSeconds(totalSeconds, waitElapsedSince(context, waitElapsedMs));
128
+ const elapsedMs = waitElapsedSince(context, waitElapsedMs);
129
+ const historical = Number.isFinite(context.activeTotalSeconds) ? null : this.#historicalFirstFrame(context.features);
130
+ const totalSeconds = Number.isFinite(context.activeTotalSeconds) ? context.activeTotalSeconds : historical?.seconds ?? null;
131
+ const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
132
132
  if (!Number.isFinite(seconds) || seconds < 0) return null;
133
+ this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
134
+ firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
133
135
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
134
136
  }
135
137
  const sameWait = current?.waitId === waitId;
@@ -139,11 +141,13 @@ export class ViewerRequests {
139
141
  const changed = this.#recordEarlyWaitSnapshot(context, context.features);
140
142
  if (sameWait && changed) context.revision += 1;
141
143
  this.#pruneEarlyWaits();
142
- const totalSeconds = Number.isFinite(context.activeTotalSeconds)
143
- ? context.activeTotalSeconds
144
- : this.#host.firstFrameSecondsOf?.(context.features) ?? null;
145
- const seconds = earlyWaitRemainingSeconds(totalSeconds, waitElapsedSince(context, waitElapsedMs));
144
+ const elapsedMs = waitElapsedSince(context, waitElapsedMs);
145
+ const historical = Number.isFinite(context.activeTotalSeconds) ? null : this.#historicalFirstFrame(context.features);
146
+ const totalSeconds = Number.isFinite(context.activeTotalSeconds) ? context.activeTotalSeconds : historical?.seconds ?? null;
147
+ const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
146
148
  if (!Number.isFinite(seconds) || seconds < 0) return null;
149
+ this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
150
+ firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
147
151
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
148
152
  }
149
153
 
@@ -192,20 +196,44 @@ export class ViewerRequests {
192
196
  ? elapsedMs / 1000 + measuredWorkSeconds
193
197
  : context.activeTotalSeconds;
194
198
  const activeEstimateChanged = hasActiveEstimate && activeTotalSeconds !== context.activeTotalSeconds;
195
- if (hasActiveEstimate) context.activeTotalSeconds = activeTotalSeconds;
199
+ if (hasActiveEstimate) {
200
+ context.activeTotalSeconds = activeTotalSeconds;
201
+ context.activeFirstRequiredSegmentsSeconds = measuredWorkSeconds;
202
+ }
196
203
  const changed = featuresChanged || activeEstimateChanged;
197
204
  if (sameWait && changed) context.revision += 1;
198
205
  this.#pruneEarlyWaits();
199
206
  const progressHasComponentForecast = Object.hasOwn(processing, "firstRequiredSegmentsSeconds");
200
207
  let totalSeconds = activeTotalSeconds;
208
+ let historical = null;
201
209
  if (!Number.isFinite(totalSeconds) && !progressHasComponentForecast) {
202
- totalSeconds = this.#host.firstFrameSecondsOf?.(context.features) ?? null;
210
+ historical = this.#historicalFirstFrame(context.features);
211
+ totalSeconds = historical?.seconds ?? null;
203
212
  }
204
213
  const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
205
214
  if (!Number.isFinite(seconds) || seconds < 0) return null;
215
+ this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical,
216
+ firstRequiredSegmentsSeconds: hasActiveEstimate ? measuredWorkSeconds : context.activeFirstRequiredSegmentsSeconds });
206
217
  return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
207
218
  }
208
219
 
220
+ #historicalFirstFrame(features) {
221
+ const estimate = this.#host.firstFrameSecondsOf?.(features) ?? null;
222
+ return Number.isFinite(estimate) ? { seconds: estimate, stagesMs: null, observations: null } : estimate;
223
+ }
224
+
225
+ #logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, historical, firstRequiredSegmentsSeconds }) {
226
+ if (context.loggedForecastRevision === context.revision) return;
227
+ const basis = Number.isFinite(firstRequiredSegmentsSeconds) ? "first-required" : "historical-stages";
228
+ const estimate = basis === "first-required"
229
+ ? { firstRequiredSegmentsSeconds }
230
+ : { stagesMs: historical?.stagesMs ?? null, observations: historical?.observations ?? null };
231
+ logger.info(`early wait forecast ${JSON.stringify({ waitId: context.waitId, consumer: context.consumerId,
232
+ generation: context.generation, features: context.features, elapsedMs, revision: context.revision,
233
+ predicted: { remainingSeconds: seconds, totalSeconds, basis, ...estimate } })}`);
234
+ context.loggedForecastRevision = context.revision;
235
+ }
236
+
209
237
  /**
210
238
  * Learn from the browser's presented-frame measurement for the current
211
239
  * viewing. The saved source and link readings are the ones available at
@@ -651,7 +651,7 @@ export function wireOutputs({
651
651
  const predicted = Number(qualityStateOf(output).predictedSpeedWhenOffered);
652
652
  return Number.isFinite(predicted) && predicted > 0 ? predicted : null;
653
653
  },
654
- firstFrameSecondsOf: (features) => parts.localObservations?.firstFrameSeconds(features) ?? null,
654
+ firstFrameSecondsOf: (features) => parts.localObservations?.firstFrameEstimate(features) ?? null,
655
655
  noteFirstFrame: (observation) => parts.localObservations?.noteFirstFrame(observation),
656
656
  audioDescriptionOf,
657
657
  openSecondsOf: (output) => runStartSecondsOf(output),