@torrent-tv/proxy 2.107.21 → 2.107.23

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.21",
3
+ "version": "2.107.23",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -187,9 +187,12 @@ export class EncodeRun {
187
187
  /** The film made and the run's own working time, as of the last progress report. */
188
188
  #workSample = null;
189
189
 
190
- /** The work sample taken when this run last measured its speed. */
190
+ /** Baseline work sample for this run's next speed reading. */
191
191
  #pieceSample = null;
192
192
 
193
+ /** A closed, published piece is waiting for a usable progress sample. */
194
+ #speedMeasurementPending = false;
195
+
193
196
  /** @type {{ speed: number, at: number } | null} */
194
197
  #speedReading = null;
195
198
 
@@ -302,6 +305,7 @@ export class EncodeRun {
302
305
  this.progress = new RunProgress({ startSeconds, totalSeconds, now: this.now });
303
306
  /** Where this run's time went: its own work, or waiting, or stopped. */
304
307
  this.clock = new RunClock({ now: this.now });
308
+ this.#pieceSample = { at: this.now(), producedSeconds: 0, workingMs: 0 };
305
309
  this.onEnded = typeof onEnded === "function" ? onEnded : () => {};
306
310
  this.onProgress = typeof onProgress === "function" ? onProgress : () => {};
307
311
  this.onProduced = typeof onProduced === "function" ? onProduced : () => {};
@@ -338,9 +342,9 @@ export class EncodeRun {
338
342
  }
339
343
 
340
344
  /**
341
- * This run's processing speed: film made over its own working time between
342
- * two closed pieces, with the time its input waited and the time it was
343
- * stopped taken out (`RunClock`). Zero until two pieces have closed.
345
+ * This run's processing speed: film made over its own working time since the
346
+ * last baseline, with the time its input waited and the time it was stopped
347
+ * taken out (`RunClock`). Zero until a closed piece has enough progress data.
344
348
  *
345
349
  * @returns {number}
346
350
  */
@@ -349,8 +353,8 @@ export class EncodeRun {
349
353
  }
350
354
 
351
355
  /**
352
- * The same speed with the moment it was measured, or null before a second
353
- * piece has closed.
356
+ * The same speed with the moment it was measured, or null before a closed
357
+ * piece has enough progress data.
354
358
  *
355
359
  * @returns {{ speed: number, at: number } | null}
356
360
  */
@@ -452,6 +456,16 @@ export class EncodeRun {
452
456
  /** An input read of this run has its bytes. */
453
457
  inputWaitEnds() {
454
458
  this.clock.inputWaitEnds();
459
+ if (this.#speedReading === null && this.#workSample !== null) {
460
+ // Progress made while an input read was waiting belongs to the wait, not
461
+ // to the encoder's rate. Start the first measurement from the latest
462
+ // progress known when the input resumes.
463
+ this.#pieceSample = {
464
+ ...this.#workSample,
465
+ at: this.now(),
466
+ workingMs: this.clock.workingMs()
467
+ };
468
+ }
455
469
  }
456
470
 
457
471
  /** @returns {boolean} Whether an input read of this run is waiting for bytes now. */
@@ -589,12 +603,7 @@ export class EncodeRun {
589
603
  producedSeconds: Math.max(0, this.progress.processedSeconds - this.progress.startPositionSeconds),
590
604
  workingMs: this.clock.workingMs(at)
591
605
  };
592
- // A run can be admitted for exactly one segment. Establish its baseline
593
- // on the first media progress report so closing that segment yields a
594
- // processing rate without counting the preceding input wait.
595
- if (this.#pieceSample === null && this.#workSample.producedSeconds > 0) {
596
- this.#pieceSample = this.#workSample;
597
- }
606
+ if (this.#speedMeasurementPending) this.#measureSpeed();
598
607
  }
599
608
  this.onProgress(this.progress.snapshot());
600
609
  }
@@ -688,7 +697,6 @@ export class EncodeRun {
688
697
  }
689
698
 
690
699
  #publishClosed(name, following = null, { quiet = false, tentative = false } = {}) {
691
- if (!tentative) this.#measureSpeed();
692
700
  // ITS SERVED NAME, which is what whoever owns the disk gives it in answer.
693
701
  // ffmpeg writes a piece under a working name and reports that; the piece
694
702
  // becomes servable by being renamed, and everything below works in the
@@ -724,7 +732,8 @@ export class EncodeRun {
724
732
  }
725
733
 
726
734
  if (!served) return false;
727
- if (tentative) this.#measureSpeed();
735
+ this.#speedMeasurementPending = true;
736
+ this.#measureSpeed();
728
737
  if (!this.#stopping) {
729
738
  this.#provenName = served;
730
739
  }
@@ -744,25 +753,22 @@ export class EncodeRun {
744
753
  }
745
754
 
746
755
  /**
747
- * A closed piece is the moment this run's speed is read: what it made since
748
- * the last reading, over its own working time. The first baseline is the
749
- * first positive progress report, so a one-piece run can be measured without
750
- * charging the wait for its first input bytes as processing. A stretch with
751
- * no progress keeps the earlier sample, and the next reading spans both.
756
+ * A published piece is measured against this run's last baseline over its
757
+ * own working time. The initial baseline is the run start, advanced to the
758
+ * latest known progress when input resumes, so a one-piece run can be
759
+ * measured even when FFmpeg reports progress only once. Input waits remain
760
+ * excluded by RunClock.
752
761
  */
753
762
  #measureSpeed() {
754
763
  const sample = this.#workSample;
755
764
  if (sample === null) {
756
765
  return;
757
766
  }
758
- if (this.#pieceSample === null) {
759
- if (sample.producedSeconds > 0) this.#pieceSample = sample;
760
- return;
761
- }
762
767
  const speed = speedFromWork(this.#pieceSample, sample);
763
768
  if (speed !== null) {
764
769
  this.#speedReading = { speed, at: sample.at };
765
770
  this.#pieceSample = sample;
771
+ this.#speedMeasurementPending = false;
766
772
  this.onSpeedMeasured(this.speedReading);
767
773
  }
768
774
  }
@@ -552,7 +552,9 @@ export function predictPlaybackReadiness(input = {}) {
552
552
  let after = contiguousEnd(delivered, at);
553
553
  for (const completion of completions.filter((item) => item.trackIndex === trackIndex)) {
554
554
  const before = after;
555
- for (const range of completion.segment.ranges) addMediaRange(delivered, range);
555
+ const ranges = compare(before, at) === 0
556
+ ? withSeekStartTolerance(completion.segment.ranges, at, unit) : completion.segment.ranges;
557
+ for (const range of ranges) addMediaRange(delivered, range);
556
558
  after = contiguousEnd(delivered, at);
557
559
  const ahead = secondsAhead(earliest(before, trackEnd), at);
558
560
  constrain(completion.at - ahead, "delivery", trackIndex, completion.segment.index, completion.at, ahead, false,
@@ -581,7 +583,9 @@ export function predictPlaybackReadiness(input = {}) {
581
583
  }
582
584
  for (const production of productions.filter((item) => item.trackIndex === trackIndex)) {
583
585
  const before = contiguousEnd(prepared, at);
584
- for (const range of production.segment.ranges) addMediaRange(prepared, range);
586
+ const ranges = compare(before, at) === 0
587
+ ? withSeekStartTolerance(production.segment.ranges, at, unit) : production.segment.ranges;
588
+ for (const range of ranges) addMediaRange(prepared, range);
585
589
  // Supply interruption coverage is stock held by the proxy as well as
586
590
  // the browser. It is not a browser buffer-size admission rule.
587
591
  // The reserve is bounded by what is left of the track, not of the window.
@@ -851,6 +855,25 @@ function heldRanges(ranges, segments, unit, position) {
851
855
  return held;
852
856
  }
853
857
 
858
+ /**
859
+ * Include the first arriving range when its start is within the media
860
+ * element's allowance for a seek target. This does not bridge later gaps.
861
+ *
862
+ * @param {import("./media-time.js").MediaRange[]} ranges
863
+ * @param {import("./media-time.js").MediaTime} position
864
+ * @param {bigint} unit
865
+ * @returns {import("./media-time.js").MediaRange[]}
866
+ */
867
+ function withSeekStartTolerance(ranges, position, unit) {
868
+ const first = ranges.reduce((earliestRange, range) =>
869
+ !earliestRange || compare(range.start, earliestRange.start) < 0 ? range : earliestRange, null);
870
+ const toleranceEnd = add(position, rescale(fromSeconds(CLIENT_RANGE_START_TOLERANCE_SECONDS), unit));
871
+ if (!first || compare(first.start, position) <= 0 || compare(first.start, toleranceEnd) > 0) {
872
+ return ranges;
873
+ }
874
+ return ranges.map((range) => range === first ? { ...range, start: position } : range);
875
+ }
876
+
854
877
  /**
855
878
  * The pieces of a track this forecast places in exact time: those still to
856
879
  * play (ending after `position`) that start before `horizonEnd`, the first