@torrent-tv/proxy 2.9.53 → 2.9.55
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 +10 -0
- package/bin/cli.js +8 -1
- package/package.json +1 -1
- package/routes/transcode/session-file/get.js +6 -1
- package/server.js +3 -1
- package/services/hls-session-manager.js +2374 -2312
- package/services/segment-formats/fmp4.js +82 -0
- package/services/segment-formats/index.js +77 -0
- package/services/segment-formats/mp4-boxes.js +160 -0
- package/services/segment-formats/mpegts.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 2.9.55
|
|
2
|
+
|
|
3
|
+
- **Fix**: One scrub of the seek bar could leave the encoder ping-ponging between positions with an empty player buffer for over a minute (field-diagnosed 2026-08-01). A single scrub makes the player fire SEVERAL segment requests within a few hundred milliseconds — field log: `#534`, `#694`, `#817`, `#828` within 361 ms — and each of them long-polls `getFileStream` every 300 ms until served. Every poll called `#ensureEncodingFor`, so the four in-flight requests took turns overwriting the seek target and restarting ffmpeg at each other's positions (`534→828→694→828→817→828`, six restarts), none surviving long enough to produce a segment: three of the four eventually timed out after 34-36 s and the fourth was served after 25 s, with the browser buffer at 0.0 s throughout. Fixed by giving each INCOMING request one sequence number (`nextRequestSeq`) that it keeps for all of its long-poll iterations, and letting only the newest request steer the encoder — an older request may still be served if its segment gets produced, but can no longer move the encode head. Verified by replaying the exact field sequence: 44 target switches before, 4 after (the initial burst, which the existing settle-debounce then collapses into a single restart), settling on the last-requested segment.
|
|
4
|
+
- **Fix**: The transcode percent read 0% for a whole run on **video-copy** sessions (`transcodeVideo:false`), the other half of the 2.9.53 timeline bug. That fix rebased ffmpeg's `-progress` `out_time` onto the absolute timeline only for the re-encode branch, on the assumption that `-copyts` already made the copy branch absolute. The assumption was never measured and is wrong: on the field host, `-ss 600 … -copyts -c:v copy` reports `out_time` = 0, 40.7, 54.9, 90.9 — relative to the run, exactly like the re-encode branch (field log: `processed=12.638` against `startPos=3312` at 12.6x speed). The rebase now applies to both branches, and both measurements are recorded in the code so neither can be exempted again without a fresh one.
|
|
5
|
+
|
|
6
|
+
## 2.9.54
|
|
7
|
+
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
|
|
1
11
|
## 2.9.53
|
|
2
12
|
|
|
3
13
|
- **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.
|
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
|
@@ -51,8 +51,13 @@ export async function handleTranscodeSessionFileGet(req, reply, { hlsSessionMana
|
|
|
51
51
|
*/
|
|
52
52
|
async function waitForSessionFile(hlsSessionManager, sessionId, fileName, timeoutMs) {
|
|
53
53
|
const startedAt = Date.now();
|
|
54
|
+
// One sequence number for THIS request, reused by every poll below, so the
|
|
55
|
+
// session can tell a newly-arrived request apart from an old one polling
|
|
56
|
+
// again — see HlsSessionManager#ensureEncodingFor for the encoder ping-pong
|
|
57
|
+
// this prevents when one seek-bar scrub fires several segment requests.
|
|
58
|
+
const requestSeq = hlsSessionManager.nextRequestSeq(sessionId);
|
|
54
59
|
while (Date.now() - startedAt < timeoutMs) {
|
|
55
|
-
const result = await hlsSessionManager.getFileStream(sessionId, fileName);
|
|
60
|
+
const result = await hlsSessionManager.getFileStream(sessionId, fileName, { requestSeq });
|
|
56
61
|
if (result.kind !== "warming-up") {
|
|
57
62
|
return result;
|
|
58
63
|
}
|
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) => {
|