@torrent-tv/proxy 2.9.104 → 2.9.105

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.
@@ -12,7 +12,7 @@ import path from "node:path";
12
12
  import { rmSync, statfsSync } from "node:fs";
13
13
  import WebTorrent from "webtorrent";
14
14
  import { logger } from "../utils/logger.js";
15
- import { SharedPieceStore } from "./piece-store/shared-piece-store.js";
15
+ import { SharedPieceStore, findSharedStore } from "./piece-store/shared-piece-store.js";
16
16
 
17
17
  // WebTorrent's default download root (see webtorrent lib/torrent.js: TMP =
18
18
  // path.join(os.tmpdir(), 'webtorrent')). We use the default store, so all
@@ -596,6 +596,73 @@ export class TorrentPool {
596
596
  *
597
597
  * @returns {void}
598
598
  */
599
+ /**
600
+ * Put back the reader windows WebTorrent has quietly dropped.
601
+ *
602
+ * A reader claims its window as a stream selection and releases it when it
603
+ * ends. That claim is NOT durable: `_gcSelections` deletes a selection the
604
+ * moment every piece in it is present, so a window that has been satisfied
605
+ * stops existing — and with it, everything the swarm had been asked for.
606
+ *
607
+ * While a reader keeps moving that is invisible, because the next window is
608
+ * claimed immediately. It becomes fatal when the reader STOPS: the encoder is
609
+ * held back by the look-ahead cap, ffmpeg stops reading, the reader parks on a
610
+ * window that is fully downloaded, the selection disappears — and nothing our
611
+ * code runs can notice, because the reader is parked inside a write. Measured
612
+ * 2026-08-05: the encoder was suspended at 22:44:51, the download hit zero at
613
+ * 22:45:05 and stayed there for **eleven minutes** with 150 peer connections
614
+ * open, `0 selection(s) covering 0 piece(s), 0 being asked, 0 blocks in
615
+ * flight`. When the encoder was let go there was nothing ahead of it, and the
616
+ * viewer's picture stopped.
617
+ *
618
+ * So the claim is re-asserted from outside, on this timer, using the windows
619
+ * the store already knows about — those are declared by live readers and
620
+ * withdrawn when they end, which is exactly the set that should be selected.
621
+ *
622
+ * @returns {void}
623
+ */
624
+ #reassertReaderWindows() {
625
+ for (const torrent of this.torrents.values()) {
626
+ const usage = this.fileUsageByTorrent.get(torrent);
627
+ if (!usage || usage.size === 0 || torrent?.done === true) {
628
+ continue;
629
+ }
630
+ const store = findSharedStore(torrent);
631
+ const ranges = typeof store?.protectedRanges === "function" ? store.protectedRanges() : [];
632
+ if (ranges.length === 0) {
633
+ continue;
634
+ }
635
+ const items = Array.isArray(torrent?._selections?._items) ? torrent._selections._items : [];
636
+ for (const range of ranges) {
637
+ const present = items.some((item) => item?.from === range.from && item?.to === range.to);
638
+ if (present) {
639
+ continue;
640
+ }
641
+ // Only worth re-claiming what is actually missing: re-claiming a window
642
+ // that is already complete would be deleted again on the next pass and
643
+ // the two would take turns forever.
644
+ let missing = false;
645
+ for (let index = range.from; index <= range.to && !missing; index += 1) {
646
+ if (!torrent.bitfield?.get(index)) {
647
+ missing = true;
648
+ }
649
+ }
650
+ if (!missing) {
651
+ continue;
652
+ }
653
+ try {
654
+ torrent._select(range.from, range.to, 1, null, true);
655
+ logger.info(
656
+ `torrent-pool: [${String(torrent.infoHash).slice(0, 8)}] re-claimed reader window ` +
657
+ `${range.from}-${range.to} — the selection had been dropped once it was satisfied`
658
+ );
659
+ } catch {
660
+ // Best effort: a torrent being torn down is not worth failing over.
661
+ }
662
+ }
663
+ }
664
+ }
665
+
599
666
  #reportStalledDownloads() {
600
667
  const now = Date.now();
601
668
  for (const torrent of this.torrents.values()) {
@@ -635,6 +702,7 @@ export class TorrentPool {
635
702
  this.fileUsageByTorrent,
636
703
  Date.now()
637
704
  );
705
+ this.#reassertReaderWindows();
638
706
  this.#reportStalledDownloads();
639
707
  const { bytesPerSec, reason } = decideUploadLimit(active);
640
708
  if (bytesPerSec === this.#uploadLimit) {