@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.60.0",
3
+ "version": "2.61.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -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
- const applied = hlsSessionManager.requestSeek(sessionId, positionSeconds);
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(hlsSessionManager, sessionId, fileName, SEGMENT_WAIT_MS);
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, { requestSeq });
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(