@torrent-tv/proxy 2.9.52 → 2.9.54

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,12 @@
1
+ ## 2.9.54
2
+
3
+ - **Fix**: Seeking left playback permanently frozen — the root cause behind the field reports of "seeking does nothing" / "100% • starting now on a dead player". After a seek the player fetched the target segment successfully, over and over (field log: segments 402 and 403 re-requested in a loop for more than two minutes at full link speed, ~250-340 KB each time, browser buffer stuck at 0.0 s) while the transcode itself was healthy. Cause: ffmpeg's HLS/fMP4 output writes `tfdt` (the box that says WHERE a fragment sits on the timeline) as **0** in every seek-restart run, and records the run's start offset in an `elst` edit list inside **that run's** init segment instead. That is self-consistent only while init and segments come from the same run — but the player fetches `#EXT-X-MAP` exactly once, so we must serve one init for the whole session. Read against that cached init, a post-seek segment loses its offset completely and claims to start at ~0 s; the player finds nothing at the position it seeked to, discards the segment and re-requests it, forever. **No ffmpeg configuration avoids this** — measured on the shipping build: HLS *and* DASH muxers, `-copyts`, `-output_ts_offset`, `-itsoffset`, `-avoid_negative_ts disabled`, `-movflags -use_edts/+dash/+frag_discont/+global_sidx`, `-video_track_timescale`; all emit `tfdt = 0`. Fixed by stamping each fragment's `tfdt` with the segment's true start time as it is served, which is what CMAF (ISO/IEC 23000-19) requires of an independently-addressable segment in the first place: the segment then carries its own position and is valid against any init for the same tracks. Verified against a reproduction of the field scenario (several consecutive seek-restarts, video+audio): a post-seek segment read with the session-cached init reports its true timestamp (80.1 s) instead of 0.083 s, and decodes cleanly.
4
+ - **New**: The HLS output container is now selectable — `--segment-format fmp4` (default) or `--segment-format mpegts` — in the spirit of Jellyfin's transcoding-container setting. Everything container-specific (muxer arguments, segment naming and matching, playlist header lines, and the per-segment serving hook) lives behind a single interface in `services/segment-formats/`, so `hls-session-manager` holds a format object and never branches on the container; adding a container means adding a module, not editing the session manager. The MPEG-TS path is the pre-fMP4 behaviour recovered from the original switch commit rather than a rewrite; its segments are self-contained (no init segment at all), so the entire class of problem fixed above cannot arise there, which makes it a genuine fallback rather than a downgrade.
5
+
6
+ ## 2.9.53
7
+
8
+ - **Fix**: On the video RE-ENCODE path, `processedSeconds` silently switched reference frame partway through every encode run — absolute (matching `startPositionSeconds`) for the placeholder set at restart, then RELATIVE-to-the-run (counting from ~0) the moment ffmpeg's own `-progress out_time`/`out_time_ms` started overwriting it — because `-output_ts_offset` (used to relabel the MUXED output's timestamps onto the absolute grid) does NOT affect what `-progress` reports; verified empirically (a 5s clip encoded with `-output_ts_offset 100` still reports `out_time` counting 0→5, not 100→105). Every consumer of `session.progress.processedSeconds` assumes it is absolute: `computeProgressMetrics` (percent/remaining), `#applyBudgetDownshift`'s mid-run restart point, and — the field-diagnosed symptom — `#ensureEncodingFor`'s look-ahead window, which anchors on `Math.max(head, segmentIndexForTime(processed))`; with `processed` wrongly near-zero this floor pins the window's advancing edge at the run's OWN start segment for its entire lifetime instead of tracking real progress, so any segment request more than `MAX_LOOKAHEAD_SEGMENTS` (8, ≈32s) past the SEEK TARGET reads as "far" and triggers ANOTHER restart — even while the encoder is happily producing well past that point. Field example (verified with a pure-math replay of the exact logged values): seek to 1824s, window pinned at segments 456–464 for the whole run regardless of real progress reaching segment 465+ within seconds, at 6x realtime. This is the mechanism behind "buffering pill stuck at 0% until playback finally starts" and very likely a contributor to the broader "seek gets stuck" class of reports this cycle. Fixed by rebasing `out_time`/`out_time_ms` onto the absolute timeline (`+ session.progress.startPositionSeconds`) for the re-encode branch only — the copy branch already reports absolute time via `-copyts`, unaffected. Verified: a standalone replay of the field's `processed`/`startPos` sequence through the actual `#segmentIndexForTime` algorithm shows the window frozen at the run's start before the fix, correctly advancing with real progress after.
9
+
1
10
  ## 2.9.52
2
11
 
