@torrent-tv/proxy 2.105.9 → 2.105.11
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/package.json
CHANGED
|
@@ -109,11 +109,12 @@ export async function admitInput({ sources, reserve, readRanges }) {
|
|
|
109
109
|
* the download a second time through it (Home Assistant 2026-10-12,
|
|
110
110
|
* torrent-tv/meta#178).
|
|
111
111
|
*/
|
|
112
|
-
export async function writeAdmittedInput(input, stdin, { next = null, waiting = null } = {}) {
|
|
112
|
+
export async function writeAdmittedInput(input, stdin, { next = null, waiting = null, onFirstInputBytes = null } = {}) {
|
|
113
113
|
if (input?.kind !== "result") throw new TypeError("Only complete input may be written to an encoder.");
|
|
114
114
|
const originSeconds = input.originSeconds;
|
|
115
115
|
const declarations = JSON.stringify(input.tracks.map(input => matroskaTrackIdentity(input.track)));
|
|
116
116
|
const seen = new Map();
|
|
117
|
+
let firstInputBytesNoted = false;
|
|
117
118
|
let current = input, includeHeader = true;
|
|
118
119
|
try {
|
|
119
120
|
while (current) {
|
|
@@ -132,6 +133,10 @@ export async function writeAdmittedInput(input, stdin, { next = null, waiting =
|
|
|
132
133
|
batch = Buffer.allocUnsafe(INPUT_WRITE_BATCH_BYTES);
|
|
133
134
|
used = 0;
|
|
134
135
|
await writeChunk(stdin, chunk);
|
|
136
|
+
if (!firstInputBytesNoted) {
|
|
137
|
+
firstInputBytesNoted = true;
|
|
138
|
+
onFirstInputBytes?.(chunk.length);
|
|
139
|
+
}
|
|
135
140
|
};
|
|
136
141
|
for await (const chunk of current.stream({ originSeconds, includeHeader, seen })) {
|
|
137
142
|
if (!Buffer.isBuffer(chunk)) throw new TypeError("Admitted input stream yielded a non-buffer chunk.");
|
|
@@ -1341,11 +1341,14 @@ export class EncodeRuns {
|
|
|
1341
1341
|
// begun at once (`next-packet-input.js`).
|
|
1342
1342
|
const following = nextPacketInputs({ take: index => this.#host.encodeInputs.take(session, index, index),
|
|
1343
1343
|
canAppend, first: nextIndex, last: ordered?.to ?? startIndex });
|
|
1344
|
-
void writeAdmittedInput(admittedInput, run.process.stdin, {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1344
|
+
void writeAdmittedInput(admittedInput, run.process.stdin, {
|
|
1345
|
+
onFirstInputBytes: bytes => this.noteFirstInputBytes(inputToken, session.file.fileIndex, bytes),
|
|
1346
|
+
next: async () => {
|
|
1347
|
+
const next = following.next(index => { run.to = index; });
|
|
1348
|
+
if (next) run.admittedInputKeys.set(run.to, this.#inputFailures.key(next, inputParameters));
|
|
1349
|
+
return next;
|
|
1350
|
+
}
|
|
1351
|
+
}).catch(error => {
|
|
1349
1352
|
this.#host.logger.warn(`encode input output=${session.outputKey} segment=${safeIndex} write failed: ${error.message}`);
|
|
1350
1353
|
run.process?.stdin?.destroy();
|
|
1351
1354
|
}).finally(() => following.release());
|
|
@@ -483,7 +483,8 @@ export function decideUploadLimit(activeTorrents, opts = {}) {
|
|
|
483
483
|
*
|
|
484
484
|
* `paused` is the library's own word for this and the path it already checks,
|
|
485
485
|
* so nothing here fights it. The pause alone does not close what is already
|
|
486
|
-
* open, so the peers are let go by hand; the data stays exactly where it is
|
|
486
|
+
* open, so the peers are let go by hand; the data stays exactly where it is,
|
|
487
|
+
* and so do the addresses dialled, for the next rejoin (`rememberSwarmAddresses`).
|
|
487
488
|
*
|
|
488
489
|
* @param {import("webtorrent").Torrent} torrent
|
|
489
490
|
* @returns {number} Connections let go.
|
|
@@ -494,6 +495,8 @@ export function leaveSwarm(torrent) {
|
|
|
494
495
|
}
|
|
495
496
|
torrent.pause();
|
|
496
497
|
const open = Array.isArray(torrent.wires) ? torrent.wires.length : 0;
|
|
498
|
+
const remembered = rememberSwarmAddresses(torrent);
|
|
499
|
+
const unanswered = describeUnansweredRejoin(torrent);
|
|
497
500
|
let closed = 0;
|
|
498
501
|
for (const peer of [...(torrent._peers?.values?.() ?? [])]) {
|
|
499
502
|
try {
|
|
@@ -505,11 +508,150 @@ export function leaveSwarm(torrent) {
|
|
|
505
508
|
}
|
|
506
509
|
logger.info(
|
|
507
510
|
`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] left the swarm — its last reader went, ` +
|
|
508
|
-
`${open} connection(s) open, ${closed} let go
|
|
511
|
+
`${open} connection(s) open, ${closed} let go, ${remembered} address(es) kept for the next reader` +
|
|
512
|
+
`${unanswered}; the data stays and the next reader rejoins`
|
|
509
513
|
);
|
|
510
514
|
return closed;
|
|
511
515
|
}
|
|
512
516
|
|
|
517
|
+
/**
|
|
518
|
+
* WebTorrent's own `maxConns` when the client states none (webtorrent 2.8.5,
|
|
519
|
+
* index.js). More remembered addresses than this could not be dialled at once.
|
|
520
|
+
*/
|
|
521
|
+
const WEBTORRENT_DEFAULT_MAX_CONNS = 55;
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The addresses a torrent dialled in its swarm, kept for every later rejoin.
|
|
525
|
+
*
|
|
526
|
+
* `leaveSwarm` destroys every peer, and a destroyed peer leaves the torrent's
|
|
527
|
+
* `_peers` and its dial queue with it, so a torrent that rejoined had nobody to
|
|
528
|
+
* dial: `resume()` only drains that empty queue, and nothing announces again
|
|
529
|
+
* until the periodic discovery round — fifteen minutes by default, longer when
|
|
530
|
+
* a tracker says so. Home Assistant 2026-10-23, torrent 507d1fa9: it rejoined
|
|
531
|
+
* with "0 connected of 0 known", the page got no segment for 73 s, and every
|
|
532
|
+
* later opening of the same film started with no peer (torrent-tv/meta#238).
|
|
533
|
+
*
|
|
534
|
+
* KEPT FOR THE LIFE OF THE TORRENT OBJECT and refreshed at each departure, not
|
|
535
|
+
* consumed by a rejoin: `addPeer` accepts a candidate, it does not connect one,
|
|
536
|
+
* and a torrent that leaves again before a candidate answered would otherwise
|
|
537
|
+
* take an empty snapshot and lose them all.
|
|
538
|
+
*
|
|
539
|
+
* @type {WeakMap<object, string[]>}
|
|
540
|
+
*/
|
|
541
|
+
const swarmAddresses = new WeakMap();
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Torrents with a DHT lookup started by a rejoin and not yet answered: a
|
|
545
|
+
* second rejoin meanwhile does not start another.
|
|
546
|
+
*
|
|
547
|
+
* @type {WeakSet<object>}
|
|
548
|
+
*/
|
|
549
|
+
const swarmLookups = new WeakSet();
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* When each torrent last rejoined, on the monotonic clock, and whether a peer
|
|
553
|
+
* and data have come since — what tells a restored swarm from a rejoin that
|
|
554
|
+
* dialled addresses nobody answers at any more.
|
|
555
|
+
*
|
|
556
|
+
* @type {WeakMap<object, { at: number, firstPeerSeconds: number | null, firstDataSeconds: number | null }>}
|
|
557
|
+
*/
|
|
558
|
+
const swarmRejoins = new WeakMap();
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Refresh the addresses kept for a torrent from the peers it has now, before
|
|
562
|
+
* they are let go. Outgoing peers only: an incoming peer's address carries the
|
|
563
|
+
* port its side connected FROM, which nobody listens on. Connected ones first,
|
|
564
|
+
* then the candidates still queued or connecting, then what was kept before;
|
|
565
|
+
* no address twice, and no more than the client may dial at once. A departure
|
|
566
|
+
* that finds no outgoing peer keeps the earlier list.
|
|
567
|
+
*
|
|
568
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
569
|
+
* @returns {number} How many addresses are kept.
|
|
570
|
+
*/
|
|
571
|
+
export function rememberSwarmAddresses(torrent) {
|
|
572
|
+
const limit = Number(torrent?.client?.maxConns) > 0 ? Number(torrent.client.maxConns) : WEBTORRENT_DEFAULT_MAX_CONNS;
|
|
573
|
+
const outgoing = [...(torrent?._peers?.values?.() ?? [])]
|
|
574
|
+
.filter((peer) => (peer?.type === "tcpOutgoing" || peer?.type === "utpOutgoing") &&
|
|
575
|
+
typeof peer.addr === "string" && peer.addr.length > 0);
|
|
576
|
+
const kept = [];
|
|
577
|
+
const seen = new Set();
|
|
578
|
+
for (const addr of [
|
|
579
|
+
...outgoing.filter((peer) => peer.wire).map((peer) => peer.addr),
|
|
580
|
+
...outgoing.filter((peer) => !peer.wire).map((peer) => peer.addr),
|
|
581
|
+
...(swarmAddresses.get(torrent) ?? [])
|
|
582
|
+
]) {
|
|
583
|
+
if (kept.length >= limit) break;
|
|
584
|
+
if (seen.has(addr)) continue;
|
|
585
|
+
seen.add(addr);
|
|
586
|
+
kept.push(addr);
|
|
587
|
+
}
|
|
588
|
+
if (kept.length > 0) swarmAddresses.set(torrent, kept);
|
|
589
|
+
return kept.length;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Said when a torrent leaves its swarm again: what the previous rejoin got.
|
|
594
|
+
*
|
|
595
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
596
|
+
* @returns {string}
|
|
597
|
+
*/
|
|
598
|
+
function describeUnansweredRejoin(torrent) {
|
|
599
|
+
const rejoin = swarmRejoins.get(torrent);
|
|
600
|
+
if (!rejoin || rejoin.firstDataSeconds !== null) return "";
|
|
601
|
+
const seconds = ((performance.now() - rejoin.at) / 1000).toFixed(1);
|
|
602
|
+
return rejoin.firstPeerSeconds === null
|
|
603
|
+
? ` (no peer and no data in the ${seconds}s since it rejoined)`
|
|
604
|
+
: ` (a peer after ${rejoin.firstPeerSeconds.toFixed(1)}s, no data in the ${seconds}s since it rejoined)`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Ask the DHT for the torrent's peers again, unless a lookup a rejoin started
|
|
609
|
+
* is still running. A private torrent has no DHT (WebTorrent passes `dht:
|
|
610
|
+
* false` to its discovery), and the kept addresses are then all it has.
|
|
611
|
+
*
|
|
612
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
613
|
+
* @param {() => string} label
|
|
614
|
+
* @returns {string} What was done, for the rejoin line.
|
|
615
|
+
*/
|
|
616
|
+
function lookUpSwarm(torrent, label) {
|
|
617
|
+
const dht = torrent.discovery?.dht;
|
|
618
|
+
if (!dht || typeof dht.lookup !== "function" || typeof torrent.infoHash !== "string") return "no DHT";
|
|
619
|
+
if (swarmLookups.has(torrent)) return "DHT lookup already running";
|
|
620
|
+
swarmLookups.add(torrent);
|
|
621
|
+
try {
|
|
622
|
+
// Found peers arrive through the discovery's own "peer" listener, which
|
|
623
|
+
// hands them to `addPeer`. The callback only ends the lookup.
|
|
624
|
+
dht.lookup(torrent.infoHash, (error) => {
|
|
625
|
+
swarmLookups.delete(torrent);
|
|
626
|
+
if (error) logger.warn(`torrent-pool: [${label()}] DHT lookup after rejoin failed: ${error?.message ?? error}`);
|
|
627
|
+
});
|
|
628
|
+
} catch (error) {
|
|
629
|
+
swarmLookups.delete(torrent);
|
|
630
|
+
logger.warn(`torrent-pool: [${label()}] DHT lookup after rejoin failed: ${error?.message ?? error}`);
|
|
631
|
+
return "DHT lookup failed";
|
|
632
|
+
}
|
|
633
|
+
return "DHT lookup started";
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Note the first peer, or the first downloaded bytes, since the torrent's last
|
|
638
|
+
* rejoin, once each.
|
|
639
|
+
*
|
|
640
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
641
|
+
* @param {"peer" | "data"} kind
|
|
642
|
+
* @returns {boolean} Whether this was the first of its kind since the rejoin.
|
|
643
|
+
*/
|
|
644
|
+
export function noteAfterRejoin(torrent, kind) {
|
|
645
|
+
const rejoin = swarmRejoins.get(torrent);
|
|
646
|
+
if (!rejoin) return false;
|
|
647
|
+
const field = kind === "peer" ? "firstPeerSeconds" : "firstDataSeconds";
|
|
648
|
+
if (rejoin[field] !== null) return false;
|
|
649
|
+
rejoin[field] = (performance.now() - rejoin.at) / 1000;
|
|
650
|
+
logger.info(`torrent-pool: [${String(torrent.infoHash ?? "?").slice(0, 8)}] after rejoin: first ` +
|
|
651
|
+
`${kind} after ${rejoin[field].toFixed(1)}s`);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
513
655
|
/**
|
|
514
656
|
* Take the swarm back for a torrent a reader has just arrived at.
|
|
515
657
|
*
|
|
@@ -517,6 +659,12 @@ export function leaveSwarm(torrent) {
|
|
|
517
659
|
* back. Both are acted on at the moment they happen, and neither is discovered
|
|
518
660
|
* by asking.
|
|
519
661
|
*
|
|
662
|
+
* `resume()` in WebTorrent 2.8.5 only clears `paused` and dials what is in the
|
|
663
|
+
* torrent's queue, which `leaveSwarm` emptied; it announces nothing. So the
|
|
664
|
+
* addresses kept at the departure are handed back, and the DHT is asked again.
|
|
665
|
+
* The trackers stay on their own schedule: each sets its own interval, and an
|
|
666
|
+
* announce at every rejoin would not respect it.
|
|
667
|
+
*
|
|
520
668
|
* @param {import("webtorrent").Torrent} torrent
|
|
521
669
|
* @returns {boolean} Whether it had to be taken back.
|
|
522
670
|
*/
|
|
@@ -525,15 +673,33 @@ export function rejoinSwarm(torrent) {
|
|
|
525
673
|
return false;
|
|
526
674
|
}
|
|
527
675
|
torrent.resume();
|
|
528
|
-
|
|
529
|
-
// explicitly closed by leaveSwarm().
|
|
676
|
+
const label = () => String(torrent.infoHash ?? "?").slice(0, 8);
|
|
677
|
+
// resume() does not recreate HTTP seeds explicitly closed by leaveSwarm().
|
|
678
|
+
// Their URLs remain in the metadata.
|
|
530
679
|
if (torrent.client?.enableWebSeeds !== false && typeof torrent.addWebSeed === "function") {
|
|
531
680
|
for (const url of torrent.urlList ?? []) {
|
|
532
681
|
if (!torrent._peers?.has(url)) torrent.addWebSeed(url);
|
|
533
682
|
}
|
|
534
683
|
}
|
|
684
|
+
swarmRejoins.set(torrent, { at: performance.now(), firstPeerSeconds: null, firstDataSeconds: null });
|
|
685
|
+
// A torrent held whole has nothing to fetch from anybody: dialling its swarm
|
|
686
|
+
// again would only bring back the connections leaving it was for.
|
|
687
|
+
const whole = torrent.progress === 1;
|
|
688
|
+
const addresses = whole ? [] : swarmAddresses.get(torrent) ?? [];
|
|
689
|
+
let handed = 0;
|
|
690
|
+
if (typeof torrent.addPeer === "function") {
|
|
691
|
+
for (const addr of addresses) {
|
|
692
|
+
try {
|
|
693
|
+
if (torrent.addPeer(addr, "rejoin")) handed += 1;
|
|
694
|
+
} catch {
|
|
695
|
+
// An address the library refuses now: the others are still worth dialling.
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const lookup = whole ? "held whole, no DHT lookup" : lookUpSwarm(torrent, label);
|
|
535
700
|
logger.info(
|
|
536
|
-
`torrent-pool: [${
|
|
701
|
+
`torrent-pool: [${label()}] rejoined the swarm — a reader arrived; ` +
|
|
702
|
+
`${handed} of ${addresses.length} kept address(es) handed back, ${lookup}`
|
|
537
703
|
);
|
|
538
704
|
return true;
|
|
539
705
|
}
|
|
@@ -1712,6 +1878,11 @@ export class TorrentPool {
|
|
|
1712
1878
|
);
|
|
1713
1879
|
});
|
|
1714
1880
|
|
|
1881
|
+
// What a rejoin restored: its first connection and its first downloaded
|
|
1882
|
+
// bytes, once each per rejoin (torrent-tv/meta#238).
|
|
1883
|
+
torrent.on("wire", () => noteAfterRejoin(torrent, "peer"));
|
|
1884
|
+
torrent.on("download", () => noteAfterRejoin(torrent, "data"));
|
|
1885
|
+
|
|
1715
1886
|
torrent.on("warning", (warning) => {
|
|
1716
1887
|
logger.warn(`torrent-pool: [${label()}] warning: ${formatWarning(warning)}`);
|
|
1717
1888
|
});
|