@torrent-tv/proxy 2.107.42 → 2.107.44

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.44",
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"),
@@ -855,6 +856,7 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
855
856
 
856
857
  let outputBits = 0;
857
858
  let latestTrackFinish = 0;
859
+ let earliestTrackStart = Number.POSITIVE_INFINITY;
858
860
  for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
859
861
  const state = trackState[trackIndex];
860
862
  const end = state.tailEndSeconds;
@@ -873,7 +875,9 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
873
875
  const localStart = tailStart - state.clockSeconds;
874
876
  const inRun = run.spans.some(span => localStart >= span.start && localStart < span.end);
875
877
  const open = work > 0 && !inRun ? run.openSeconds : 0;
876
- const finishedAt = work > 0 ? processing.finish(work, trackFinish[trackIndex] + open) : trackFinish[trackIndex];
878
+ const productionStart = trackFinish[trackIndex] + open;
879
+ const finishedAt = work > 0 ? processing.finish(work, productionStart) : trackFinish[trackIndex];
880
+ if (work > 0) earliestTrackStart = Math.min(earliestTrackStart, productionStart);
877
881
  latestTrackFinish = Math.max(latestTrackFinish, finishedAt);
878
882
  const deliveryStart = Math.max(tailStart, position + buffered);
879
883
  outputBits += Math.max(0, state.bitsPerMediaSecond) * Math.max(0, end - deliveryStart);
@@ -884,7 +888,13 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
884
888
 
885
889
  if (outputBits > 0) {
886
890
  const end = Math.max(...trackState.map(state => state.tailEndSeconds));
887
- const completedAt = link.finish(outputBits, Math.max(transferFinish, latestTrackFinish));
891
+ // Production and transfer are tandem services. Starting the link only
892
+ // after the last track finishes charges both complete workloads in series.
893
+ // The fluid suffix is bounded by the later service completion, with the
894
+ // link queue beginning no earlier than the first produced output.
895
+ const deliveryStart = Math.max(transferFinish, Number.isFinite(earliestTrackStart) ? earliestTrackStart : 0);
896
+ const deliveryAt = link.finish(outputBits, deliveryStart);
897
+ const completedAt = Math.max(latestTrackFinish, deliveryAt);
888
898
  const ahead = Math.max(0, end - position);
889
899
  constrain(completedAt - Math.max(0, ahead - reserve), "delivery", 0,
890
900
  trackState[0]?.segments.at(-1)?.index ?? 0, completedAt, ahead, false, sourceArrivalAt, false);
@@ -1417,6 +1427,10 @@ function rateCurve(readings, now, weighting = "last") {
1417
1427
 
1418
1428
  function summarizeRate(readings) {
1419
1429
  const latest = readings.at(-1);
1430
+ const workKnown = readings.every(reading => Number.isFinite(reading.workSeconds) && reading.workSeconds > 0 &&
1431
+ Number.isFinite(reading.workingSeconds) && reading.workingSeconds > 0);
1432
+ const workSeconds = workKnown ? readings.reduce((sum, reading) => sum + reading.workSeconds, 0) : null;
1433
+ const workingSeconds = workKnown ? readings.reduce((sum, reading) => sum + reading.workingSeconds, 0) : null;
1420
1434
  let weighted = 0;
1421
1435
  for (let index = 0; index + 1 < readings.length; index += 1) {
1422
1436
  weighted += readings[index].value * (readings[index + 1].at - readings[index].at);
@@ -1427,12 +1441,13 @@ function summarizeRate(readings) {
1427
1441
  lastAt: latest.at,
1428
1442
  lastValue: latest.value,
1429
1443
  meanValue: readings.reduce((sum, reading) => sum + reading.value, 0) / readings.length,
1430
- // Each reading one piece of work: work over the time it took.
1444
+ // Equal-work summary for legacy readings without measured work quantities.
1431
1445
  harmonicValue: readings.some(reading => !(reading.value > 0)) ? 0
1432
1446
  : readings.length / readings.reduce((sum, reading) => sum + 1 / reading.value, 0),
1433
1447
  // Each reading holding until the next: quantity over the span.
1434
1448
  timeWeightedValue: spanMs > 0 ? weighted / spanMs : latest.value,
1435
- spanSeconds: spanMs / 1000
1449
+ spanSeconds: spanMs / 1000,
1450
+ ...(workKnown ? { workSeconds, workingSeconds, workRateValue: workSeconds / workingSeconds } : {})
1436
1451
  };
1437
1452
  }
1438
1453
 
@@ -1440,7 +1455,8 @@ function rateCurveFrom(summary, weighting = "last") {
1440
1455
  // Conditional projection of the measured service. No exponential
1441
1456
  // relaxation, smoothing time, slope extrapolation or guessed recovery.
1442
1457
  // A new measurement changes the integral; polling does not change its rate.
1443
- const value = weighting === "piece" && Number.isFinite(summary.harmonicValue) ? summary.harmonicValue
1458
+ const value = weighting === "piece" && Number.isFinite(summary.workRateValue) ? summary.workRateValue
1459
+ : weighting === "piece" && Number.isFinite(summary.harmonicValue) ? summary.harmonicValue
1444
1460
  : weighting === "time" && Number.isFinite(summary.timeWeightedValue) ? summary.timeWeightedValue
1445
1461
  : summary.lastValue;
1446
1462
  const rate = Math.max(0, value);