@torrent-tv/proxy 2.9.58 → 2.9.60

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,11 @@
1
+ ## 2.9.60
2
+
3
+ - **Fix**: Seeking restarted the encoder at the position it was **already encoding**, destroying the very work being waited for — visible in the field log 2026-08-02 as `restart at #865` twice within ten seconds, each killing a run that was encoding #865. While the target segment is being produced the player keeps re-requesting it, and every such request looks "far" from where the encoder USED to be, so each one re-triggered a restart at the position we had only just moved to; playback data kept appearing and vanishing, and a seek only completed when a segment happened to reach the player before the next restart. A settled seek whose target equals the current runs start index is now ignored outright. This is distinct from the 2.9.58 guard, which only decides whether to let the current run finish its first segment — not whether a new run is needed at all; that guard behaved correctly here (it logged `run produced 4.5s (first segment done)`) and still let the pointless restart through.
4
+
5
+ ## 2.9.59
6
+
7
+ - **Chore**: Diagnostics for seek handling. The session-start line now carries the proxy version (`transcode <id> start (proxy 2.9.59) "<file>"`), so a field report answers "is the host running the build I published?" by itself. And the seek restart guard added in 2.9.58 now states its decision: either `seek #N HELD — current run has produced Xs of the Ys first segment` or, on the restart line, why it was allowed (`run is dead` / `run produced Xs (first segment done)` / `grace expired`). Previously a permitted restart was indistinguishable in the log from the runaway ping-pong the guard exists to stop, which made diagnosing "seek still did not work" guesswork.
8
+
1
9
  ## 2.9.58
2
10
 
3
11
  - **Fix**: A single user seek could leave playback permanently stuck with a flickering loading pill and no video. The encoder was allowed to restart at a new position even when the current run had not yet produced a **single segment**, so each restart destroyed the previous one's work and began the wait again — self-perpetuating, because the first segment after a seek is the slowest thing the pipeline does (field log 2026-08-02: restarts at #617 → #717 → #732 → #732 every 5-7 s, none producing anything). The extra targets were not further user seeks: unable to get its segment, the player SCANS the playlist (our synthetic VOD playlist lists every segment, so from its side they all exist), and each far-enough probe looked like a fresh seek. A seek restart now waits for the current run to produce its first segment (bounded by a 30 s grace, and skipped entirely if the run has died), which makes the scan harmless and lets one genuine seek complete. Independent of segment format.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.58",
3
+ "version": "2.9.60",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -14,7 +14,11 @@ import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { spawn } from "node:child_process";
17
+ import { createRequire } from "node:module";
17
18
  import { logger } from "../utils/logger.js";
19
+
20
+ /** Own package version, stamped onto session-start log lines. */
21
+ const PROXY_VERSION = createRequire(import.meta.url)("../package.json").version;
18
22
  import {
19
23
  softwareDescriptor,
20
24
  chooseSoftwareEncodeSettings,
@@ -1077,7 +1081,10 @@ export class HlsSessionManager {
1077
1081
  this.sessionIdBySource.set(sourceMapKey, sessionId);
1078
1082
 
1079
1083
  logger.info(
1080
- `transcode ${sessionId} start "${logName}" ` +
1084
+ // Proxy version on the session-start line: a field report always includes
1085
+ // one of these, so "is the host actually running the build I published?"
1086
+ // is answered by the log itself instead of a round trip to the machine.
1087
+ `transcode ${sessionId} start (proxy ${PROXY_VERSION}) "${logName}" ` +
1081
1088
  `video=${transcodeVideo ? `${this.videoEncoder.name}${softwarePreset ? `/${softwarePreset}` : ""}` : "copy"} ` +
1082
1089
  `audio=${transcodeAudio ? "aac" : "copy"} ` +
1083
1090
  // Branch tag for log correlation: A = video re-encode (fixed GOP, grid
@@ -2006,6 +2013,23 @@ export class HlsSessionManager {
2006
2013
  session.seekFirstFarAt = 0;
2007
2014
  return;
2008
2015
  }
2016
+ // Already encoding exactly this position — there is nothing to seek TO, so
2017
+ // restarting can only destroy the very work being waited for. The player
2018
+ // keeps re-requesting the target segment while it is still being produced,
2019
+ // and every such request looks "far" from where the encoder USED to be, so
2020
+ // without this check each one re-triggered a restart at the position we had
2021
+ // only just moved to: field log 2026-08-02 shows `restart at #865` twice in
2022
+ // ten seconds, each killing a run that was encoding #865. The guard below
2023
+ // did not catch it — it only decides whether to let the current run finish,
2024
+ // not whether a new run is needed at all.
2025
+ if (target === session.encodeStartIndex && session.ffmpeg != null && !hasChildExited(session.ffmpeg)) {
2026
+ logger.info(
2027
+ `transcode ${session.id} seek #${target} ignored — the current run already starts there`
2028
+ );
2029
+ session.seekTarget = null;
2030
+ session.seekFirstFarAt = 0;
2031
+ return;
2032
+ }
2009
2033
  // Minimum gap between actual restarts (the settle already collapses bursts;
2010
2034
  // this only guards back-to-back seeks). If still cooling down, re-arm once
2011
2035
  // for the remaining cooldown instead of restarting now.
@@ -2040,13 +2064,26 @@ export class HlsSessionManager {
2040
2064
  producedThisRun < this.segmentDurationSec &&
2041
2065
  sinceLastRestart < RUN_FIRST_SEGMENT_GRACE_MS
2042
2066
  ) {
2067
+ logger.info(
2068
+ `transcode ${session.id} seek #${target} HELD — current run has produced ` +
2069
+ `${producedThisRun.toFixed(1)}s of the ${this.segmentDurationSec}s first segment ` +
2070
+ `(${(sinceLastRestart / 1000).toFixed(1)}s into a ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s grace)`
2071
+ );
2043
2072
  session.seekSettleTimer = setTimeout(() => this.#fireSettledSeek(session), SEEK_SETTLE_MS);
2044
2073
  session.seekSettleTimer.unref?.();
2045
2074
  return;
2046
2075
  }
2076
+ // Why the restart was allowed — the counterpart of the HELD line above.
2077
+ // Without it a restart is indistinguishable from the runaway ping-pong this
2078
+ // guard exists to stop, and diagnosing a field report becomes guesswork.
2079
+ const allowedBecause = !runIsAlive
2080
+ ? "run is dead"
2081
+ : producedThisRun >= this.segmentDurationSec
2082
+ ? `run produced ${producedThisRun.toFixed(1)}s (first segment done)`
2083
+ : `grace of ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s expired`;
2047
2084
  session.seekTarget = null;
2048
2085
  session.seekFirstFarAt = 0;
2049
- logger.info(`transcode ${session.id} seek settle → restart at segment #${target}`);
2086
+ logger.info(`transcode ${session.id} seek settle → restart at segment #${target} (${allowedBecause})`);
2050
2087
  void this.#startEncodeRun(session, target);
2051
2088
  }
2052
2089