@torrent-tv/proxy 2.9.119 → 2.9.120
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,9 @@
|
|
|
1
|
+
## 2.9.120
|
|
2
|
+
|
|
3
|
+
- **Fix**: The rest of the file is fetched while the viewer needs nothing, instead of the link sitting idle. The background fill was re-evaluated only when a reader window MOVED, so during exactly the state it exists for — the encoder held back by the look-ahead cap, the viewer comfortably ahead, the link free — nothing was fetched at all. It is now owned by the pool's own timer rather than by the reader, because a parked reader cannot act, and parked is the whole point. Priority 0 against the window's 1, and withdrawn the moment any reader window wants something, so it can never take capacity from the picture.
|
|
4
|
+
- **Fix**: The stall warning stays quiet when a download of zero is correct. It fired all through a healthy session on 2026-08-06 — 65.3% of the file present, the encoder 134-159 s ahead, its window complete — and a warning that goes off when everything is right teaches the reader to ignore it. It now checks whether any reader window is actually missing a piece before saying anything.
|
|
5
|
+
- **New**: The progress response carries what this host takes to create a session and to produce a first segment. Both are on the playback plan too, but the browser reads that once per file: measured 2026-08-06 across four seeks, a proxy that had just restarted answered null for both, so every later seek estimated the wait with one term of four — the figure reached zero after 3.5 s of an 11.8 s wait and read "starting now" for the remaining 8.4 s. This response is polled about every 1.5 s.
|
|
6
|
+
|
|
1
7
|
## 2.9.119
|
|
2
8
|
|
|
3
9
|
- **Fix**: The progress report says the height the viewer is actually watching, not only the one an encoder is producing. It was zero whenever the video was copied — which is most sessions — so the quality menu read a bare "Auto" in exactly the case it was built to explain. Copying reports the source height, re-encoding reports the rung the proxy has settled on.
|
package/package.json
CHANGED
|
@@ -3504,6 +3504,15 @@ export class HlsSessionManager {
|
|
|
3504
3504
|
currentHeight: session.transcodeVideo
|
|
3505
3505
|
? (session.encodeHeight ?? session.sourceHeight ?? 0)
|
|
3506
3506
|
: (session.sourceHeight ?? 0),
|
|
3507
|
+
// What this host takes to create a session and to make a first segment.
|
|
3508
|
+
// Also on the playback plan, but the browser reads that once per file:
|
|
3509
|
+
// measured 2026-08-06 across four seeks, a proxy that had just restarted
|
|
3510
|
+
// answered null for both, and every later seek then computed its estimate
|
|
3511
|
+
// with one term of four — the figure hit zero after 3.5 s of an 11.8 s
|
|
3512
|
+
// wait and read "starting now" for the remaining 8.4 s. This response is
|
|
3513
|
+
// polled about every 1.5 s, so carrying them here keeps them current.
|
|
3514
|
+
expectedSessionCreateMs: this.expectedSessionCreateMs(),
|
|
3515
|
+
expectedFirstSegmentMs: this.expectedFirstSegmentMs(),
|
|
3507
3516
|
updatedAt: session.progress.updatedAt,
|
|
3508
3517
|
error: session.state === "failed" ? session.lastError : ""
|
|
3509
3518
|
};
|
package/services/torrent-pool.js
CHANGED
|
@@ -450,6 +450,14 @@ export class TorrentPool {
|
|
|
450
450
|
*/
|
|
451
451
|
#readPositionByTorrent = new Map();
|
|
452
452
|
|
|
453
|
+
/**
|
|
454
|
+
* The background-fill selection held for each torrent, so it can be withdrawn
|
|
455
|
+
* again. See #updateBackgroundFill.
|
|
456
|
+
*
|
|
457
|
+
* @type {Map<import("webtorrent").Torrent, { from: number, to: number }>}
|
|
458
|
+
*/
|
|
459
|
+
#backgroundFill = new Map();
|
|
460
|
+
|
|
453
461
|
/** When each torrent's download first fell below the stall threshold. */
|
|
454
462
|
#stallSince = new Map();
|
|
455
463
|
/** When each torrent's stall was last reported, so it is not repeated hotly. */
|
|
@@ -663,6 +671,97 @@ export class TorrentPool {
|
|
|
663
671
|
}
|
|
664
672
|
}
|
|
665
673
|
|
|
674
|
+
/**
|
|
675
|
+
* The reader windows of a torrent, and whether any of them still wants
|
|
676
|
+
* something. Two questions with one answer, because both callers below need
|
|
677
|
+
* exactly this: the background fill may only run when nothing is wanted, and
|
|
678
|
+
* a download sitting at zero is only worth warning about when something is.
|
|
679
|
+
*
|
|
680
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
681
|
+
* @returns {{ ranges: Array<{ from: number, to: number }>, missing: boolean }}
|
|
682
|
+
*/
|
|
683
|
+
#readerDemand(torrent) {
|
|
684
|
+
const store = findSharedStore(torrent);
|
|
685
|
+
const ranges = typeof store?.protectedRanges === "function" ? store.protectedRanges() : [];
|
|
686
|
+
let missing = false;
|
|
687
|
+
for (const range of ranges) {
|
|
688
|
+
for (let index = range.from; index <= range.to; index += 1) {
|
|
689
|
+
if (!torrent.bitfield?.get(index)) {
|
|
690
|
+
missing = true;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (missing) {
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return { ranges, missing };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Keep fetching the rest of the file while the viewer needs nothing.
|
|
703
|
+
*
|
|
704
|
+
* Owned here rather than by the reader, because the reader cannot act while
|
|
705
|
+
* it is parked — and parked is exactly the state this is for. The encoder is
|
|
706
|
+
* held back by the look-ahead cap, the viewer is comfortably ahead, the link
|
|
707
|
+
* is idle: that is the cheapest bandwidth of the whole session and it was
|
|
708
|
+
* going unused, because the fill was re-evaluated only when a reader window
|
|
709
|
+
* MOVED. Priority 0 against the window's 1, and withdrawn the moment any
|
|
710
|
+
* window wants something, so it can never take capacity from the picture.
|
|
711
|
+
*
|
|
712
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
713
|
+
* @param {{ ranges: Array<{ from: number, to: number }>, missing: boolean }} demand
|
|
714
|
+
* @returns {void}
|
|
715
|
+
*/
|
|
716
|
+
#updateBackgroundFill(torrent, demand) {
|
|
717
|
+
const held = this.#backgroundFill.get(torrent) ?? null;
|
|
718
|
+
const wanted = !demand.missing && demand.ranges.length > 0
|
|
719
|
+
? this.#tailAfterWindows(torrent, demand.ranges)
|
|
720
|
+
: null;
|
|
721
|
+
if (held && (!wanted || held.from !== wanted.from || held.to !== wanted.to)) {
|
|
722
|
+
try {
|
|
723
|
+
torrent._deselect?.(held.from, held.to, false);
|
|
724
|
+
} catch {
|
|
725
|
+
// Best effort.
|
|
726
|
+
}
|
|
727
|
+
this.#backgroundFill.delete(torrent);
|
|
728
|
+
}
|
|
729
|
+
if (wanted && !this.#backgroundFill.has(torrent)) {
|
|
730
|
+
try {
|
|
731
|
+
torrent._select?.(wanted.from, wanted.to, 0, null, false);
|
|
732
|
+
this.#backgroundFill.set(torrent, wanted);
|
|
733
|
+
} catch {
|
|
734
|
+
// Best effort.
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Everything after the furthest reader window, up to the end of the file it
|
|
741
|
+
* belongs to. Null when there is nothing left.
|
|
742
|
+
*
|
|
743
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
744
|
+
* @param {Array<{ from: number, to: number }>} ranges
|
|
745
|
+
* @returns {{ from: number, to: number } | null}
|
|
746
|
+
*/
|
|
747
|
+
#tailAfterWindows(torrent, ranges) {
|
|
748
|
+
const pieceLength = Number(torrent.pieceLength);
|
|
749
|
+
if (!Number.isFinite(pieceLength) || pieceLength <= 0) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
const usage = this.fileUsageByTorrent.get(torrent);
|
|
753
|
+
let lastPiece = -1;
|
|
754
|
+
for (const [fileIndex, count] of usage ?? []) {
|
|
755
|
+
const file = count > 0 ? torrent.files?.[fileIndex] : null;
|
|
756
|
+
if (!file) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
lastPiece = Math.max(lastPiece, Math.floor((file.offset + file.length - 1) / pieceLength));
|
|
760
|
+
}
|
|
761
|
+
const from = Math.max(...ranges.map((range) => range.to)) + 1;
|
|
762
|
+
return lastPiece >= from ? { from, to: lastPiece } : null;
|
|
763
|
+
}
|
|
764
|
+
|
|
666
765
|
#reportStalledDownloads() {
|
|
667
766
|
const now = Date.now();
|
|
668
767
|
for (const torrent of this.torrents.values()) {
|
|
@@ -675,6 +774,16 @@ export class TorrentPool {
|
|
|
675
774
|
this.#stallSince.delete(torrent);
|
|
676
775
|
continue;
|
|
677
776
|
}
|
|
777
|
+
// Nothing is being downloaded because nothing is wanted: every reader has
|
|
778
|
+
// all the pieces of its window. That is the encoder being held back, not
|
|
779
|
+
// a fault, and it warned all through a healthy session on 2026-08-06 —
|
|
780
|
+
// 65.3% of the file present, the encoder 134-159 s ahead, its window
|
|
781
|
+
// complete. A warning that fires when everything is right teaches the
|
|
782
|
+
// reader to ignore it.
|
|
783
|
+
if (!this.#readerDemand(torrent).missing) {
|
|
784
|
+
this.#stallSince.delete(torrent);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
678
787
|
const since = this.#stallSince.get(torrent) ?? now;
|
|
679
788
|
this.#stallSince.set(torrent, since);
|
|
680
789
|
if (now - since < STALL_REPORT_AFTER_MS) {
|
|
@@ -703,6 +812,13 @@ export class TorrentPool {
|
|
|
703
812
|
Date.now()
|
|
704
813
|
);
|
|
705
814
|
this.#reassertReaderWindows();
|
|
815
|
+
for (const torrent of this.torrents.values()) {
|
|
816
|
+
const usage = this.fileUsageByTorrent.get(torrent);
|
|
817
|
+
if (!usage || usage.size === 0 || torrent?.done === true) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
this.#updateBackgroundFill(torrent, this.#readerDemand(torrent));
|
|
821
|
+
}
|
|
706
822
|
this.#reportStalledDownloads();
|
|
707
823
|
const { bytesPerSec, reason } = decideUploadLimit(active);
|
|
708
824
|
if (bytesPerSec === this.#uploadLimit) {
|
|
@@ -322,90 +322,18 @@ export async function* readFragments({
|
|
|
322
322
|
* @type {(() => void) | null}
|
|
323
323
|
*/
|
|
324
324
|
let releaseHeldPin = null;
|
|
325
|
-
/**
|
|
326
|
-
* The rest of the file, claimed at the lowest priority so it is fetched with
|
|
327
|
-
* whatever capacity the near window does not need. Null whenever it must not
|
|
328
|
-
* be fetched at all — see {@link updateBackfill}.
|
|
329
|
-
*
|
|
330
|
-
* @type {{ from: number, to: number } | null}
|
|
331
|
-
*/
|
|
332
|
-
let backfill = null;
|
|
333
325
|
|
|
334
326
|
// Identity of this read, so the store can tell one reader's window from
|
|
335
327
|
// another's. Each read gets its own; `readerSequence` never repeats within a
|
|
336
328
|
// process.
|
|
337
329
|
const readerId = `read-${(readerSequence += 1)}`;
|
|
338
330
|
|
|
339
|
-
/**
|
|
340
|
-
* Whether every piece of the current window is already downloaded.
|
|
341
|
-
*
|
|
342
|
-
* A handful of pieces, checked against the bitfield, so this is cheap enough
|
|
343
|
-
* to re-run on every window move.
|
|
344
|
-
*
|
|
345
|
-
* @returns {boolean}
|
|
346
|
-
*/
|
|
347
|
-
const windowIsComplete = () => {
|
|
348
|
-
if (!window) {
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
for (let index = window.from; index <= window.to; index += 1) {
|
|
352
|
-
if (!torrent.bitfield?.get(index)) {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return true;
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Keep the rest of the file downloading in the background, but ONLY while
|
|
361
|
-
* that cannot cost the viewer anything.
|
|
362
|
-
*
|
|
363
|
-
* The rule is deliberately blunt rather than clever: the tail is in the
|
|
364
|
-
* download set only when every piece of the near window is already on hand.
|
|
365
|
-
* The moment one is missing — the window slid onto undownloaded content, or a
|
|
366
|
-
* seek moved it somewhere new — the tail leaves the set, so the swarm has
|
|
367
|
-
* nothing else to work on. Relying on WebTorrent's priority ordering alone
|
|
368
|
-
* would be weaker: it decides which selection a wire is offered FIRST, not
|
|
369
|
-
* what a wire already has outstanding, so a seek would still queue behind
|
|
370
|
-
* whatever tail blocks were in flight.
|
|
371
|
-
*
|
|
372
|
-
* Priority 0 against the window's 1 is kept as well, for the moments between
|
|
373
|
-
* one evaluation and the next.
|
|
374
|
-
*
|
|
375
|
-
* What this buys: a file watched for a while ends up downloaded, and every
|
|
376
|
-
* later seek into it is instant. What it costs: the pool owner's bandwidth
|
|
377
|
-
* and disk for a film the viewer may abandon — which is why it never runs
|
|
378
|
-
* ahead of the viewer's own needs.
|
|
379
|
-
*
|
|
380
|
-
* @returns {void}
|
|
381
|
-
*/
|
|
382
|
-
const updateBackfill = () => {
|
|
383
|
-
const wanted = window && window.to < lastPiece && windowIsComplete()
|
|
384
|
-
? { from: window.to + 1, to: lastPiece }
|
|
385
|
-
: null;
|
|
386
|
-
if (backfill && (!wanted || backfill.from !== wanted.from || backfill.to !== wanted.to)) {
|
|
387
|
-
releaseWindow(torrent, backfill);
|
|
388
|
-
backfill = null;
|
|
389
|
-
}
|
|
390
|
-
if (wanted && !backfill) {
|
|
391
|
-
claimWindow(torrent, wanted, 0);
|
|
392
|
-
backfill = wanted;
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
|
|
396
331
|
const moveWindowTo = (pieceIndex) => {
|
|
397
332
|
const next = readWindowFor({ pieceIndex, lastPiece, windowPieces });
|
|
398
333
|
if (window && window.from === next.from && window.to === next.to) {
|
|
399
|
-
updateBackfill();
|
|
400
334
|
return;
|
|
401
335
|
}
|
|
402
336
|
const isJump = !window || next.from > window.to || next.from < window.from;
|
|
403
|
-
// Whatever was being fetched for later stops being wanted the instant the
|
|
404
|
-
// window moves: the new position has to have the whole swarm to itself.
|
|
405
|
-
if (backfill) {
|
|
406
|
-
releaseWindow(torrent, backfill);
|
|
407
|
-
backfill = null;
|
|
408
|
-
}
|
|
409
337
|
if (window) {
|
|
410
338
|
releaseWindow(torrent, window);
|
|
411
339
|
}
|
|
@@ -429,9 +357,6 @@ export async function* readFragments({
|
|
|
429
357
|
);
|
|
430
358
|
}
|
|
431
359
|
}
|
|
432
|
-
// Only now, with the new window claimed and marked, may anything else be
|
|
433
|
-
// asked for — and only if the window needs nothing.
|
|
434
|
-
updateBackfill();
|
|
435
360
|
};
|
|
436
361
|
|
|
437
362
|
try {
|
|
@@ -555,9 +480,6 @@ export async function* readFragments({
|
|
|
555
480
|
// Reached on completion, on cancellation, on a throw, and when the consumer
|
|
556
481
|
// stops iterating — a window left behind would keep the swarm fetching for
|
|
557
482
|
// a reader that no longer exists.
|
|
558
|
-
if (backfill) {
|
|
559
|
-
releaseWindow(torrent, backfill);
|
|
560
|
-
}
|
|
561
483
|
if (window) {
|
|
562
484
|
releaseWindow(torrent, window);
|
|
563
485
|
}
|