3
12
  - **Chore**: `npm audit` fixes. `@fastify/static` 9.1.3 → 10.1.2 (fixes GHSA-83w8-p2f5-377r route-guard path-traversal bypass and GHSA-8pvw-jcv7-9cmj non-canonical-path authorization bypass — no API change to our usage, verified with a live smoke test: healthz, tunnel connect, and static registration all still work). `brace-expansion`/`fast-uri`/`find-my-way` bumped via `npm audit fix` (transitive, no direct dependency change). Residual: `ip` (via `webtorrent@2.8.5` → `torrent-discovery` → `bittorrent-tracker`) stays flagged high (GHSA-2p57-rm9w-gvfp / CVE-2024-29415, SSRF via `isPublic()` misclassification) — investigated and left as an accepted risk, not an oversight: the advisory has no upstream fix (`first_patched_version: null`, every published version of `ip` is flagged) and `npm audit fix --force`'s only offered fix is downgrading `webtorrent` to 0.7.3, which would reintroduce the exact download-freeze regressions 2.9.44 rolled back from 3.x to avoid. The only actual call site in our dependency tree (`bittorrent-tracker/lib/server/parse-udp.js`) uses `ip.toString()` for UDP-integer→string formatting in the tracker-SERVER's request parser — code we never execute (WebTorrent only uses `bittorrent-tracker` as a tracker CLIENT) — and the vulnerable function itself, `isPublic()`, is not called anywhere in the chain. Revisit if/when a maintained `ip` replacement lands upstream in `bittorrent-tracker`.
package/bin/cli.js CHANGED
@@ -21,6 +21,7 @@ import { createDataChannelHandler } from "../services/data-channel-handler.js";
21
21
  import { collectHealthMetrics } from "../services/health-collector.js";
22
22
  import { createPortMapper } from "../services/port-mapper.js";
23
23
  import { classifyNat } from "../services/nat-classifier.js";
24
+ import { DEFAULT_SEGMENT_FORMAT_ID, SEGMENT_FORMAT_IDS } from "../services/segment-formats/index.js";
24
25
  import { logger } from "../utils/logger.js";
25
26
 
26
27
  const require = createRequire(import.meta.url);
@@ -74,6 +75,11 @@ program
74
75
  .option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
75
76
  .option("--max-disk-bytes <bytes>", "Cap total downloaded torrent data (0 = disabled; default min(10GB, half free disk))")
76
77
  .option("--ffmpeg-bin <path>", "Path to ffmpeg binary")
78
+ .option(
79
+ "--segment-format <format>",
80
+ `HLS output container: ${SEGMENT_FORMAT_IDS.join(" | ")}`,
81
+ DEFAULT_SEGMENT_FORMAT_ID
82
+ )
77
83
  .option("--token <token>", "Registration token", "")
78
84
  .addHelpText("after", HELP_EXAMPLES);
79
85
 
@@ -258,7 +264,8 @@ try {
258
264
  port: localPort,
259
265
  transcodeAudio,
260
266
  ffmpegBin,
261
- maxDiskBytes
267
+ maxDiskBytes,
268
+ segmentFormat: options.segmentFormat
262
269
  });
263
270
  app = started.app;
264
271
  actualPort = started.port;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.52",
3
+ "version": "2.9.54",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
package/server.js CHANGED
@@ -62,6 +62,7 @@ function buildPortCandidates(startPort, maxAttempts = 51) {
62
62
  * @property {boolean} transcodeAudio - Whether HLS audio transcoding is enabled.
63
63
  * @property {string} ffmpegBin - Path to the ffmpeg executable.
64
64
  * @property {number} [maxDiskBytes] - Global disk cap for torrent data (undefined = pool default).
65
+ * @property {string} [segmentFormat] - HLS output container: "fmp4" (default) or "mpegts".
65
66
  */
66
67
 
67
68
  /**
@@ -70,7 +71,7 @@ function buildPortCandidates(startPort, maxAttempts = 51) {
70
71
  * @param {ProxyServerOptions} options
71
72
  * @returns {Promise<{ app: import("fastify").FastifyInstance, port: number }>}
72
73
  */
73
- export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin, maxDiskBytes }) {
74
+ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin, maxDiskBytes, segmentFormat }) {
74
75
  const app = Fastify({
75
76
  // No practical body-size limit — the proxy server is localhost-only and
76
77
  // receives torrent source payloads that may be arbitrarily large.
@@ -126,6 +127,7 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
126
127
  videoEncoder,
127
128
  softwarePresetBenchmark,
128
129
  tonemapSupported,
130
+ segmentFormatId: segmentFormat,
129
131
  // Live download stats accessor for the realtime budget: lets it tell a
130
132
  // CPU-bound transcode from a download-starved input before downscaling.
131
133
  getSourceStats: async (sourceKey, fileIndex) => {