@torrent-tv/proxy 2.15.1 → 2.15.2

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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.15.2
2
+
3
+ - **Fix**: A segment request that can never be answered is answered as absent instead of being held for a minute. Changing audio track makes hls.js ask the new stream for segment #0 before anything else; the run was at #354, the repair reaches sixty segments back and no further, no seek was coming, and an encoder only moves forward — so the request was unanswerable from the moment it arrived, and holding it simply spent the player's own patience. Measured 2026-08-15: the track was made ready in 7.1 s at the viewer's position, and the viewer then watched a spinner for **63 s** — sixty of them the hold, the rest the player recovering after it failed. Deliberately narrower than the refusal 2.14.1 shipped and 2.14.2 withdrew: a request within the repair's reach, or one with a seek on its way, is still held, because for those the encoder is about to be moved there.
4
+
1
5
  ## 2.15.1
2
6
 
3
7
  - **Fix**: A magnet that never found its metadata no longer makes the same film unplayable from its own `.torrent`. One infohash is one torrent, so the second add is refused and the pool takes the one already there — which is right when it is ready and wrong when it is not: a magnet whose swarm has not answered has no file list, and everything bound to it is answered 404. Measured 2026-08-15 on the addon host: a magnet with no reachable trackers was added first, the film's own `.torrent` then joined that empty torrent instead of replacing it, `/stream` answered 404, the encoder died on its first read, and the film stayed unplayable until the proxy was restarted. A `.torrent` carries the file list, the piece hashes and the trackers outright, so when it meets a torrent with no metadata it now replaces it; two magnets still wait, because neither has anything the other lacks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.15.1",
3
+ "version": "2.15.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -6067,6 +6067,35 @@ export class HlsSessionManager {
6067
6067
  // at this position (server-side seeking). The caller long-polls.
6068
6068
  if (!isPlaylist) {
6069
6069
  const requestedIndex = session.segmentFormat.segmentIndexFromName(fileName);
6070
+ // Unanswerable, and known to be: behind a run that only moves forward,
6071
+ // too far behind for the repair to fetch it, and no seek on its way to
6072
+ // move the encoder there. Holding it changes nothing about whether it can
6073
+ // be produced — it only spends the player's patience.
6074
+ //
6075
+ // This is what a track change costs when it is held instead: measured
6076
+ // 2026-08-15, hls.js asked the new track for segment #0 while the run was
6077
+ // at #354, the request was held for the full minute, and only when it
6078
+ // failed did the player move to the segment it actually needed — 63 s of
6079
+ // spinner after a track that had been made ready in 7.
6080
+ //
6081
+ // Narrow on purpose. A request behind the head is USUALLY temporary: the
6082
+ // repair moves the encoder back for anything within its reach, and a
6083
+ // reported seek is about to move it anyway. Refusing those was 2.14.1,
6084
+ // and it left a viewer retrying a 404 for ever.
6085
+ if (
6086
+ Number.isFinite(requestedIndex) &&
6087
+ requestedIndex < (session.encodeStartIndex ?? 0) &&
6088
+ (session.encodeStartIndex ?? 0) - requestedIndex > BEHIND_HEAD_REPAIR_MAX_SEGMENTS &&
6089
+ session.ffmpeg != null &&
6090
+ session.seekTarget == null &&
6091
+ session.seekSettleTimer == null
6092
+ ) {
6093
+ logger.info(
6094
+ `transcode ${session.id} segment #${requestedIndex} is ${(session.encodeStartIndex ?? 0) - requestedIndex} ` +
6095
+ `segments behind the run and beyond the repair's reach; answered as absent rather than held`
6096
+ );
6097
+ return { kind: "not-found" };
6098
+ }
6070
6099
  this.#ensureEncodingFor(
6071
6100
  session,
6072
6101
  requestedIndex,
@@ -108,12 +108,11 @@ test("a burst of different segments behind the run is a scan, and moves nothing"
108
108
  for (const index of scanned) {
109
109
  session.firstWantedAt.set(index, Date.now() - 1000);
110
110
  }
111
- // Interleaved, as they arrive on the wire: the player opens them together
112
- // rather than finishing with one before opening the next.
113
- for (const seq of [1, 2]) {
114
- for (const index of scanned) {
115
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(index), { requestSeq: seq });
116
- }
111
+ // Asked ONCE each, which is what a scan is: the player opens them together
112
+ // and abandons them together. Field log 2026-08-02 #178, #681, #725, #807,
113
+ // #74, #245, #387 within half a second, none of them repeated.
114
+ for (const index of scanned) {
115
+ await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(index), { requestSeq: 1 });
117
116
  }
118
117
 
119
118
  assert.equal(
@@ -219,3 +218,34 @@ test("a request ahead of the run is not touched", async (t) => {
219
218
  "the running encode may yet reach it; restarting on a far request is what produced nine restarts in a minute"
220
219
  );
221
220
  });
221
+
222
+ test("a request far beyond the repair's reach is answered at once, not held", async (t) => {
223
+ const { manager, session, dirPath } = await managerWithRunAhead();
224
+ t.after(async () => {
225
+ await manager.disposeAll();
226
+ await rm(dirPath, { recursive: true, force: true });
227
+ });
228
+ // What a track change does: hls.js asks the new stream for segment #0 while
229
+ // the run is hundreds of segments in. The repair cannot reach it, no seek is
230
+ // coming, and the run only moves forward — so it can never be produced.
231
+ // Measured 2026-08-15: held for the full minute, and the viewer watched a
232
+ // spinner for 63 s after a track that had been made ready in 7.
233
+ const answer = await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(0), { requestSeq: 1 });
234
+
235
+ assert.equal(answer.kind, "not-found", "answered, so the player can move on to what it can have");
236
+ assert.equal(session.seekTarget, null, "and the encoder was not sent to the start of the film for it");
237
+ });
238
+
239
+ test("a request just behind the run is still held, because the repair will fetch it", async (t) => {
240
+ const { manager, session, dirPath } = await managerWithRunAhead();
241
+ t.after(async () => {
242
+ await manager.disposeAll();
243
+ await rm(dirPath, { recursive: true, force: true });
244
+ });
245
+ const name = fmp4Format.segmentFileName(WANTED);
246
+ session.firstWantedAt.set(WANTED, Date.now() - 1000);
247
+
248
+ const answer = await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
249
+
250
+ assert.equal(answer.kind, "warming-up", "within reach: the encoder is about to be moved there");
251
+ });