@torrent-tv/proxy 2.9.91 → 2.9.92

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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 2.9.92
2
+
3
+ - **Fix**: A seek acts on what the viewer asked for, instead of waiting out guards built for a signal that no longer exists. Three delays sat in front of every seek, all of them there because a far segment REQUEST used to steer the encoder and the player's playlist scan produced dozens of them. Requests stopped steering anything when the position became explicit, so what arrives now is only ever a position the viewer stated. The settle window drops from 1.2 s to 300 ms (the browser already collapses a drag into one report at 300 ms — this was a second debounce on an already-debounced signal, and it cost 1.2 s of every measured seek). The floor between restarts drops from 4 s to 500 ms, now a guard against a client spamming the endpoint rather than a policy about noise. And a run in progress is no longer protected for up to 30 s while it reaches its first segment: finishing a segment for a position the viewer has left is work nobody wants, and the hold could delay a genuine second seek by the whole grace. Measured cost of the old behaviour, 2026-08-04: two seeks 1.3 s apart produced two restarts 4.4 s apart, the first encoding 119.5 s of content before the second killed it.
4
+ - **Chore**: Removed `ENCODER_STALL_MS`, declared with a paragraph describing a watchdog that was never wired to anything.
5
+
1
6
  ## 2.9.91
2
7
 
3
8
  - **Fix**: The encoder no longer runs away from the viewer. Nothing bounded how far ahead it produced: measured 2026-08-04, three minutes after a film was opened the encode had reached 00:39:24 of a 01:26:51 source at 12.8x while the viewer was still at the start, and the torrent had pulled 80% of 4.7 GB to feed it — the pool owner's bandwidth and disk spent on a viewer who may watch two minutes, the pieces being read evicted from memory by pieces forty minutes ahead, and the swarm busy with anything but the segment being waited for. An encoder more than two minutes of content ahead of the last segment its viewer asked for is now **suspended**, and released once the viewer is within a minute of it — or at once when a segment is requested. Suspended rather than killed on purpose: restarting costs about nine seconds on this hardware, so a viewer reaching the end of the produced range would stall every time, while suspending keeps the process, its input and its position. POSIX only; where `SIGSTOP` does not exist the attempt fails once, is logged, and that session keeps the old behaviour. Every path that terminates an encoder now releases it first — a suspended process does not act on `SIGTERM` until it is continued, which would have hung the wait a seek performs before starting its replacement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.91",
3
+ "version": "2.9.92",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -43,26 +43,15 @@ const DEFAULT_SEGMENT_DURATION_SEC = 4;
43
43
  // is allowed to be before we restart ffmpeg at that position (server-side seek).
44
44
  // Requests within the window are served by waiting for the running encode.
45
45
  const MAX_LOOKAHEAD_SEGMENTS = 8;
46
- // After a seek-restart, ignore competing restart requests for this long. The
47
- // synthetic VOD playlist lets the player request distant segments in quick
48
- // succession (stall-recovery seeks); without a cooldown ffmpeg ping-pongs
49
- // between positions, restarting endlessly and producing nothing.
50
- const RESTART_COOLDOWN_MS = 4_000;
51
- // How long a seek restart waits for the CURRENT run to produce its first
52
- // segment before it is allowed to pre-empt it anyway. Generous, because the
53
- // first segment after a seek is the slowest thing this pipeline does (ffmpeg
54
- // restart + torrent pieces for a fresh position); still bounded so a wedged
55
- // run cannot block seeking forever. See #fireSettledSeek.
56
- const RUN_FIRST_SEGMENT_GRACE_MS = 30_000;
57
- // Encoder stall watchdog. A running ffmpeg emits `-progress` output on stdout
58
- // continuously while it encodes; when it hangs mid-file (alive, but producing
59
- // no output and no stderr — a deadlock, e.g. a stalled input read), that output
60
- // stops and `progress.updatedAt` freezes. If a segment INSIDE the look-ahead
61
- // window is being demanded but progress has not advanced for this long, the
62
- // encoder is wedged (observed: the segment 503s forever). Treat it like a seek
63
- // and restart ffmpeg at the demanded segment. Conservative — a slow-but-moving
64
- // encode keeps advancing `updatedAt`, so this only fires on a true freeze.
65
- const ENCODER_STALL_MS = 12_000;
46
+ // Floor between actual restarts. It used to be 4 s, from when a far segment
47
+ // REQUEST could steer the encoder and a playlist scan produced a burst of them.
48
+ // Requests no longer steer anything (see #ensureEncodingFor) every restart
49
+ // now comes from a position the viewer stated — so this is no longer a policy
50
+ // about noise, only a guard against a client that spams the seek endpoint.
51
+ // Measured cost of the old value 2026-08-04: two seeks 1.3 s apart produced two
52
+ // restarts 4.4 s apart, the first encoding 119.5 s of content nobody wanted
53
+ // before the second killed it.
54
+ const RESTART_COOLDOWN_MS = 500;
66
55
  // How far ahead of the viewer the encoder may run before it is stopped, and how
67
56
  // far it must fall back to before it is let go again.
68
57
  //
@@ -101,10 +90,17 @@ const LOOKAHEAD_RESUME_SECONDS = 60;
101
90
  // encoding 125 s of content before reaching the viewer's position. Field
