@torrent-tv/proxy 2.10.0 → 2.11.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 CHANGED
@@ -1,3 +1,14 @@
1
+ ## 2.11.0
2
+
3
+ - **Fix**: A quality change places the new rung where the PLAYER asked for it, not where the rung being left had read to. After a level switch hls.js discards what it had buffered ahead and fetches from the picture's own position, so its first request for the new rung IS that position; the read head is a whole buffer further on. Measured 2026-08-11 on a switch back up to 400p: a 240p rung encoding at 5-6x had read 56 s past the picture, the run was placed at 3084 s, the player needed 3028 s, and nothing it asked for was ever produced.
4
+ - **Fix**: A segment request BELOW the running encode is repaired instead of being held for ever. The encoder only moves forward from where its run began, so such a request cannot be answered by anything that run does — every other far request is a claim the run may yet reach, this one is a hole. Same session: it was held for two minutes forty-one while the encoder produced 409 s of video nobody had asked for at 2.48x, and the viewer sat on a loading screen until they gave up. It now moves the encoder there, through the same settle a reported seek goes through, and never overrides a seek the viewer has actually stated.
5
+ - **New**: A quality variant can now accompany a COPIED video. The obstacle was never the encoder but the cut points: a copy can only be cut where the source already has a keyframe, while a re-encode was always cut on an even grid, so a rung's segment covered a different span from the copy's and could not stand in its place. A session now carries which grid it is cut on as a fact of its own, separately from whether its video is copied, and a variant of a keyframe-cut session inherits that grid — the same times serve as the muxer's cut list and as the keyframes the encoder is told to force. A copied stream is offered variants only when its own grid is real: with no readable keyframe index it falls back to an even grid that ffmpeg does not cut on, and nothing can be aligned to a fiction.
6
+ - **New**: What a container's keyframe index says about its own file is now counted and reported. The cut times of a copied video ARE that index, and an index can be wrong — measured 2026-08-06, one claimed a keyframe four seconds from where the real ones were. Every produced segment states where it truly begins and is already read whole in order to be stamped, so the comparison costs a subtraction and no scan: nothing is downloaded for it, and only boundaries somebody actually watched are counted. Each session ends with one line naming the container, how many boundaries were examined, how many disagreed and by how much — so silence can be told from nobody having watched, which the per-boundary warning alone could not do.
7
+ - **Fix**: The repair above is bounded, and cannot become the request-steering this proxy removed in 2.9.100. A player that cannot get what it wants scans the playlist — field log 2026-08-02, probes at #178, #681, #725, #807, #74, #245, #387 within half a second — and moving the encoder to the lowest of those would put it at the start of the film with the viewer's own requests unreachable ahead of it. What separates the two: a run placed wrongly is out by at most the buffer the player was holding, fourteen segments in the measured case, while a scan probe is out by anything at all. So only a request within sixty segments behind the head is repaired, only while an encoder is actually running (a rung the viewer switched away from stays parked), and never over a seek the viewer has stated or a target the circuit breaker has already refused.
8
+ - **Fix**: How long a segment has gone unanswered is measured against the run in force. The record was kept for the life of the session, so a timestamp left by an abandoned scan probe minutes earlier said a fresh request had already waited long enough — which would have fired the repair on the first poll, before the browser's own seek report could arrive. It is cleared with each run, which also stops the map growing all session.
9
+ - **Fix**: A copied video whose keyframe index could not be read keeps its explicit cut list. Making the list conditional on the keyframe grid dropped that case onto the `hls` muxer, which takes no cut list and writes no self-contained pieces — so nothing could read where a segment truly begins and each was stamped with a time the file does not have. That is the 4.17 s drift between speech and subtitles, which had already cost one release.
10
+ - **New**: A rung that does not cut where its grid says now says so. The check above runs on re-encoded variants too, where the meaning is different: the encoder was TOLD to put a keyframe there and did not, so a switch to that rung will not join cleanly. Hardware encoders honouring an explicit cut list is unverified — this is what will name it if one does not.
11
+
1
12
  ## 2.10.0
2
13
 
3
14
  - **New**: Quality can be changed without interrupting playback. A session that re-encodes its video now also publishes a master playlist — `GET /transcode/:id/master.m3u8` — listing every height the file can be served at, each as an ordinary HLS variant under `v/<height>/`. The player then does the switching itself: it fetches the other variant, appends it after what is already buffered, and changes the decoder's type if the codec parameters differ. Until now a change of resolution could only re-open the session, which is a cold start with the picture gone. Rewriting the media playlist underneath the player is not an alternative — ours is VOD and terminated with `#EXT-X-ENDLIST`, and hls.js re-reads only a live playlist, so anything written into it afterwards is never seen.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.10.0",
3
+ "version": "2.11.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": {
@@ -62,12 +62,16 @@ const READERS = [
62
62
  * @param {ReadRange} params.readRange
63
63
  * @param {number} params.fileSize
64
64
  * @param {string} [params.label] - For logging only.
65
- * @returns {Promise<number[] | null>} Ascending seconds, or null when this file
66
- * has no readable index the caller must then not claim to know the grid.
65
+ * @returns {Promise<{ times: number[] | null, format: string }>} Ascending
66
+ * seconds, or null times when this file has no readable index the caller
67
+ * must then not claim to know the grid. The format is which reader matched,
68
+ * reported whether or not it produced anything: how often an index disagrees
69
+ * with its own file is a question about the CONTAINER, and it cannot be
70
+ * answered by a measurement that does not say which one it came from.
67
71
  */
68
72
  export async function readKeyframeIndex({ readRange, fileSize, label = "" }) {
69
73
  if (typeof readRange !== "function" || !Number.isFinite(fileSize) || fileSize <= 0) {
70
- return null;
74
+ return { times: null, format: "unknown" };
71
75
  }
72
76
 
73
77
  const startedAt = Date.now();
@@ -76,7 +80,7 @@ export async function readKeyframeIndex({ readRange, fileSize, label = "" }) {
76
80
  try {
77
81
  const sniff = await readRange(0, Math.min(SNIFF_BYTES - 1, fileSize - 1));
78
82
  if (!sniff) {
79
- return null;
83
+ return { times: null, format: "unread" };
80
84
  }
81
85
  const reader = READERS.find((candidate) => candidate.matches(sniff));
82
86
  if (reader) {
@@ -87,7 +91,7 @@ export async function readKeyframeIndex({ readRange, fileSize, label = "" }) {
87
91
  // A malformed or partially-downloaded index must never take playback down —
88
92
  // it only means the grid is unknown, which the caller already handles.
89
93
  logger.warn(`container-index: failed to read index for "${label}": ${error?.message ?? error}`);
90
- return null;
94
+ return { times: null, format };
91
95
  }
92
96
 
93
97
  const elapsedMs = Date.now() - startedAt;
@@ -98,5 +102,5 @@ export async function readKeyframeIndex({ readRange, fileSize, label = "" }) {
98
102
  } else {
99
103
  logger.info(`container-index: no usable index for "${label}" (${format}, ${elapsedMs}ms)`);
100
104
  }
101
- return times;
105
+ return { times, format };
102
106
  }