@torrent-tv/proxy 2.55.2 → 2.55.3

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,4 +1,6 @@
1
- ## 2.55.2
1
+ ## 2.55.3
2
+
3
+ - **Fix**: A track's subtitle cues are now walked ahead of being asked for, instead of only when a browser first requests them. `cuesHeldFor` only ever read clusters on demand, inside the `/api/subtitles` request itself — cheap once caught up, but the FIRST call for a track had to walk the whole backlog of already-downloaded-but-unparsed clusters serially, with no `pending`/streaming pattern the way the ffmpeg fallback has one. On a film well into playback that backlog is not small (`Minions.and.Monsters.1080p.mkv` indexes over a thousand cluster positions per track), so a viewer who turned subtitles on after watching for a while waited on that catch-up instead of seeing cues appear at once — the opposite of the rule this file states its own reason for existing ("the region the viewer is watching is downloaded before they reach it, so its cues are ready before they are needed"): true of the DATA, not of when it got READ. A new periodic pass in the torrent worker (`warmSubtitleCues`, every 3 s, one per actively-read file) walks new clusters as they arrive, reusing `cuesHeldFor`'s own memoized state — a file nobody has opened costs nothing, and a file being watched is caught up by the time a track is switched on.
2
4
 
3
5
  - **Fix**: The decode pipe's sanity log carried a fixed editorial line — "a reading where these are far apart is worth a second look" — printed on every single reading regardless of whether the two figures actually were, which was noise the first time it ran in the field. The comparison is also now taken over the same window the speed itself is (bytes are snapshotted alongside each progress sample), rather than over the whole run from process start, which read systematically low for no reason but that mismatch. The line only says "worth a second look" when the two figures are actually more than 1.5x apart.
4
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.55.2",
3
+ "version": "2.55.3",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -316,6 +316,29 @@ export async function cuesHeldFor(torrent, fileIndex, sourceKey, trackNumber) {
316
316
 
317
317
  }
318
318
 
319
+ /**
320
+ * Walk whatever clusters have newly arrived, for every text track a file
321
+ * carries — so the browser's first request for a track finds its cues already
322
+ * caught up instead of paying for the whole backlog inside that request.
323
+ *
324
+ * `cuesHeldFor` already skips positions it has walked before (`state.walked`),
325
+ * so calling this on a timer is cheap once a file is caught up: the only cost
326
+ * is deciding there is nothing new to read. It is `getSubtitleCues` run ahead
327
+ * of being asked, on the same state that call itself would build — nothing is
328
+ * duplicated, and a browser that never asks costs nothing beyond this.
329
+ *
330
+ * @param {object} torrent
331
+ * @param {number} fileIndex
332
+ * @param {string} sourceKey
333
+ * @returns {Promise<void>}
334
+ */
335
+ export async function warmSubtitleCues(torrent, fileIndex, sourceKey) {
336
+ const plan = await planFor(torrent, fileIndex, `${sourceKey}:${fileIndex}`);
337
+ for (const track of plan?.tracks ?? []) {
338
+ await cuesHeldFor(torrent, fileIndex, sourceKey, track.trackNumber);
339
+ }
340
+ }
341
+
319
342
  /**
320
343
  * The text subtitle tracks of a file, for the menu the viewer sees.
321
344
  *
@@ -27,7 +27,7 @@ import { parentPort, workerData } from "node:worker_threads";
27
27
  import { createSendStream } from "./channel.js";
28
28
  import { createFileClaims } from "./file-claims.js";
29
29
  import { readFragments, supplyFiguresFor } from "./piece-reader.js";
30
- import { cuesHeldFor, declaredSubtitleTracksOf, subtitleTracksOf } from "./subtitle-cues.js";
30
+ import { cuesHeldFor, declaredSubtitleTracksOf, subtitleTracksOf, warmSubtitleCues } from "./subtitle-cues.js";
31
31
  import { Command, Event } from "./protocol.js";
32
32
 
33
33
  // Imported dynamically, and that is load-bearing: static imports are RESOLVED
@@ -507,4 +507,26 @@ setInterval(() => {
507
507
  }
508
508
  }, STORE_REPORT_INTERVAL_MS).unref();
509
509
 
510
+ /**
511
+ * How often an actively-read file's subtitle cues are walked ahead of being
512
+ * asked for. Cheap once caught up (`warmSubtitleCues` skips clusters it has
513
+ * already read), so this can run often; every read.js pass is one it does not
514
+ * have to do.
515
+ */
516
+ const SUBTITLE_WARMUP_INTERVAL_MS = 3_000;
517
+
518
+ setInterval(() => {
519
+ for (const [sourceKey, torrent] of pool.torrents) {
520
+ const usage = pool.fileUsageByTorrent.get(torrent);
521
+ if (!usage) {
522
+ continue;
523
+ }
524
+ for (const fileIndex of usage.keys()) {
525
+ warmSubtitleCues(torrent, fileIndex, sourceKey).catch((error) => {
526
+ log(`subtitle warmup ${sourceKey}:${fileIndex} failed: ${error instanceof Error ? error.message : error}`);
527
+ });
528
+ }
529
+ }
530
+ }, SUBTITLE_WARMUP_INTERVAL_MS).unref();
531
+
510
532
  log("torrent worker started");