@torrent-tv/proxy 2.60.0 → 2.61.0
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 +1227 -1220
- package/package.json +1 -1
- package/routes/api/transcode-sessions/post.js +7 -1
- package/routes/api/transcode-sessions/seek/post.js +5 -1
- package/routes/transcode/session-file/get.js +23 -4
- package/services/hls-session-manager.js +9594 -9449
- package/test/held-request-width.test.js +148 -0
- package/test/seek-target-not-superseded.test.js +76 -0
package/package.json
CHANGED
|
@@ -146,7 +146,13 @@ export async function handleApiTranscodeSessionsPost(req, reply, { hlsSessionMan
|
|
|
146
146
|
// discovered. The browser checks what it actually got against this: a
|
|
147
147
|
// track that never arrives is otherwise noticed only by its absence,
|
|
148
148
|
// minutes later, as a black picture with working sound.
|
|
149
|
-
tracks: hlsSessionManager.declaredTracks(session)
|
|
149
|
+
tracks: hlsSessionManager.declaredTracks(session),
|
|
150
|
+
// How far ahead of the viewer this proxy lets the encoder run, in seconds
|
|
151
|
+
// of playback. The browser sizes its own forward buffer from it, so the
|
|
152
|
+
// two sides agree by construction instead of each carrying a constant of
|
|
153
|
+
// its own — which is how the browser came to hold thirty seconds while
|
|
154
|
+
// two minutes stood produced on disk (roadmap item 4).
|
|
155
|
+
lookaheadSeconds: hlsSessionManager.lookaheadSeconds
|
|
150
156
|
});
|
|
151
157
|
} catch (error) {
|
|
152
158
|
if (error instanceof Error && error.code === "TRANSCODE_DISABLED") {
|
|
@@ -36,7 +36,11 @@ export async function handleApiTranscodeSessionSeekPost(req, reply, { hlsSession
|
|
|
36
36
|
return reply.send({ error: "positionSeconds must be a non-negative number." });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Who moved. A session can be shared, and the seeking viewer's own position
|
|
40
|
+
// has to move with them or their next request is judged against where they
|
|
41
|
+
// were before the jump.
|
|
42
|
+
const consumerId = typeof body.consumerId === "string" ? body.consumerId.trim() : "";
|
|
43
|
+
const applied = hlsSessionManager.requestSeek(sessionId, positionSeconds, consumerId);
|
|
40
44
|
if (!applied) {
|
|
41
45
|
// Unknown or disposed session — nothing to steer. Not an error worth
|
|
42
46
|
// surfacing to the viewer: the seek will be handled by whatever session
|
|
@@ -58,6 +58,12 @@ export async function handleTranscodeSessionFileGet(req, reply, { hlsSessionMana
|
|
|
58
58
|
* @returns {Promise<void>}
|
|
59
59
|
*/
|
|
60
60
|
export async function serveSessionFile(req, reply, { hlsSessionManager, sessionId, fileName }) {
|
|
61
|
+
// Which viewer is asking. One session serves everyone watching a copied
|
|
62
|
+
// picture, so without it their positions collapse into one and a seek by the
|
|
63
|
+
// viewer in front releases the requests held for the viewer behind. Absent on
|
|
64
|
+
// a plain HTTP transport, where no loader of ours builds the URL, and then
|
|
65
|
+
// the single shared position decides as it always did.
|
|
66
|
+
const consumerId = typeof req.query?.consumer === "string" ? req.query.consumer : "";
|
|
61
67
|
// Hold the request only briefly, then answer "retry" instead of waiting for
|
|
62
68
|
// the segment. iOS's native HLS player (AVPlayer) enforces a hard ~3.5 s
|
|
63
69
|
// deadline on RESPONSE HEADERS and raises -12889 ("No response for media
|
|
@@ -79,7 +85,13 @@ export async function serveSessionFile(req, reply, { hlsSessionManager, sessionI
|
|
|
79
85
|
const onClientAbort = () => { clientAborted = true; };
|
|
80
86
|
req.raw.on("close", onClientAbort);
|
|
81
87
|
|
|
82
|
-
const result = await waitForSessionFile(
|
|
88
|
+
const result = await waitForSessionFile(
|
|
89
|
+
hlsSessionManager,
|
|
90
|
+
sessionId,
|
|
91
|
+
fileName,
|
|
92
|
+
SEGMENT_WAIT_MS,
|
|
93
|
+
consumerId
|
|
94
|
+
);
|
|
83
95
|
|
|
84
96
|
req.raw.off("close", onClientAbort);
|
|
85
97
|
const heldMs = Date.now() - holdStartedAt;
|
|
@@ -148,9 +160,13 @@ export async function serveSessionFile(req, reply, { hlsSessionManager, sessionI
|
|
|
148
160
|
* @param {string} sessionId
|
|
149
161
|
* @param {string} fileName
|
|
150
162
|
* @param {number} timeoutMs
|
|
163
|
+
* @param {string} [consumerId] - Which viewer is asking, where the transport
|
|
164
|
+
* carries it. Their own position is what decides whether a held request has
|
|
165
|
+
* been made pointless by a seek — a session can have several viewers, and
|
|
166
|
+
* the seek epoch belongs to all of them.
|
|
151
167
|
* @returns {Promise<Awaited<ReturnType<import("../../../services/hls-session-manager.js").HlsSessionManager["getFileStream"]>>>}
|
|
152
168
|
*/
|
|
153
|
-
export async function waitForSessionFile(hlsSessionManager, sessionId, fileName, timeoutMs) {
|
|
169
|
+
export async function waitForSessionFile(hlsSessionManager, sessionId, fileName, timeoutMs, consumerId = "") {
|
|
154
170
|
const startedAt = Date.now();
|
|
155
171
|
// One sequence number for THIS request, reused by every poll below, so the
|
|
156
172
|
// session can tell a newly-arrived request apart from an old one polling
|
|
@@ -165,7 +181,10 @@ export async function waitForSessionFile(hlsSessionManager, sessionId, fileName,
|
|
|
165
181
|
// for.
|
|
166
182
|
let seekEpoch = hlsSessionManager.seekEpoch(sessionId);
|
|
167
183
|
while (Date.now() - startedAt < timeoutMs) {
|
|
168
|
-
const result = await hlsSessionManager.getFileStream(sessionId, fileName, {
|
|
184
|
+
const result = await hlsSessionManager.getFileStream(sessionId, fileName, {
|
|
185
|
+
requestSeq,
|
|
186
|
+
consumerId
|
|
187
|
+
});
|
|
169
188
|
if (result.kind !== "warming-up") {
|
|
170
189
|
return result;
|
|
171
190
|
}
|
|
@@ -175,7 +194,7 @@ export async function waitForSessionFile(hlsSessionManager, sessionId, fileName,
|
|
|
175
194
|
// seek notification and would otherwise be refused at the exact moment it
|
|
176
195
|
// is needed — measured 2026-08-18, two 503s within 80 ms on the segment
|
|
177
196
|
// at the seek target, after which the player never asked for it again.
|
|
178
|
-
if (!hlsSessionManager.requestStillWanted(sessionId, fileName)) {
|
|
197
|
+
if (!hlsSessionManager.requestStillWanted(sessionId, fileName, consumerId)) {
|
|
179
198
|
return { kind: "superseded" };
|
|
180
199
|
}
|
|
181
200
|
logger.info(
|