@torrent-tv/proxy 2.9.46 → 2.9.47

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,7 @@
1
+ ## 2.9.47
2
+
3
+ - **Fix**: Playback could get permanently stuck (hls.js endlessly re-fetching the manifest and the first segment, buffer never advancing) even though the transcode itself was encoding fine, running ahead of realtime. Root cause: ffmpeg creates the fMP4 `init.mp4` file before it finishes writing the codec-header boxes into it (unlike segments, its write is not gated behind an atomic rename), so a request could race a moment where the file exists but is still empty. That empty read was then cached forever as the session's init segment — a zero-length `Buffer` is still a truthy object, so the `if (session.initBytes)` cache guard treated it as "already resolved" and kept serving the empty file for the rest of the session, which hls.js can never initialize a SourceBuffer from. Fixed by treating a zero-byte read as not-yet-ready (keeps the caller's existing long-poll retrying) instead of caching it as final.
4
+
1
5
  ## 2.9.46
2
6
 
3
7
  - **New**: `getFileStats` now reports `resumeNeededBytes` / `resumeDownloadedBytes` — the bytes still to download in the 16 MB window ahead of the file's current read position (tracked per file by `prioritizeByteRange`, cleared on torrent removal), counted byte-accurately including partial pieces. Lets the browser show how much is left to download and the time to resume while buffering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.46",
3
+ "version": "2.9.47",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -48,6 +48,15 @@ const MAX_LOOKAHEAD_SEGMENTS = 8;
48
48
  // succession (stall-recovery seeks); without a cooldown ffmpeg ping-pongs
49
49
  // between positions, restarting endlessly and producing nothing.
50
50
  const RESTART_COOLDOWN_MS = 4_000;
51
+ // Encoder stall watchdog. A running ffmpeg emits `-progress` output on stdout
52
+ // continuously while it encodes; when it hangs mid-file (alive, but producing
53
+ // no output and no stderr — a deadlock, e.g. a stalled input read), that output
54
+ // stops and `progress.updatedAt` freezes. If a segment INSIDE the look-ahead
55
+ // window is being demanded but progress has not advanced for this long, the
56
+ // encoder is wedged (observed: the segment 503s forever). Treat it like a seek
57
+ // and restart ffmpeg at the demanded segment. Conservative — a slow-but-moving
58
+ // encode keeps advancing `updatedAt`, so this only fires on a true freeze.
59
+ const ENCODER_STALL_MS = 12_000;
51
60
  // Seek debounce. A far (out-of-window) segment request is a server-side seek.
52
61
  // Rather than restart ffmpeg on the first one, wait a short quiet period:
53
62
  // further far requests re-arm it and update the target to the latest index, so
@@ -1804,12 +1813,28 @@ export class HlsSessionManager {
1804
1813
  // and position-independent, but each seek-restart run REWRITES init.mp4, so
1805
1814
  // cache the FIRST one and always serve that — otherwise the init the player
1806
1815
  // fetched could differ from a later run's, breaking playback after a seek.
1816
+ //
1817
+ // ffmpeg creates init.mp4 before it has finished writing the fMP4 header
1818
+ // boxes into it (unlike segments, its write is not gated behind an atomic
1819
+ // rename), so a read can race a moment where the file EXISTS but is still
1820
+ // EMPTY. Root cause of a real incident: that empty read used to be cached
1821
+ // as `session.initBytes` — a zero-length Buffer is still a truthy object,
1822
+ // so `if (session.initBytes)` treated it as "already resolved" and served
1823
+ // the empty file for the rest of the session's life, permanently breaking
1824
+ // playback (hls.js can never initialize its SourceBuffer from an empty
1825
+ // init segment) while the transcode itself kept encoding normally. Guard
1826
+ // on non-empty content on both the cache check and the fresh read, so an
1827
+ // empty read is treated as not-yet-ready and the caller's long-poll keeps
1828
+ // retrying until ffmpeg has actually written the header.
1807
1829
  if (fileName === SEGMENT_INIT_FILE_NAME) {
1808
- if (session.initBytes) {
1830
+ if (session.initBytes && session.initBytes.length > 0) {
1809
1831
  return { kind: "file", stream: Readable.from([session.initBytes]), contentType: "video/mp4", isPlaylist: false };
1810
1832
  }
1811
1833
  try {
1812
1834
  const bytes = await readFile(path.join(session.dirPath, SEGMENT_INIT_FILE_NAME));
1835
+ if (bytes.length === 0) {
1836
+ return { kind: "warming-up" };
1837
+ }
1813
1838
  session.initBytes = bytes;
1814
1839
  return { kind: "file", stream: Readable.from([bytes]), contentType: "video/mp4", isPlaylist: false };
1815
1840
  } catch {