@torrent-tv/proxy 2.101.30 → 2.101.31

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.101.30",
3
+ "version": "2.101.31",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -290,86 +290,30 @@ export class LocalObservations {
290
290
  }
291
291
 
292
292
  /**
293
- * Predict the first-frame wait as the sum of separately predicted cold-start
294
- * stages. Each stage uses all compatible measurements, weighted by the
295
- * normalized distance between the measured inputs; no complete past wait is
296
- * returned as the forecast. A known cache/source state must also be known in
297
- * the training observation, otherwise that observation is not comparable.
293
+ * Price the early wait from separate measured stage medians for the same
294
+ * output path. Numeric proximity of a past file, source rate, or link rate
295
+ * does not select a completed run. Each observation contributes once, using
296
+ * its latest snapshot that states the current cache and operation path.
298
297
  *
299
298
  * @param {object} features
300
299
  * @returns {number | null}
301
300
  */
302
301
  firstFrameSeconds(features) {
303
302
  if (!features || typeof features !== "object") return null;
304
- const candidates = (this.#entries.firstFrame[this.#firstFrameKey()] ?? [])
305
- .filter(reading => reading.extension === (features.extension ?? "") && reading.stages);
306
- if (candidates.length === 0) return null;
307
- const dimensions = ["fileLength", "downloadSpeed", "linkMbps", "linkSampleAgeMs", "preparedOutputBytes",
308
- "fileAvailable", "headerRemaining", "peers", "videoOperation", "audioOperation", "videoSpeedX", "audioSpeedX"];
309
- const pointsOf = reading => Array.isArray(reading.featureSnapshots) && reading.featureSnapshots.length > 0
310
- ? reading.featureSnapshots : [reading];
311
- const scales = new Map(dimensions.map(key => [key, median(candidates.flatMap(reading => pointsOf(reading)
312
- .map(point => FIRST_FRAME_CATEGORICAL.has(key) ? null : nonNegative(point[key])))
313
- .filter(value => value !== null && value > 0))]));
314
- const distances = [];
315
- for (const reading of candidates) {
316
- let bestDistance = null;
317
- for (const point of pointsOf(reading)) {
318
- if ((features.preparedOutputBytes !== null && features.preparedOutputBytes !== undefined &&
319
- point.preparedOutputBytes === null) ||
320
- [...FIRST_FRAME_CATEGORICAL].some(key => {
321
- const current = key === "fileAvailable" ? features[key] : operationOf(features[key]);
322
- return current !== null && current !== undefined && point[key] !== current;
323
- })) continue;
324
- let distance = 0;
325
- let used = 0;
326
- let shared = 0;
327
- for (const key of dimensions) {
328
- const current = FIRST_FRAME_CATEGORICAL.has(key)
329
- ? key === "fileAvailable" ? (typeof features[key] === "boolean" ? features[key] : null)
330
- : operationOf(features[key])
331
- : nonNegative(features[key]);
332
- const previous = FIRST_FRAME_CATEGORICAL.has(key)
333
- ? key === "fileAvailable" ? (typeof point[key] === "boolean" ? point[key] : null)
334
- : operationOf(point[key])
335
- : nonNegative(point[key]);
336
- if (current === null && previous === null) continue;
337
- if (current === null || previous === null) {
338
- distance += 1;
339
- used += 1;
340
- continue;
341
- }
342
- shared += 1;
343
- const scale = scales.get(key);
344
- const difference = typeof current !== "number" && typeof previous !== "number"
345
- ? Number(current !== previous)
346
- : scale === null
347
- ? Number(current !== previous)
348
- : Math.log1p(current / scale) - Math.log1p(previous / scale);
349
- distance += difference * difference;
350
- used += 1;
351
- }
352
- if (used === 0 || shared === 0) continue;
353
- distance /= used;
354
- if (bestDistance === null || distance < bestDistance) bestDistance = distance;
355
- }
356
- if (bestDistance !== null) distances.push({ distance: bestDistance, reading });
357
- }
358
- if (distances.length === 0) return null;
359
- const exact = distances.filter(candidate => candidate.distance === 0);
360
- const weighted = exact.length > 0
361
- ? exact.map(candidate => ({ ...candidate, weight: 1 }))
362
- : distances.map(candidate => ({ ...candidate, weight: 1 / candidate.distance }));
363
303
  const stages = ["transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
364
- const predicted = stages.map(stage => {
365
- const applicable = weighted.filter(candidate => Number.isFinite(candidate.reading.stages[stage]));
366
- const totalWeight = applicable.reduce((sum, candidate) => sum + candidate.weight, 0);
367
- return totalWeight > 0
368
- ? applicable.reduce((sum, candidate) => sum + candidate.reading.stages[stage] * candidate.weight, 0) / totalWeight
369
- : null;
370
- });
371
- if (predicted.some(value => value === null)) return null;
372
- return predicted.reduce((sum, value) => sum + value, 0) / 1000;
304
+ const candidates = [];
305
+ for (const reading of this.#entries.firstFrame[this.#firstFrameKey()] ?? []) {
306
+ if (reading.extension !== (features.extension ?? "") || !reading.stages) continue;
307
+ const points = Array.isArray(reading.featureSnapshots) && reading.featureSnapshots.length > 0
308
+ ? reading.featureSnapshots : [reading];
309
+ const point = [...points].reverse().find(candidate => firstFramePathMatches(features, candidate));
310
+ if (point) candidates.push(reading.stages);
311
+ }
312
+ if (candidates.length === 0) return null;
313
+ const prices = stages.map(stage => median(candidates.map(reading => reading[stage])
314
+ .filter(Number.isFinite)));
315
+ if (prices.some(value => value === null)) return null;
316
+ return prices.reduce((sum, value) => sum + value, 0) / 1000;
373
317
  }
374
318
 
375
319
  #firstFrameKey() {
@@ -487,6 +431,20 @@ function operationOf(value) {
487
431
  return value === "copy" || value === "encode" ? value : null;
488
432
  }
489
433
 
434
+ /** @param {object} current @param {object} previous @returns {boolean} */
435
+ function firstFramePathMatches(current, previous) {
436
+ for (const key of FIRST_FRAME_CATEGORICAL) {
437
+ const expected = key === "fileAvailable" ? current[key] : operationOf(current[key]);
438
+ if (expected !== null && expected !== undefined && previous[key] !== expected) return false;
439
+ }
440
+ const preparedBytes = nonNegative(current.preparedOutputBytes);
441
+ if (preparedBytes !== null) {
442
+ const previousBytes = nonNegative(previous.preparedOutputBytes);
443
+ if (previousBytes === null || (previousBytes > 0) !== (preparedBytes > 0)) return false;
444
+ }
445
+ return true;
446
+ }
447
+
490
448
  /** @param {unknown} value @returns {{ totalMs: number, transportMs: number, planMs: number, prepareMs: number, prebufferMs: number, presentationMs: number } | null} */
491
449
  function stagesOf(value) {
492
450
  if (!value || typeof value !== "object") return null;