@torrent-tv/proxy 2.107.42 → 2.107.43

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.42",
3
+ "version": "2.107.43",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
 
40
40
  import { ENCODE_RUN_EVENT, ENCODE_RUN_STATE, INITIAL_RUN_STATE, nextState } from "./encode-run-state.js";
41
41
  import { classifyEncodeExit, ENCODE_EXIT } from "./encode-exit.js";
42
- import { speedFromWork } from "./encoder-readings.js";
42
+ import { workDeltaBetween } from "./encoder-readings.js";
43
43
  import { RunClock } from "./RunClock.js";
44
44
  import { RunProgress } from "./RunProgress.js";
45
45
 
@@ -229,7 +229,7 @@ export class EncodeRun {
229
229
  * @param {{ info: (line: string) => void, warn: (line: string) => void, error?: (line: string) => void }} params.logger
230
230
  * @param {() => number} [params.now]
231
231
  * @param {(ended: RunEnded) => void} [params.onEnded]
232
- * @param {(reading: { speed: number, at: number }) => void} [params.onSpeedMeasured] -
232
+ * @param {(reading: { speed: number, at: number, workSeconds: number, workingSeconds: number }) => void} [params.onSpeedMeasured] -
233
233
  * Told each time this run has measured its processing speed, once per
234
234
  * reading.
235
235
  * @param {(name: string, following: string | null) => string | null} [params.onClosed] - Called with the
@@ -353,10 +353,10 @@ export class EncodeRun {
353
353
  }
354
354
 
355
355
  /**
356
- * The same speed with the moment it was measured, or null before a closed
357
- * piece has enough progress data.
356
+ * The processing speed, its produced media and active working time, with
357
+ * the measurement time; null before a piece supplies a measured interval.
358
358
  *
359
- * @returns {{ speed: number, at: number } | null}
359
+ * @returns {{ speed: number, at: number, workSeconds: number, workingSeconds: number } | null}
360
360
  */
361
361
  get speedReading() {
362
362
  return this.#speedReading === null ? null : { ...this.#speedReading };
@@ -764,9 +764,9 @@ export class EncodeRun {
764
764
  if (sample === null) {
765
765
  return;
766
766
  }
767
- const speed = speedFromWork(this.#pieceSample, sample);
768
- if (speed !== null) {
769
- this.#speedReading = { speed, at: sample.at };
767
+ const work = workDeltaBetween(this.#pieceSample, sample);
768
+ if (work !== null) {
769
+ this.#speedReading = { speed: work.workSeconds / work.workingSeconds, at: sample.at, ...work };
770
770
  this.#pieceSample = sample;
771
771
  this.#speedMeasurementPending = false;
772
772
  this.onSpeedMeasured(this.speedReading);
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  export class SpeedReadings {
17
- /** @type {WeakMap<object, Array<{ speed: number, at: number }>>} */
17
+ /** @type {WeakMap<object, Array<{ speed: number, at: number, workSeconds?: number, workingSeconds?: number }>>} */
18
18
  #byOutput = new WeakMap();
19
19
  #keepMs;
20
20
 
@@ -25,7 +25,7 @@ export class SpeedReadings {
25
25
 
26
26
  /**
27
27
  * @param {object} output
28
- * @param {{ speed: number, at: number } | null} reading
28
+ * @param {{ speed: number, at: number, workSeconds?: number, workingSeconds?: number } | null} reading
29
29
  */
30
30
  note(output, reading) {
31
31
  if (!output || !(reading?.speed > 0) || !Number.isFinite(reading.at)) return;
@@ -33,7 +33,12 @@ export class SpeedReadings {
33
33
  if (!readings) this.#byOutput.set(output, readings = []);
34
34
  let place = readings.length;
35
35
  while (place > 0 && readings[place - 1].at > reading.at) place -= 1;
36
- readings.splice(place, 0, { speed: reading.speed, at: reading.at });
36
+ const sample = { speed: reading.speed, at: reading.at };
37
+ if (Number.isFinite(reading.workSeconds) && reading.workSeconds > 0 &&
38
+ Number.isFinite(reading.workingSeconds) && reading.workingSeconds > 0) {
39
+ Object.assign(sample, { workSeconds: reading.workSeconds, workingSeconds: reading.workingSeconds });
40
+ }
41
+ readings.splice(place, 0, sample);
37
42
  const cutoff = readings.at(-1).at - this.#keepMs;
38
43
  // As the forecast's own trend keeps them: the newest one before the horizon stays.
39
44
  while (readings.length > 1 && readings[1].at <= cutoff) readings.shift();
@@ -41,7 +46,7 @@ export class SpeedReadings {
41
46
 
42
47
  /**
43
48
  * @param {object} output
44
- * @returns {Array<{ speed: number, at: number }>} The window, in the order the readings were taken.
49
+ * @returns {Array<{ speed: number, at: number, workSeconds?: number, workingSeconds?: number }>} The readings in measurement order.
45
50
  */
46
51
  windowOf(output) {
47
52
  return [...(this.#byOutput.get(output) ?? [])];
@@ -31,13 +31,21 @@
31
31
  * @returns {number | null} Film seconds made per second of the run's own work.
32
32
  */
33
33
  export function speedFromWork(previous, current) {
34
- if (!previous || !current) {
35
- return null;
36
- }
37
- const producedSeconds = current.producedSeconds - previous.producedSeconds;
34
+ const work = workDeltaBetween(previous, current);
35
+ return work === null ? null : work.workSeconds / work.workingSeconds;
36
+ }
37
+
38
+ /**
39
+ * Produced media and active processing time between samples of one run.
40
+ * @param {WorkSample | null} previous
41
+ * @param {WorkSample | null} current
42
+ * @returns {{ workSeconds: number, workingSeconds: number } | null}
43
+ */
44
+ export function workDeltaBetween(previous, current) {
45
+ if (!previous || !current) return null;
46
+ const workSeconds = current.producedSeconds - previous.producedSeconds;
38
47
  const workingSeconds = (current.workingMs - previous.workingMs) / 1000;
39
- if (!(producedSeconds > 0) || !(workingSeconds > 0)) {
40
- return null;
41
- }
42
- return producedSeconds / workingSeconds;
48
+ return Number.isFinite(workSeconds) && workSeconds > 0 &&
49
+ Number.isFinite(workingSeconds) && workingSeconds > 0
50
+ ? { workSeconds, workingSeconds } : null;
43
51
  }
@@ -76,12 +76,13 @@ export function forecastRate(readings, now = Date.now(), horizonSeconds = 0, wei
76
76
  * ordered by when each was taken, readings taken in the same millisecond kept
77
77
  * apart. Null for no reading.
78
78
  *
79
- * @param {Array<{ at: number, speed?: number, value?: number }>} readings
79
+ * @param {Array<{ at: number, speed?: number, value?: number, workSeconds?: number, workingSeconds?: number }>} readings
80
80
  * @returns {object | null}
81
81
  */
82
82
  export function rateSummaryOf(readings) {
83
83
  const ordered = (Array.isArray(readings) ? readings : [])
84
- .map(reading => ({ at: reading?.at, value: reading?.value ?? reading?.speed }))
84
+ .map(reading => ({ at: reading?.at, value: reading?.value ?? reading?.speed,
85
+ workSeconds: reading?.workSeconds, workingSeconds: reading?.workingSeconds }))
85
86
  .filter(reading => Number.isFinite(reading.at) && Number.isFinite(reading.value) && reading.value >= 0)
86
87
  .sort((left, right) => left.at - right.at);
87
88
  return ordered.length > 0 ? summarizeRate(ordered) : null;
@@ -278,9 +279,9 @@ export function predictPlaybackReadiness(input = {}) {
278
279
  [{ start: at, end: add(at, measured ? nearest(fromSeconds(buffered), unit) :
279
280
  rescale(fromSeconds(buffered), unit)), frame: zero }],
280
281
  ready: new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : []),
281
- // Film made over the encoder's working time, over the readings the trend
282
- // keeps: one reading is one closed piece, so the harmonic mean is that
283
- // ratio. The last reading alone is one piece's content and machine — a
282
+ // Film made over active processing time, summed over the readings retained.
283
+ // Point-only legacy readings retain their equal-work harmonic summary.
284
+ // The last reading alone is one piece's content and machine — a
284
285
  // first piece of credits read 6.49x and started a cold AVI on 2.4 s of
285
286
  // buffer that the next piece, at 0.57x, did not hold (torrent-tv/meta#178).
286
287
  curve: rateCurve(track.readings, now, "piece"),
@@ -1417,6 +1418,10 @@ function rateCurve(readings, now, weighting = "last") {
1417
1418
 
1418
1419
  function summarizeRate(readings) {
1419
1420
  const latest = readings.at(-1);
1421
+ const workKnown = readings.every(reading => Number.isFinite(reading.workSeconds) && reading.workSeconds > 0 &&
1422
+ Number.isFinite(reading.workingSeconds) && reading.workingSeconds > 0);
1423
+ const workSeconds = workKnown ? readings.reduce((sum, reading) => sum + reading.workSeconds, 0) : null;
1424
+ const workingSeconds = workKnown ? readings.reduce((sum, reading) => sum + reading.workingSeconds, 0) : null;
1420
1425
  let weighted = 0;
1421
1426
  for (let index = 0; index + 1 < readings.length; index += 1) {
1422
1427
  weighted += readings[index].value * (readings[index + 1].at - readings[index].at);
@@ -1427,12 +1432,13 @@ function summarizeRate(readings) {
1427
1432
  lastAt: latest.at,
1428
1433
  lastValue: latest.value,
1429
1434
  meanValue: readings.reduce((sum, reading) => sum + reading.value, 0) / readings.length,
1430
- // Each reading one piece of work: work over the time it took.
1435
+ // Equal-work summary for legacy readings without measured work quantities.
1431
1436
  harmonicValue: readings.some(reading => !(reading.value > 0)) ? 0
1432
1437
  : readings.length / readings.reduce((sum, reading) => sum + 1 / reading.value, 0),
1433
1438
  // Each reading holding until the next: quantity over the span.
1434
1439
  timeWeightedValue: spanMs > 0 ? weighted / spanMs : latest.value,
1435
- spanSeconds: spanMs / 1000
1440
+ spanSeconds: spanMs / 1000,
1441
+ ...(workKnown ? { workSeconds, workingSeconds, workRateValue: workSeconds / workingSeconds } : {})
1436
1442
  };
1437
1443
  }
1438
1444
 
@@ -1440,7 +1446,8 @@ function rateCurveFrom(summary, weighting = "last") {
1440
1446
  // Conditional projection of the measured service. No exponential
1441
1447
  // relaxation, smoothing time, slope extrapolation or guessed recovery.
1442
1448
  // A new measurement changes the integral; polling does not change its rate.
1443
- const value = weighting === "piece" && Number.isFinite(summary.harmonicValue) ? summary.harmonicValue
1449
+ const value = weighting === "piece" && Number.isFinite(summary.workRateValue) ? summary.workRateValue
1450
+ : weighting === "piece" && Number.isFinite(summary.harmonicValue) ? summary.harmonicValue
1444
1451
  : weighting === "time" && Number.isFinite(summary.timeWeightedValue) ? summary.timeWeightedValue
1445
1452
  : summary.lastValue;
1446
1453
  const rate = Math.max(0, value);