@torrent-tv/proxy 2.9.88 → 2.9.90
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/package.json +1 -1
- package/routes/api/transcode-sessions/post.js +7 -1
- package/routes/transcode/session-file/get.js +17 -0
- package/services/hls-session-manager.js +2832 -2779
- package/services/torrent-pool.js +31 -123
- package/services/torrent-worker/piece-reader.js +232 -43
- package/test/read-window.test.js +242 -0
- package/test/segment-hold-supersede.test.js +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 2.9.90
|
|
2
|
+
|
|
3
|
+
- **New**: The output container is chosen per session, by the viewer, instead of once per proxy. `POST /api/transcode-sessions` accepts `segmentFormat`; `--segment-format` remains the default for a client that expresses no preference, and an unrecognised value falls back to it rather than to the library default. The browser is the only party that knows what its media stack will accept for the tracks it asked to be copied: a copied MP3 track cannot be appended from fMP4 at all (`audio/mp4; codecs="mp4a.69"` is refused by MediaSource) but works from MPEG-TS, which hls.js demuxes itself and hands to a plain `audio/mpeg` buffer — the same file, the same browser, silent loop one way and normal playback the other. Sessions are keyed by container too, so two viewers wanting different ones do not share an encoder. Nothing branches on the format outside `services/segment-formats/`; the manager now reads it off the session.
|
|
4
|
+
|
|
5
|
+
## 2.9.89
|
|
6
|
+
|
|
7
|
+
- **Fix**: What the torrent downloads is now decided by the readers, and by nobody else. Three places were claiming pieces for the same file and overwriting each other on every request: `acquireFile` selected the whole file, `prioritizeByteRange` selected from the read position to the end, and the reader selected its entire requested range. The reader's claim was the worst of the three — ffmpeg opens its input as `bytes <position>-<EOF>`, so the first read of a session claimed the **whole file** and marked **every piece critical**, and nothing ever gave it back, because that read is abandoned a second later when ffmpeg seeks. No later prioritisation could outrank a permanent whole-file claim, which is why 2.9.88 changed nothing measurable. Each read now holds a moving window ahead of its own head, as a **stream selection** — the kind WebTorrent counts rather than merges, so several parallel readers (the codec probe's head and tail, subtitles, one input per viewer) produce the union of their windows — and releases it on completion, cancellation and abandonment. `critical` marks only the piece being waited for and at most two more, which is the rule WebTorrent's own reader uses and what the flag is supposed to mean. `prioritizeByteRange` keeps only what readers cannot do: the read position for the resume figures, and the jump log line.
|
|
8
|
+
- **Fix**: A seek releases the segment requests it made pointless. hls.js keeps one fragment load outstanding, so a request being held for the old position blocks the one for the new position — measured 2026-08-04: a backward seek into fully downloaded data waited **57 s** for a held request for `#609` to run out the 60 s hold, then fetched the segment it wanted in 15 ms. The same hold trapped 45 requests at once during a forward seek. A viewer seek now ends every wait that started before it with a retryable 503, as `hls-media-server` does (`research/hls-seek-prior-art-2026-08-02.md`, prescribed there and never built).
|
|
9
|
+
|
|
1
10
|
## 2.9.88
|
|
2
11
|
|
|
3
12
|
- **Fix**: A seek no longer makes the swarm walk the file to get there. Two faults, both confirmed by running WebTorrent's own selection code on the numbers of a measured session (588 pieces, download at 38.4%, seek to 89.1%). First: a selection carries an `offset` — how many pieces from its start are already downloaded — and the picker scans from `from + offset`; `deselect` subtracts an interval and copies that offset into what survives, so demoting the pieces behind the playhead left `{523-587, offset 226}`, a selection whose scan begins at piece 749 of 587. The seek target ended up wanted by nobody. The range is now re-selected right after the demotion, which replaces the dead entry with a fresh one starting at the playhead. Second: a request with no byte range was reported as an ordinary read at offset 0, and ffmpeg opens its input with exactly such a request and abandons it as soon as it seeks — as do the keyframe index and the codec probe, four of them around every encoder restart. Each one re-selected the whole file from piece zero, undoing the seek; the picker then skipped what was on disk and downloaded forward from the first hole. Measured cost of the pair: a seek to 89.1% of a 4.7 GB film fetched **2.47 GB over 93 s** where one 8 MiB piece was needed. A range-less read now sets the read position only when nothing else has.
|
package/package.json
CHANGED
|
@@ -39,6 +39,11 @@ export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionMan
|
|
|
39
39
|
const manualQuality = payload.manualQuality === true;
|
|
40
40
|
const startPositionSeconds = Number(payload.startPositionSeconds);
|
|
41
41
|
const audioTrackIndex = Number(payload.audioTrackIndex);
|
|
42
|
+
// Which container to produce. The browser knows what its media stack will
|
|
43
|
+
// accept for the tracks it asked to be copied; an absent or unknown value
|
|
44
|
+
// leaves the proxy's own `--segment-format` in charge.
|
|
45
|
+
const segmentFormatId =
|
|
46
|
+
typeof payload.segmentFormat === "string" ? payload.segmentFormat.trim() : "";
|
|
42
47
|
|
|
43
48
|
if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
|
|
44
49
|
return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
|
|
@@ -60,7 +65,8 @@ export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionMan
|
|
|
60
65
|
? startPositionSeconds
|
|
61
66
|
: 0,
|
|
62
67
|
audioTrackIndex:
|
|
63
|
-
Number.isInteger(audioTrackIndex) && audioTrackIndex > 0 ? audioTrackIndex : 0
|
|
68
|
+
Number.isInteger(audioTrackIndex) && audioTrackIndex > 0 ? audioTrackIndex : 0,
|
|
69
|
+
segmentFormatId
|
|
64
70
|
});
|
|
65
71
|
return reply.send({
|
|
66
72
|
sessionId: session.id,
|
|
@@ -75,6 +75,13 @@ export async function handleTranscodeSessionFileGet(req, reply, { hlsSessionMana
|
|
|
75
75
|
if (result.kind === "not-found") {
|
|
76
76
|
return reply.code(404).send({ error: "Transcode session file was not found." });
|
|
77
77
|
}
|
|
78
|
+
if (result.kind === "superseded") {
|
|
79
|
+
// The viewer moved while this was being held. Answer at once so the player
|
|
80
|
+
// can ask for where it is now; `Retry-After: 0` because there is nothing to
|
|
81
|
+
// wait for — this segment is simply no longer the one being watched.
|
|
82
|
+
reply.header("Retry-After", "0");
|
|
83
|
+
return reply.code(503).send({ error: "Superseded by a seek." });
|
|
84
|
+
}
|
|
78
85
|
if (result.kind === "warming-up") {
|
|
79
86
|
// The segment is still being produced (e.g. just after a seek-restart).
|
|
80
87
|
// Return a retryable 503 — never 202, which hls.js cannot consume as a
|
|
@@ -122,11 +129,21 @@ async function waitForSessionFile(hlsSessionManager, sessionId, fileName, timeou
|
|
|
122
129
|
// again — see HlsSessionManager#ensureEncodingFor for the encoder ping-pong
|
|
123
130
|
// this prevents when one seek-bar scrub fires several segment requests.
|
|
124
131
|
const requestSeq = hlsSessionManager.nextRequestSeq(sessionId);
|
|
132
|
+
// The viewer's position when this request was made. A seek makes every held
|
|
133
|
+
// request stale — it asks for a segment nobody is going to watch — and hls.js
|
|
134
|
+
// keeps only ONE fragment load outstanding, so holding on blocks the request
|
|
135
|
+
// the player actually needs now. Measured: 57 s of a 58 s backward seek was
|
|
136
|
+
// this wait, and the segment the viewer wanted took 15 ms once it was asked
|
|
137
|
+
// for.
|
|
138
|
+
const seekEpoch = hlsSessionManager.seekEpoch(sessionId);
|
|
125
139
|
while (Date.now() - startedAt < timeoutMs) {
|
|
126
140
|
const result = await hlsSessionManager.getFileStream(sessionId, fileName, { requestSeq });
|
|
127
141
|
if (result.kind !== "warming-up") {
|
|
128
142
|
return result;
|
|
129
143
|
}
|
|
144
|
+
if (hlsSessionManager.seekEpoch(sessionId) !== seekEpoch) {
|
|
145
|
+
return { kind: "superseded" };
|
|
146
|
+
}
|
|
130
147
|
await delay(300);
|
|
131
148
|
}
|
|
132
149
|
return { kind: "warming-up" };
|