@torrent-tv/proxy 2.9.6 → 2.9.7

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.7
2
+
3
+ - **Fix**: `playback-planner` retries the codec probe while the file header is still downloading and no longer caches an **empty** probe result. Previously a transient empty probe (common for a later file in a multi-file torrent whose pieces arrive late) was cached permanently, so the file was mis-planned as directly playable forever — an unsupported video codec (e.g. xvid) got copied and played as a **black screen**. The probe now retries (up to 60 s) until at least one codec is detected, and only a successful detection is cached.
4
+
1
5
  ## 2.9.6
2
6
 
3
7
  - **Fix**: `probeInputDurationSeconds` now returns as soon as ffmpeg prints the container header (`Duration:`) instead of letting `-f null -` decode the whole stream until the 8 s timeout. Transcode-session creation was wasting ~8.6 s per session on this redundant decode (the duration was already available from the header, and `playback-plan` had probed it moments earlier). Cuts session-creation latency from ~9.7 s to ~1 s.
package/CLAUDE.md CHANGED
@@ -50,9 +50,14 @@ Linux-only host (e.g. POSIX-only signals must degrade elsewhere).
50
50
  ## Changelog
51
51
 
52
52
  Every behavioural change must be recorded in `CHANGELOG.md` — add an entry under
53
- a new `## <version>` heading at the top (the next patch version that
54
- `npm run patch` will publish), following the existing
55
- `- **New**/**Fix**/**Chore**:` format. See the parent `../CLAUDE.md`.
53
+ a new `## <version>` heading at the top, following the existing
54
+ `- **New**/**Fix**/**Chore**:` format.
55
+
56
+ **Do NOT edit `package.json` version.** `npm run patch`/`minor` runs `npm
57
+ version …` which bumps it. Write the CHANGELOG entry at the version that bump
58
+ will produce: **current `package.json` version + 1 patch** (or + 1 minor).
59
+ Accumulate bullets into that single pending entry until it's published. See the
60
+ parent `../CLAUDE.md`.
56
61
 
57
62
  ## Release
58
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.6",
3
+ "version": "2.9.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -97,6 +97,18 @@ function probeStreamCodecs({ ffmpegBin, inputUrl, userAgent = "", timeoutMs = 8_
97
97
  });
98
98
  }
99
99
 
100
+ /**
101
+ * Resolve after a given number of milliseconds.
102
+ *
103
+ * @param {number} ms
104
+ * @returns {Promise<void>}
105
+ */
106
+ function delay(ms) {
107
+ return new Promise((resolve) => {
108
+ setTimeout(resolve, ms);
109
+ });
110
+ }
111
+
100
112
  /**
101
113
  * Build the direct stream URL for a source file served by the local proxy.
102
114
  *
@@ -198,28 +210,34 @@ export function createPlaybackPlanner({
198
210
  return plan;
199
211
  }
200
212
 
201
- // Pre-fetch file edges (head + tail) before probing so that WebTorrent
202
- // has the MOOV atom (or MKV EBML headers) ready for ffprobe.
203
- // Without this, ffprobe times out on fresh torrents whose MOOV sits at
204
- // the end of the file and hasn't been downloaded yet.
213
+ // Pre-fetch file edges (head + tail), then probe retrying while the
214
+ // file header is still downloading. In a multi-file torrent the pieces
215
+ // for a given file arrive unevenly, so the first probe can return empty
216
+ // codecs. A transient empty probe must NOT be cached: otherwise the wrong
217
+ // plan (file treated as directly playable) sticks permanently for this
218
+ // file, and an unsupported codec like xvid gets copied → black video.
205
219
  await torrentPool.prefetchFileEdges(torrent, fileIndex);
220
+ let probe = await probeStreamCodecs({ ffmpegBin, inputUrl: directUrl, userAgent });
221
+ const probeDeadline = Date.now() + 60_000;
222
+ let attempt = 0;
223
+ while (
224
+ probe.audioCodec.length === 0 &&
225
+ probe.videoCodec.length === 0 &&
226
+ Date.now() < probeDeadline
227
+ ) {
228
+ attempt += 1;
229
+ await delay(Math.min(3_000, 500 + attempt * 250));
230
+ await torrentPool.prefetchFileEdges(torrent, fileIndex);
231
+ probe = await probeStreamCodecs({ ffmpegBin, inputUrl: directUrl, userAgent });
232
+ }
233
+ const { audioCodec, videoCodec, container, durationSeconds } = probe;
234
+ const codecsDetected = audioCodec.length > 0 || videoCodec.length > 0;
206
235
 
207
- const { audioCodec, videoCodec, container, durationSeconds } = await probeStreamCodecs({
208
- ffmpegBin,
209
- inputUrl: directUrl,
210
- userAgent
211
- });
212
-
213
- // Only transcode when the codec is known AND not natively supported.
214
- // When ffprobe cannot detect the codec (e.g. the torrent has just started
215
- // downloading and the MOOV atom at the end of the MP4 is not yet available),
216
- // fall back to "direct" so the browser can attempt native playback. The
217
- // browser-side loading pipeline already has its own transcode fallback.
236
+ // `mode` is advisory only (audio-codec based). The browser makes the
237
+ // authoritative decision independently per stream via canPlayType /
238
+ // mediaCapabilities, transcoding only what it cannot play.
218
239
  const requiresTranscode = audioCodec.length > 0 && !DIRECT_AUDIO_CODECS.has(audioCodec);
219
240
  const plan = {
220
- // `mode` is advisory only (audio-codec based). The browser makes the
221
- // authoritative decision independently per stream via canPlayType /
222
- // mediaCapabilities, transcoding only what it cannot play.
223
241
  mode: requiresTranscode ? "hls" : "direct",
224
242
  directUrl,
225
243
  reason: requiresTranscode ? "audio-codec-transcode-required" : "audio-codec-supported",
@@ -228,7 +246,12 @@ export function createPlaybackPlanner({
228
246
  container,
229
247
  durationSeconds
230
248
  };
231
- cache.set(cacheKey, plan);
249
+ // Only cache a plan whose codecs were actually detected. An empty probe is
250
+ // a "header not downloaded yet" signal, not a valid result — caching it
251
+ // would permanently mis-plan the file.
252
+ if (codecsDetected) {
253
+ cache.set(cacheKey, plan);
254
+ }
232
255
  return plan;
233
256
  }
234
257
  };