@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.
package/services/torrent-pool.js
CHANGED
|
@@ -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) {
|