@torrent-tv/proxy 2.9.93 → 2.9.95
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 +10 -0
- package/package.json +1 -1
- package/routes/stream/get.js +62 -2
- package/services/hls-session-manager.js +50 -0
- package/services/piece-store/piece-lru.js +71 -0
- package/services/piece-store/shared-piece-store.js +57 -0
- package/services/playback-planner.js +17 -0
- package/services/torrent-worker/client.js +448 -447
- package/services/torrent-worker/piece-reader.js +124 -22
- package/services/torrent-worker/worker.js +412 -410
- package/test/piece-lru.test.js +64 -0
- package/test/read-window.test.js +9 -5
|
@@ -26,6 +26,9 @@ import { logger } from "../../utils/logger.js";
|
|
|
26
26
|
/** Only waits at least this long are reported; sequential reading stays silent. */
|
|
27
27
|
const PIECE_WAIT_LOG_MS = 1_000;
|
|
28
28
|
|
|
29
|
+
/** Distinguishes concurrent readers to the piece store. Never reused. */
|
|
30
|
+
let readerSequence = 0;
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* How far ahead of the read head pieces are asked for.
|
|
31
34
|
*
|
|
@@ -46,22 +49,6 @@ const PIECE_WAIT_LOG_MS = 1_000;
|
|
|
46
49
|
*/
|
|
47
50
|
const READ_WINDOW_BYTES = 32 * 1024 * 1024;
|
|
48
51
|
|
|
49
|
-
/**
|
|
50
|
-
* How many pieces past the one being waited for are marked critical.
|
|
51
|
-
*
|
|
52
|
-
* `critical` means "a reader is blocked on this now" — it is what lets a piece
|
|
53
|
-
* jump the sequential scan. Marking a whole range critical, as this did, says
|
|
54
|
-
* it about hundreds of pieces at once and the signal stops meaning anything.
|
|
55
|
-
* WebTorrent's own reader marks `min(1 MB / pieceLength, 2)` pieces, i.e. the
|
|
56
|
-
* one under the head and at most two more; the same rule is used here.
|
|
57
|
-
*
|
|
58
|
-
* @param {number} pieceLength
|
|
59
|
-
* @returns {number}
|
|
60
|
-
*/
|
|
61
|
-
function criticalRunLength(pieceLength) {
|
|
62
|
-
return Math.min(Math.floor((1024 * 1024) / Math.max(1, pieceLength)), 2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
52
|
/**
|
|
66
53
|
* The pieces a reader at `pieceIndex` wants next, clamped to its own range.
|
|
67
54
|
*
|
|
@@ -84,14 +71,16 @@ export function readWindowFor({ pieceIndex, lastPiece, windowPieces }) {
|
|
|
84
71
|
*
|
|
85
72
|
* @param {import("webtorrent").Torrent} torrent
|
|
86
73
|
* @param {{ from: number, to: number }} window
|
|
74
|
+
* @param {number} [priority] - 1 for what a reader needs next, 0 for the
|
|
75
|
+
* background fill of the rest of the file.
|
|
87
76
|
* @returns {void}
|
|
88
77
|
*/
|
|
89
|
-
function claimWindow(torrent, { from, to }) {
|
|
78
|
+
function claimWindow(torrent, { from, to }, priority = 1) {
|
|
90
79
|
try {
|
|
91
80
|
if (typeof torrent._select === "function") {
|
|
92
|
-
torrent._select(from, to,
|
|
81
|
+
torrent._select(from, to, priority, null, true);
|
|
93
82
|
} else if (typeof torrent.select === "function") {
|
|
94
|
-
torrent.select(from, to,
|
|
83
|
+
torrent.select(from, to, priority);
|
|
95
84
|
}
|
|
96
85
|
} catch {
|
|
97
86
|
// Best effort — never fail a read because selection bookkeeping refused.
|
|
@@ -287,22 +276,120 @@ export async function* readFragments({
|
|
|
287
276
|
// `bytes 0-<EOF>` left a permanent selection over the entire file, and no
|
|
288
277
|
// later prioritisation could outrank it.
|
|
289
278
|
const windowPieces = Math.max(1, Math.ceil(Math.max(1, windowBytes) / pieceLength));
|
|
290
|
-
const criticalRun = criticalRunLength(pieceLength);
|
|
291
279
|
/** @type {{ from: number, to: number } | null} */
|
|
292
280
|
let window = null;
|
|
293
281
|
/** @type {{ from: number, to: number } | null} */
|
|
294
282
|
let criticalMark = null;
|
|
283
|
+
/**
|
|
284
|
+
* The rest of the file, claimed at the lowest priority so it is fetched with
|
|
285
|
+
* whatever capacity the near window does not need. Null whenever it must not
|
|
286
|
+
* be fetched at all — see {@link updateBackfill}.
|
|
287
|
+
*
|
|
288
|
+
* @type {{ from: number, to: number } | null}
|
|
289
|
+
*/
|
|
290
|
+
let backfill = null;
|
|
291
|
+
|
|
292
|
+
// Identity of this read, so the store can tell one reader's window from
|
|
293
|
+
// another's. Each read gets its own; `readerSequence` never repeats within a
|
|
294
|
+
// process.
|
|
295
|
+
const readerId = `read-${(readerSequence += 1)}`;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Whether every piece of the current window is already downloaded.
|
|
299
|
+
*
|
|
300
|
+
* A handful of pieces, checked against the bitfield, so this is cheap enough
|
|
301
|
+
* to re-run on every window move.
|
|
302
|
+
*
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
const windowIsComplete = () => {
|
|
306
|
+
if (!window) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
for (let index = window.from; index <= window.to; index += 1) {
|
|
310
|
+
if (!torrent.bitfield?.get(index)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Keep the rest of the file downloading in the background, but ONLY while
|
|
319
|
+
* that cannot cost the viewer anything.
|
|
320
|
+
*
|
|
321
|
+
* The rule is deliberately blunt rather than clever: the tail is in the
|
|
322
|
+
* download set only when every piece of the near window is already on hand.
|
|
323
|
+
* The moment one is missing — the window slid onto undownloaded content, or a
|
|
324
|
+
* seek moved it somewhere new — the tail leaves the set, so the swarm has
|
|
325
|
+
* nothing else to work on. Relying on WebTorrent's priority ordering alone
|
|
326
|
+
* would be weaker: it decides which selection a wire is offered FIRST, not
|
|
327
|
+
* what a wire already has outstanding, so a seek would still queue behind
|
|
328
|
+
* whatever tail blocks were in flight.
|
|
329
|
+
*
|
|
330
|
+
* Priority 0 against the window's 1 is kept as well, for the moments between
|
|
331
|
+
* one evaluation and the next.
|
|
332
|
+
*
|
|
333
|
+
* What this buys: a file watched for a while ends up downloaded, and every
|
|
334
|
+
* later seek into it is instant. What it costs: the pool owner's bandwidth
|
|
335
|
+
* and disk for a film the viewer may abandon — which is why it never runs
|
|
336
|
+
* ahead of the viewer's own needs.
|
|
337
|
+
*
|
|
338
|
+
* @returns {void}
|
|
339
|
+
*/
|
|
340
|
+
const updateBackfill = () => {
|
|
341
|
+
const wanted = window && window.to < lastPiece && windowIsComplete()
|
|
342
|
+
? { from: window.to + 1, to: lastPiece }
|
|
343
|
+
: null;
|
|
344
|
+
if (backfill && (!wanted || backfill.from !== wanted.from || backfill.to !== wanted.to)) {
|
|
345
|
+
releaseWindow(torrent, backfill);
|
|
346
|
+
backfill = null;
|
|
347
|
+
}
|
|
348
|
+
if (wanted && !backfill) {
|
|
349
|
+
claimWindow(torrent, wanted, 0);
|
|
350
|
+
backfill = wanted;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
295
353
|
|
|
296
354
|
const moveWindowTo = (pieceIndex) => {
|
|
297
355
|
const next = readWindowFor({ pieceIndex, lastPiece, windowPieces });
|
|
298
356
|
if (window && window.from === next.from && window.to === next.to) {
|
|
357
|
+
updateBackfill();
|
|
299
358
|
return;
|
|
300
359
|
}
|
|
360
|
+
const isJump = !window || next.from > window.to || next.from < window.from;
|
|
361
|
+
// Whatever was being fetched for later stops being wanted the instant the
|
|
362
|
+
// window moves: the new position has to have the whole swarm to itself.
|
|
363
|
+
if (backfill) {
|
|
364
|
+
releaseWindow(torrent, backfill);
|
|
365
|
+
backfill = null;
|
|
366
|
+
}
|
|
301
367
|
if (window) {
|
|
302
368
|
releaseWindow(torrent, window);
|
|
303
369
|
}
|
|
304
370
|
claimWindow(torrent, next);
|
|
305
371
|
window = next;
|
|
372
|
+
// Tell the store these pieces are wanted, so it evicts something else.
|
|
373
|
+
// Without it the piece the decoder reads next looks exactly as stale as one
|
|
374
|
+
// the encoder fetched forty minutes ahead, and the second kind is what
|
|
375
|
+
// fills the store while the encoder runs ahead of the viewer.
|
|
376
|
+
store.protectRange?.(readerId, next.from, next.to);
|
|
377
|
+
if (isJump) {
|
|
378
|
+
// A jump — a seek, not the window sliding along — can land on pieces that
|
|
379
|
+
// are already downloaded but have been spilled to disk. Bring the whole
|
|
380
|
+
// window back at once instead of one disk round trip per piece as the
|
|
381
|
+
// reader reaches them.
|
|
382
|
+
const revived = store.warmRange?.(next.from, next.to) ?? 0;
|
|
383
|
+
if (revived > 0) {
|
|
384
|
+
logger.info(
|
|
385
|
+
`piece-reader: reviving ${revived} spilled piece(s) of ${next.from}-${next.to} ` +
|
|
386
|
+
`for a jump to ${(start / 1024 / 1024).toFixed(0)}MB of "${file.name}"`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Only now, with the new window claimed and marked, may anything else be
|
|
391
|
+
// asked for — and only if the window needs nothing.
|
|
392
|
+
updateBackfill();
|
|
306
393
|
};
|
|
307
394
|
|
|
308
395
|
try {
|
|
@@ -318,8 +405,19 @@ export async function* readFragments({
|
|
|
318
405
|
moveWindowTo(pieceIndex);
|
|
319
406
|
|
|
320
407
|
if (!torrent.bitfield?.get(pieceIndex)) {
|
|
321
|
-
//
|
|
322
|
-
|
|
408
|
+
// Everything from here to the end of the window is wanted NOW, so all
|
|
409
|
+
// of it is marked, not just the piece under the head. `critical`
|
|
410
|
+
// enables hotswap: a block reserved by a slow peer is re-requested from
|
|
411
|
+
// a faster one instead of holding up the reader. Measured 2026-08-04
|
|
412
|
+
// with only the blocked piece marked, the first segment after a seek
|
|
413
|
+
// took 7.2 s while its four 4 MB pieces arrived one after another at
|
|
414
|
+
// ~2.2 MB/s, with waits of 1.3 s and 2.8 s on single pieces.
|
|
415
|
+
//
|
|
416
|
+
// This is not the old behaviour returning: that marked the whole
|
|
417
|
+
// REQUESTED RANGE, which for ffmpeg's input means every piece to the
|
|
418
|
+
// end of the file — hundreds of them, at which point the flag says
|
|
419
|
+
// nothing. A window is what a reader genuinely needs next.
|
|
420
|
+
criticalMark = markCritical(torrent, pieceIndex, window.to, criticalMark);
|
|
323
421
|
}
|
|
324
422
|
|
|
325
423
|
const waitStartedAt = Date.now();
|
|
@@ -372,9 +470,13 @@ export async function* readFragments({
|
|
|
372
470
|
// Reached on completion, on cancellation, on a throw, and when the consumer
|
|
373
471
|
// stops iterating — a window left behind would keep the swarm fetching for
|
|
374
472
|
// a reader that no longer exists.
|
|
473
|
+
if (backfill) {
|
|
474
|
+
releaseWindow(torrent, backfill);
|
|
475
|
+
}
|
|
375
476
|
if (window) {
|
|
376
477
|
releaseWindow(torrent, window);
|
|
377
478
|
}
|
|
479
|
+
store.releaseProtection?.(readerId);
|
|
378
480
|
if (criticalMark) {
|
|
379
481
|
clearCritical(torrent, criticalMark);
|
|
380
482
|
}
|