@torrent-tv/proxy 2.9.5 → 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 +9 -0
- package/CLAUDE.md +8 -3
- package/package.json +1 -1
- package/services/hls-session-manager.js +15 -0
- package/services/playback-planner.js +42 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
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
|
+
|
|
5
|
+
## 2.9.6
|
|
6
|
+
|
|
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.
|
|
8
|
+
- **New**: `GET /api/transcode-sessions/:id/progress` now includes `segmentDurationSec`, so the browser can show progress toward the first segment (the only thing it waits for before playback) instead of a percentage of the whole-file transcode.
|
|
9
|
+
|
|
1
10
|
## 2.9.5
|
|
2
11
|
|
|
3
12
|
- **Fix**: Segment files are now read with a 4 MB `highWaterMark` (`hls-session-manager.js` `getFileStream`) so the body is delivered in few, large chunks. On a busy ARM host the in-process WebTorrent hashing starves the Node event loop in bursts while the first segments are served; reading in fewer iterations cuts the time lost between chunks (the first segment previously transferred in ~79 × 43 KB reads spaced ~610 ms apart).
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
@@ -277,6 +277,17 @@ async function probeInputDurationSeconds(ffmpegBin, inputUrl) {
|
|
|
277
277
|
}, 8_000);
|
|
278
278
|
ffmpeg.stderr.on("data", (chunk) => {
|
|
279
279
|
stderr += String(chunk);
|
|
280
|
+
// ffmpeg prints the container header ("Duration:") almost immediately,
|
|
281
|
+
// long before it decodes anything. Bail as soon as we have it instead of
|
|
282
|
+
// letting `-f null -` decode the whole stream until the 8 s timeout.
|
|
283
|
+
const duration = parseFfmpegDurationSeconds(stderr);
|
|
284
|
+
if (duration != null) {
|
|
285
|
+
clearTimeout(timeoutId);
|
|
286
|
+
if (!ffmpeg.killed) {
|
|
287
|
+
ffmpeg.kill("SIGTERM");
|
|
288
|
+
}
|
|
289
|
+
finish(duration);
|
|
290
|
+
}
|
|
280
291
|
});
|
|
281
292
|
ffmpeg.on("error", () => {
|
|
282
293
|
clearTimeout(timeoutId);
|
|
@@ -983,6 +994,10 @@ export class HlsSessionManager {
|
|
|
983
994
|
remainingSeconds: session.progress.remainingSeconds,
|
|
984
995
|
warmupPercent,
|
|
985
996
|
warmupRemainingSeconds,
|
|
997
|
+
// Segment length, so the browser can show progress toward the FIRST
|
|
998
|
+
// segment (the only thing it waits for before playback starts) instead
|
|
999
|
+
// of a percentage of the whole-file transcode.
|
|
1000
|
+
segmentDurationSec: this.segmentDurationSec,
|
|
986
1001
|
speed: session.progress.speed,
|
|
987
1002
|
updatedAt: session.progress.updatedAt,
|
|
988
1003
|
error: session.state === "failed" ? session.lastError : ""
|
|
@@ -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)
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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
|
};
|