@torrent-tv/proxy 2.9.50 → 2.9.52
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 +8 -0
- package/package.json +2 -2
- package/services/hls-session-manager.js +40 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.9.52
|
|
2
|
+
|
|
3
|
+
- **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`.
|
|
4
|
+
|
|
5
|
+
## 2.9.51
|
|
6
|
+
|
|
7
|
+
- **Fix**: The 2.9.50 keyframe-snap seek fix did not reliably apply on the re-encode path for containers needing a full packet scan (observed: AVI). The probe ran with a 6 s cap shared with the video-copy path (there it is fast, moov-index based); on a container needing a full scan, 6 s was not enough, the probe returned null, and the seek fell back to the raw (unsnapped) target — the exact case the circuit breaker exists to catch, not prevent. Split the two paths: video-copy keeps the blocking 6 s probe (segment boundaries need it before the first segment can be produced); video re-encode now runs the probe in the BACKGROUND with a full 25 s budget, since segment boundaries there are the uniform grid and never depend on it — only a later seek benefits from the snap. `#startEncodeRun` already reads `session.keyframeTimes` fresh on every call, so a seek arriving after the background probe resolves picks up the snap automatically; one arriving before still falls back to the existing circuit breaker (no regression). Verified live on the field AVI: far seek to the previously-hanging segment now returns in ~12 s instead of the ~90 s stall.
|
|
8
|
+
|
|
1
9
|
## 2.9.50
|
|
2
10
|
|
|
3
11
|
- **Fix**: Seeking could get stuck in an infinite restart loop on some containers (observed: AVI with VBR MP3 audio), producing nothing for ~90 s until the whole WebRTC session died — the on-screen symptom of "seeking does nothing." Root cause, two parts: (1) `-accurate_seek -ss X` before `-i` trusts the container's own on-the-fly seek/index to land near X; for this AVI it pointed at a position with no valid frame boundary at all, so ffmpeg failed outright ("Seek failed" / "Header missing") — not just imprecisely — and every retry re-tried the SAME bad container-computed position. (2) `#ensureEncodingFor`/`#fireSettledSeek` never checked for a `"failed"` session state, and `#startEncodeRun` unconditionally resets state back to `"starting"` on every call — so a failed run's next client poll silently re-armed and re-ran the identical failing seek, forever. Fixed both: the video-keyframe probe (previously only used for the copy path's segment boundaries) now also feeds a two-step seek — jump to the nearest REAL keyframe (a position ffmpeg has already proven it can decode, read directly from the packet list, not the container's live index) before `-i`, then trim the short residual precisely after `-i` (always frame-accurate, no reliance on `-accurate_seek`'s trust in the container). A circuit breaker caps consecutive fast failures (exits within 2 s — never did real work) at the SAME target to 3 before the session is left in its terminal `failed` state instead of looping — a different seek target still gets a fresh attempt budget. Verified: the keyframe-snap helper against synthetic data, and the breaker's state machine (3 attempts at one target → blocked, a different target → fresh budget, only 4 real ffmpeg spawns instead of an unbounded loop).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@torrent-tv/proxy",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.52",
|
|
4
4
|
"description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@fastify/cors": "^11.2.0",
|
|
22
22
|
"@fastify/helmet": "^13.0.2",
|
|
23
|
-
"@fastify/static": "^
|
|
23
|
+
"@fastify/static": "^10.1.2",
|
|
24
24
|
"@silentbot1/nat-api": "^0.4.9",
|
|
25
25
|
"chalk": "^5.4.1",
|
|
26
26
|
"commander": "^12.1.0",
|
|
@@ -882,24 +882,57 @@ export class HlsSessionManager {
|
|
|
882
882
|
// branches; on failure both fall back to their current behaviour (uniform
|
|
883
883
|
// grid for boundaries, raw target for seeking) — no regression.
|
|
884
884
|
let keyframeTimes = null;
|
|
885
|
-
let keyframeMs = -1; // -1 = not run (skipped)
|
|
886
|
-
if (hasDuration) {
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
885
|
+
let keyframeMs = -1; // -1 = not run (skipped), -2 = running in the background
|
|
886
|
+
if (hasDuration && !transcodeVideo) {
|
|
887
|
+
// Video-COPY path: keyframeTimes are REQUIRED to build correct segment
|
|
888
|
+
// boundaries (the playlist itself), so this MUST block session creation —
|
|
889
|
+
// an incorrect playlist is worse than a slower start. Short timeout: mp4
|
|
890
|
+
// keyframes come from the moov index (fast); containers that force a full
|
|
891
|
+
// packet scan time out and fall back to a uniform grid, so this never adds
|
|
892
|
+
// more than ~6 s to session start.
|
|
890
893
|
const keyframeStartMs = Date.now();
|
|
891
894
|
keyframeTimes = await probeVideoKeyframeTimes(this.ffmpegBin, inputUrl.toString(), 6_000);
|
|
892
895
|
keyframeMs = Date.now() - keyframeStartMs;
|
|
893
896
|
if (!keyframeTimes) {
|
|
894
897
|
logger.warn(
|
|
895
|
-
`transcode ${sessionId}: keyframe probe unavailable; using uniform grid
|
|
898
|
+
`transcode ${sessionId}: keyframe probe unavailable; using uniform grid ` +
|
|
896
899
|
`for "${logName}" (seek precision may be reduced)`
|
|
897
900
|
);
|
|
898
901
|
}
|
|
902
|
+
} else if (hasDuration && transcodeVideo) {
|
|
903
|
+
// Re-encode path: keyframeTimes are ONLY used to snap a LATER seek (see
|
|
904
|
+
// #startEncodeRun) — segment boundaries stay on the uniform grid either
|
|
905
|
+
// way. So this does NOT need to block session creation / the first
|
|
906
|
+
// segment's start. Run it in the background with a FULL budget instead of
|
|
907
|
+
// the 6 s cap: AVI-class containers need a full packet scan, which 6 s can
|
|
908
|
+
// never afford without delaying playback start — that starved budget is
|
|
909
|
+
// exactly why the probe kept missing on the container where the seek bug
|
|
910
|
+
// was field-diagnosed. #startEncodeRun reads session.keyframeTimes fresh
|
|
911
|
+
// on every call, so a seek that happens AFTER this finishes picks it up
|
|
912
|
+
// automatically; one that happens before falls back to the existing
|
|
913
|
+
// circuit breaker as a safety net (no regression either way).
|
|
914
|
+
keyframeMs = -2;
|
|
915
|
+
const backgroundStartedAt = Date.now();
|
|
916
|
+
void probeVideoKeyframeTimes(this.ffmpegBin, inputUrl.toString(), 25_000).then((times) => {
|
|
917
|
+
const liveSession = this.sessionsById.get(sessionId);
|
|
918
|
+
if (!liveSession || liveSession.state === "disposed") {
|
|
919
|
+
return; // Session gone before the probe finished — nothing to update.
|
|
920
|
+
}
|
|
921
|
+
liveSession.keyframeTimes = times;
|
|
922
|
+
const elapsedMs = Date.now() - backgroundStartedAt;
|
|
923
|
+
logger.info(
|
|
924
|
+
times
|
|
925
|
+
? `transcode ${sessionId}: background keyframe probe found ${times.length} keyframes ` +
|
|
926
|
+
`(${elapsedMs}ms) for "${logName}" — later seeks will snap to them`
|
|
927
|
+
: `transcode ${sessionId}: background keyframe probe unavailable (${elapsedMs}ms) for "${logName}" ` +
|
|
928
|
+
`— seeks keep using the raw target (falls back to the circuit breaker on failure)`
|
|
929
|
+
);
|
|
930
|
+
});
|
|
899
931
|
}
|
|
900
932
|
logger.info(
|
|
901
933
|
`cold-start ${sessionId.slice(0, 8)}: media-info=${mediaInfoMs}ms (${mediaInfoSource}) ` +
|
|
902
|
-
`keyframes=${keyframeMs
|
|
934
|
+
`keyframes=${keyframeMs === -1 ? "skipped" : keyframeMs === -2 ? "background" : `${keyframeMs}ms`} ` +
|
|
935
|
+
`create-total=${Date.now() - createEntryMs}ms`
|
|
903
936
|
);
|
|
904
937
|
const segmentBoundaries = hasDuration
|
|
905
938
|
? computeSegmentBoundaries({
|