@torrent-tv/proxy 2.43.0 → 2.43.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,15 @@
1
+ ## 2.43.2
2
+
3
+ - **Fix**: The proxy no longer dies without a word in the middle of a film. It was a segmentation fault in the uTP native library — `on_uv_read` parsed a sender address that a FAILED read never produced, and libuv passes null there — so a read error dereferenced a null pointer on the thread that owns the torrent client. Three core dumps in two days, each about three and a half hours into an otherwise healthy run, each with the same top frame; the last one on a swarm of 63-75 peers delivering 13 MB/s, one segment after a successful 158 Mbit/s send. Fixed in our build (`@torrent-tv/utp-native@2.5.3-ttv.3`, which this now depends on) and absent from upstream master. Detail: `research/utp-native-null-addr-2026-08-20.md`.
4
+ - **Fix**: A soundtrack no longer moves the grid the picture is cut on. The boundary table is the picture's cut list — it is built from the container's keyframe index, and a copied stream can be cut nowhere else — but a produced sound segment was writing its own start into it too, and the two readings are of different things: a soundtrack has no keyframes and is cut exactly where `-segment_times` asks, to within one audio frame, while the picture's cuts are the file's real keyframes. Measured 2026-08-20 on `Minions.and.Monsters.1080p.mkv`, boundary #521 was corrected 2086.084s → 2084.082s by the picture and 2084.082s → 2086.033s by the sound 1.6 s later — **1.951 s apart**, against the 0.25 s that stops a correction and the 0.5 s a player bridges. Each reading contradicted the table the other had just written, so it never converged and the correction repeated for as long as the film ran. Only a session carrying picture may correct the grid now.
5
+ - **Chore**: The line that reports a produced segment starting away from the playlist says what it measures. On the picture that is the container's keyframe index being wrong, which is what it always claimed; on a soundtrack there is no keyframe involved at all, and what it measures is how far the grid has moved since that run was launched. The per-boundary warning and the periodic summary both name the two apart now (`keyframe-index` against `sound-vs-grid`), and the summary no longer ends a soundtrack's figures with a count of keyframes read.
6
+ - **Chore**: The per-boundary warning is limited to once per segment per five seconds, like the playlist-disagreement line beside it. A run keeps cutting on the `-segment_times` list it was launched with, so once the picture has moved the grid under a soundtrack every one of that run's segments deviates — and the same segment is produced and served again and again while a player refuses it. A line each time buries the first one, which is the one somebody is reading the log for. The soundtrack summary also keeps the tolerance its count was made against; only the count of keyframes read is dropped, since a soundtrack has none.
7
+ - **Note, so the next field session is not read as a regression**: this closes the oscillation, not the gap. A run already producing keeps cutting on the `-segment_times` list it was launched with, so after a correction its segments still begin at pre-correction times until it is restarted — only a member whose run BEGINS at the corrected boundary is moved. Making the published grid agree with where runs really begin is the separate piece of work the code has been carrying a note about since 2026-08-17.
8
+
9
+ ## 2.43.1
10
+
11
+ - **New**: A subtitle request can say where the browser's copy ends (`?after=<seconds>`) and gets back only the cues past it. A track read out of downloaded clusters grows as the film does, and the browser was being sent all of it every few seconds — 76 KB a time on the field file — for the few lines at its end. The language is still detected from every cue held rather than from the handful being sent, because three lines say much less about a language than a whole track does.
12
+
1
13
  ## 2.43.0
2
14
 
3
15
  - **New**: An MP4's text subtitles are read the same way, and more cheaply than Matroska's. Where a Matroska cue costs whatever cluster holds it — the picture around it included — an MP4 states every sample's own byte range in its sample table (ISO/IEC 14496-12 §8.6.1.2, §8.7.3-8.7.5), so a cue costs its own few dozen bytes and nothing else. The tables are read out of the `moov` the keyframe reader already fetches: `stts` for when each cue starts and how long it lasts, `stsz` for its length, `stsc` with `stco`/`co64` for where its bytes are. `tx3g` (3GPP timed text) and `wvtt` (WebVTT in MP4) are decoded; `stpp` (TTML) is XML and is deliberately left out rather than half-shown. An empty sample is the format's way of saying nothing is on screen and is not turned into a blank cue. Same rule as before: only samples whose bytes are already downloaded are read, so a cue never costs a request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.43.0",
3
+ "version": "2.43.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": {
@@ -44,6 +44,6 @@
44
44
  "@biomejs/biome": "^2.5.7"
45
45
  },
46
46
  "overrides": {
47
- "utp-native": "npm:@torrent-tv/utp-native@2.5.3-ttv.2"
47
+ "utp-native": "npm:@torrent-tv/utp-native@2.5.3-ttv.3"
48
48
  }
49
49
  }
@@ -100,7 +100,19 @@ export async function handleApiSubtitlesGet(req, reply, { sourceRegistry, torren
100
100
  // read that way. Costs no network at all and answers with the part of the
101
101
  // film they are watching; the rest arrives as they watch it. Only when the
102
102
  // container cannot be read this way does the old extraction run.
103
- const held = await cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, trackIndex);
103
+ // Where the browser's copy of this track ends. It already holds every cue
104
+ // before this second, so sending them again is bytes for nothing: measured
105
+ // 2026-08-19, one track is 76 KB and the browser asked for it every few
106
+ // seconds while the film downloaded. Absent or unparsable means "send
107
+ // everything", which is what a browser asking for the first time wants.
108
+ const after = Number.parseFloat(String(req.query?.after ?? ""));
109
+ const held = await cuesFromDownloadedClusters(
110
+ torrentPool,
111
+ torrent,
112
+ fileIndex,
113
+ trackIndex,
114
+ Number.isFinite(after) ? after : null
115
+ );
104
116
  if (held !== null) {
105
117
  setLanguageHeaders(reply, held.language);
106
118
  reply.header("content-type", "text/vtt; charset=utf-8");
@@ -252,7 +264,7 @@ function readFileFully(file, maxBytes) {
252
264
  * @returns {Promise<{ vtt: string, language: object | null, coveredClusters: number, indexedClusters: number } | null>}
253
265
  * Null when this file cannot be read this way, and then the caller falls back.
254
266
  */
255
- async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, trackIndex) {
267
+ async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, trackIndex, after = null) {
256
268
  if (typeof torrentPool?.getSubtitleTracks !== "function") {
257
269
  return null;
258
270
  }
@@ -275,7 +287,13 @@ async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, track
275
287
  if (!held || !Array.isArray(held.cues)) {
276
288
  return null;
277
289
  }
278
- const vtt = cuesToVtt(held.cues, held.codecId);
290
+ // Only what the browser does not have. The language is still detected from
291
+ // EVERYTHING held, because three new lines say much less about a language
292
+ // than the whole track does.
293
+ const fresh = Number.isFinite(after)
294
+ ? held.cues.filter((cue) => cue.startSeconds > after)
295
+ : held.cues;
296
+ const vtt = cuesToVtt(fresh, held.codecId);
279
297
  const language = held.cues.length > 0
280
298
  ? detectLanguage(held.cues.map((cue) => cue.text).join("\n"))
281
299
  : null;