102
91
  // 2026-08-02: a seek took 56 s, of which ~50 s was this backoff.
103
92
  const SEEK_BACKOFF_SEGMENTS = 1;
104
- const SEEK_SETTLE_MS = 1_200;
105
- // Hard cap on the total settle wait, measured from the first far request of a
93
+ // How long to wait for a scrub to stop moving before acting on it. Small,
94
+ // because the browser already collapses a drag into ONE report
95
+ // (`SEEK_REPORT_DEBOUNCE_MS`, 300 ms) and only reports where it settled — this
96
+ // is a second debounce on an already-debounced signal, and every millisecond of
97
+ // it is dead time in front of the viewer. It was 1.2 s when the encoder was
98
+ // also steered by segment requests, which arrive in bursts of dozens; measured
99
+ // 2026-08-04, that cost 1.2 s of every seek.
100
+ const SEEK_SETTLE_MS = 300;
101
+ // Hard cap on the total settle wait, measured from the first request of a
106
102
  // burst, so a still-moving scrubber cannot delay a genuine seek forever.
107
- const SEEK_SETTLE_MAX_MS = 2_500;
103
+ const SEEK_SETTLE_MAX_MS = 1_000;
108
104
  // Grace period to wait for the PREVIOUS ffmpeg process to exit (per signal
109
105
  // escalation step: SIGTERM, then SIGKILL) before spawning its replacement into
110
106
  // the same session directory. See #startEncodeRun.
@@ -2464,48 +2460,21 @@ export class HlsSessionManager {
2464
2460
  session.seekSettleTimer.unref?.();
2465
2461
  return;
2466
2462
  }
2467
- // Let the CURRENT run finish what it started. Restarting a run that has not
2468
- // yet produced a single segment destroys all its work and starts the wait
2469
- // over and after a seek the first segment is always the slowest, so this
2470
- // is self-perpetuating: field log (2026-08-02, one user seek) shows
2471
- // restarts at #617 → #717 → #732 → #732 every 5-7 s, none of which ever
2472
- // produced anything, leaving the viewer with a flickering loading pill and
2473
- // no playback at all.
2474
- //
2475
- // These extra targets are NOT further user seeks: when the player cannot
2476
- // get its segment it SCANS the playlist (every segment is listed in our
2477
- // synthetic VOD playlist, so from its point of view they all exist), and
2478
- // each far-enough probe looked like a fresh seek to us. Waiting for the
2479
- // first segment makes the scan harmless — it can no longer steer the
2480
- // encoder — and one genuine seek now reliably completes.
2481
- //
2482
- // Bounded by RUN_FIRST_SEGMENT_GRACE_MS so a wedged run cannot block seeks
2483
- // forever; the encoder-stall watchdog and the exit handler cover a run that
2484
- // dies outright.
2463
+ // A run in progress is NOT protected any more. It used to be: a restart was
2464
+ // held for up to 30 s while the current run reached its first segment,
2465
+ // because a far segment REQUEST could steer the encoder and the player's
2466
+ // playlist scan produced dozens of them restarts at #617 → #717 → #732 →
2467
+ // #732 every 5-7 s, none producing anything (field 2026-08-02). Requests
2468
+ // stopped steering anything when the position became explicit, so the only
2469
+ // thing that can arrive here is a position the viewer has stated, and
2470
+ // finishing a segment for where they no longer are is work nobody wants.
2471
+ // Holding it was also expensive in the other direction: a genuine second
2472
+ // seek could be delayed by the whole grace.
2485
2473
  const producedThisRun = this.#producedSecondsThisRun(session);
2486
2474
  const runIsAlive = session.ffmpeg != null && !hasChildExited(session.ffmpeg);
2487
- if (
2488
- runIsAlive &&
2489
- producedThisRun < this.segmentDurationSec &&
2490
- sinceLastRestart < RUN_FIRST_SEGMENT_GRACE_MS
2491
- ) {
2492
- logger.info(
2493
- `transcode ${session.id} seek #${target} HELD — current run has produced ` +
2494
- `${producedThisRun.toFixed(1)}s of the ${this.segmentDurationSec}s first segment ` +
2495
- `(${(sinceLastRestart / 1000).toFixed(1)}s into a ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s grace)`
2496
- );
2497
- session.seekSettleTimer = setTimeout(() => this.#fireSettledSeek(session), SEEK_SETTLE_MS);
2498
- session.seekSettleTimer.unref?.();
2499
- return;
2500
- }
2501
- // Why the restart was allowed — the counterpart of the HELD line above.
2502
- // Without it a restart is indistinguishable from the runaway ping-pong this
2503
- // guard exists to stop, and diagnosing a field report becomes guesswork.
2504
2475
  const allowedBecause = !runIsAlive
2505
2476
  ? "run is dead"
2506
- : producedThisRun >= this.segmentDurationSec
2507
- ? `run produced ${producedThisRun.toFixed(1)}s (first segment done)`
2508
- : `grace of ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s expired`;
2477
+ : `viewer moved; run had produced ${producedThisRun.toFixed(1)}s`;
2509
2478
  // The start is exactly what requestSeek computed — one segment before the
2510
2479
  // viewer's position — and nothing else may move it.
2511
2480
  //