@torrent-tv/proxy 2.98.16 → 2.98.18
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
|
@@ -1319,8 +1319,13 @@ export class EncodeRuns {
|
|
|
1319
1319
|
/**
|
|
1320
1320
|
* How long this run took to open its original input, for sizing the next
|
|
1321
1321
|
* stretch: from its spawn to the name of its first closed piece, less what
|
|
1322
|
-
* encoding that piece costs at
|
|
1323
|
-
*
|
|
1322
|
+
* encoding that piece costs at a measured speed — the run's own where it
|
|
1323
|
+
* closed two pieces, else the output's newest reading, which is taken
|
|
1324
|
+
* between two closed pieces of a run too. Never a predicted speed: the
|
|
1325
|
+
* output's starting figure, scaled by a machine share that missed short
|
|
1326
|
+
* runs, read 0.08x on Home Assistant and recorded opens of 0.00 s (r41,
|
|
1327
|
+
* torrent-tv/meta#178). Nothing is noted where no speed was measured or the
|
|
1328
|
+
* rest is negative, and the open estimated before stands.
|
|
1324
1329
|
*
|
|
1325
1330
|
* @param {object} session
|
|
1326
1331
|
* @param {import("./EncodeRun.js").EncodeRun} run
|
|
@@ -1330,14 +1335,21 @@ export class EncodeRuns {
|
|
|
1330
1335
|
#noteOpen(session, run, ended) {
|
|
1331
1336
|
const grid = session.timeline?.published ?? session.timeline?.boundaries;
|
|
1332
1337
|
const pieceSeconds = Number(grid?.[run.from + 1]) - Number(grid?.[run.from]);
|
|
1333
|
-
const speed = run.speedX > 0 ? run.speedX : this.#
|
|
1338
|
+
const speed = run.speedX > 0 ? run.speedX : this.#speedReadings.windowOf(session).at(-1)?.speed ?? 0;
|
|
1334
1339
|
// FFmpeg's reads may have waited for the stretch being copied; that wait
|
|
1335
1340
|
// is the copy's, and the stretch is sized from the open (`run-open.js`).
|
|
1336
1341
|
const open = runOpenOf({
|
|
1337
1342
|
firstNamedMs: ended.firstNamedMs, firstNamedWallMs: ended.firstNamedWallMs, startedAt: run.startedAt,
|
|
1338
1343
|
waitedBefore: run.originalInput?.waitedBefore, pieceSeconds, speed
|
|
1339
1344
|
});
|
|
1340
|
-
if (!open)
|
|
1345
|
+
if (!open) {
|
|
1346
|
+
if (Number.isFinite(ended.firstNamedMs) && pieceSeconds > 0) {
|
|
1347
|
+
this.#host.logger.info(`encode input of run #${run.from}..#${run.to} on ${session.outputKey}: open not measured ` +
|
|
1348
|
+
`(first piece named at ${ended.firstNamedMs}ms, ${pieceSeconds.toFixed(3)}s of film, ` +
|
|
1349
|
+
`${speed > 0 ? `at a measured ${speed.toFixed(2)}x the piece takes longer than that` : "no speed measured between two closed pieces"})`);
|
|
1350
|
+
}
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1341
1353
|
const { openSeconds, waitedMs } = open;
|
|
1342
1354
|
this.#host.encodeOrchestrator.noteOpen?.(session.outputKey ?? "", openSeconds);
|
|
1343
1355
|
this.#host.logger.info(`encode input of run #${run.from}..#${run.to} on ${session.outputKey}: opened in ` +
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
* wall clock, so they are taken up to the wall-clock moment the first piece
|
|
9
9
|
* was named — the run's start plus its time to that name, not that time alone,
|
|
10
10
|
* which as a moment lay before every wait and counted none of them.
|
|
11
|
+
*
|
|
12
|
+
* THE SPEED IS A MEASURED ONE, between two closed pieces, and a negative rest
|
|
13
|
+
* is no open at all: on Home Assistant runs of one piece took 6.0-7.5 s to it
|
|
14
|
+
* for 4 s of film, were priced at a speed of 0.08x that no run had measured,
|
|
15
|
+
* and recorded opens of 0.00 s; the next inputs were sized from them to one
|
|
16
|
+
* piece, and the runs stayed one piece each (r41, torrent-tv/meta#178). A rest
|
|
17
|
+
* below zero says the speed does not describe that piece, and nothing is
|
|
18
|
+
* recorded: the open estimated before stands.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -18,12 +26,14 @@
|
|
|
18
26
|
* @param {((until: number) => number) | null | undefined} params.waitedBefore - The input's wall-clock waits for its copy before `until`, ms.
|
|
19
27
|
* @param {number} params.pieceSeconds - The first piece's film.
|
|
20
28
|
* @param {number} params.speed - Film seconds per second; 0 when not known.
|
|
21
|
-
* @returns {{ openSeconds: number, waitedMs: number } | null} Null where any term is
|
|
29
|
+
* @returns {{ openSeconds: number, waitedMs: number } | null} Null where any term is
|
|
30
|
+
* unknown, or where the piece's encoding at that speed exceeds the time to it.
|
|
22
31
|
*/
|
|
23
32
|
export function runOpenOf({ firstNamedMs, firstNamedWallMs, startedAt, waitedBefore, pieceSeconds, speed }) {
|
|
24
33
|
if (!Number.isFinite(firstNamedMs) || !(pieceSeconds > 0) || !(speed > 0)) return null;
|
|
25
34
|
const waitedMs = Number.isFinite(firstNamedWallMs) && startedAt > 0 && typeof waitedBefore === "function"
|
|
26
35
|
? waitedBefore(startedAt + firstNamedWallMs)
|
|
27
36
|
: 0;
|
|
28
|
-
|
|
37
|
+
const openSeconds = (firstNamedMs - waitedMs) / 1000 - pieceSeconds / speed;
|
|
38
|
+
return openSeconds >= 0 ? { waitedMs, openSeconds } : null;
|
|
29
39
|
}
|
|
@@ -1545,7 +1545,13 @@ export class TorrentPool {
|
|
|
1545
1545
|
wire.request = function (index, offset, length, callback) {
|
|
1546
1546
|
const queued = this.requests.length;
|
|
1547
1547
|
const answer = request.call(this, index, offset, length, callback);
|
|
1548
|
-
if (this.requests.length > queued)
|
|
1548
|
+
if (this.requests.length > queued) {
|
|
1549
|
+
const now = Date.now();
|
|
1550
|
+
timings.asked(index, offset, peer, now);
|
|
1551
|
+
// When this wire was asked for the block: how long it has held it is
|
|
1552
|
+
// what decides whether a second peer is asked (`duplicateTailFor`).
|
|
1553
|
+
this.requests.at(-1).askedAt = now;
|
|
1554
|
+
}
|
|
1549
1555
|
return answer;
|
|
1550
1556
|
};
|
|
1551
1557
|
wire.on("piece", (index, offset) => timings.arrived(index, offset, peer, Date.now()));
|
|
@@ -264,25 +264,42 @@ export function describePieceTail(torrent, pieceIndex) {
|
|
|
264
264
|
* @param {object} torrent
|
|
265
265
|
* @param {number} pieceIndex
|
|
266
266
|
* @param {object} [options]
|
|
267
|
+
* A BLOCK IS ASKED AGAIN ONLY ONCE IT IS LATE. A short tail is not by itself a
|
|
268
|
+
* delay: copied as soon as the tail was short, 12-16 blocks of nearly every
|
|
269
|
+
* piece were asked twice and 82 of 215 copies arrived after their piece was
|
|
270
|
+
* complete (r41, Home Assistant, torrent-tv/meta#178). So a block is asked of a
|
|
271
|
+
* second peer once its request to the first has been out `minAgeMs` — by
|
|
272
|
+
* default the second WebTorrent sizes a wire's queue to fill
|
|
273
|
+
* (`PIPELINE_MAX_DURATION`), a starting point to be checked in the field — and
|
|
274
|
+
* the earliest moment a held block becomes late is returned, so whoever calls
|
|
275
|
+
* this can look again then.
|
|
276
|
+
*
|
|
277
|
+
* @param {object} torrent
|
|
278
|
+
* @param {number} pieceIndex
|
|
279
|
+
* @param {object} [options]
|
|
267
280
|
* @param {number} [options.maxCopies] - How many wires a missing block may be
|
|
268
281
|
* in flight on at once: the first request and one copy beside it.
|
|
269
|
-
* @
|
|
282
|
+
* @param {number} [options.minAgeMs] - How long the first request must have been out.
|
|
283
|
+
* @param {number} [options.now] - Wall clock, ms.
|
|
284
|
+
* @returns {{ duplicated: number, missing: number, wires: number, unplaced: number, dueAt: number | null }}
|
|
270
285
|
* `duplicated` counts requests the library actually placed for a block that
|
|
271
|
-
* was already outstanding elsewhere; `unplaced` the blocks in flight on
|
|
272
|
-
* wire alone that no other wire could be asked for now
|
|
286
|
+
* was already outstanding elsewhere; `unplaced` the late blocks in flight on
|
|
287
|
+
* one wire alone that no other wire could be asked for now; `dueAt` when the
|
|
288
|
+
* first block held by one wire and not yet late becomes late, or null.
|
|
273
289
|
*/
|
|
274
|
-
export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
290
|
+
export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2, minAgeMs = 1000, now = Date.now() } = {}) {
|
|
275
291
|
const piece = torrent?.pieces?.[pieceIndex];
|
|
276
292
|
const buffer = Array.isArray(piece?._buffer) ? piece._buffer : null;
|
|
277
293
|
if (!piece || !buffer || typeof piece.cancel !== "function" || !canPlaceRequests(torrent)) {
|
|
278
|
-
return { duplicated: 0, missing: 0, wires: 0, unplaced: 0 };
|
|
294
|
+
return { duplicated: 0, missing: 0, wires: 0, unplaced: 0, dueAt: null };
|
|
279
295
|
}
|
|
280
296
|
const chunks = Number.isFinite(piece._chunks) ? piece._chunks : buffer.length;
|
|
281
297
|
// Most calls are for a piece still arriving in bulk; its own count says so
|
|
282
298
|
// without walking its blocks or every wire's queue.
|
|
283
299
|
if (Number.isFinite(piece._buffered) && chunks - piece._buffered > SHORT_TAIL_BLOCKS) {
|
|
284
|
-
return { duplicated: 0, missing: chunks - piece._buffered, wires: 0, unplaced: 0 };
|
|
300
|
+
return { duplicated: 0, missing: chunks - piece._buffered, wires: 0, unplaced: 0, dueAt: null };
|
|
285
301
|
}
|
|
302
|
+
|
|
286
303
|
const missing = [];
|
|
287
304
|
for (let index = 0; index < chunks; index += 1) {
|
|
288
305
|
if (!buffer[index]) {
|
|
@@ -291,7 +308,7 @@ export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
|
291
308
|
}
|
|
292
309
|
const candidates = wiresForPiece(torrent, pieceIndex);
|
|
293
310
|
if (missing.length === 0 || candidates.length === 0) {
|
|
294
|
-
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0 };
|
|
311
|
+
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0, dueAt: null };
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
// A tail this short is what a blocked read is actually waiting on, and a
|
|
@@ -300,26 +317,34 @@ export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
|
300
317
|
// bytes that are already coming — measured 2026-08-19, the tails a reader
|
|
301
318
|
// waits on are 2 to 14 blocks of 512.
|
|
302
319
|
if (missing.length > SHORT_TAIL_BLOCKS) {
|
|
303
|
-
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0 };
|
|
320
|
+
return { duplicated: 0, missing: missing.length, wires: candidates.length, unplaced: 0, dueAt: null };
|
|
304
321
|
}
|
|
305
322
|
|
|
306
|
-
// Which wires each missing block is in flight on,
|
|
307
|
-
// queues: the library keeps a block's reservation for one
|
|
308
|
-
// copy asked here is visible nowhere else.
|
|
309
|
-
|
|
323
|
+
// Which wires each missing block is in flight on, and since when, read from
|
|
324
|
+
// the wires' own queues: the library keeps a block's reservation for one
|
|
325
|
+
// wire only, and a copy asked here is visible nowhere else. A request with no
|
|
326
|
+
// time of its own was asked before anybody watched, and is old.
|
|
327
|
+
const holders = new Map(missing.map(block => [block, { wires: new Set(), since: Number.POSITIVE_INFINITY }]));
|
|
310
328
|
for (const wire of Array.isArray(torrent.wires) ? torrent.wires : []) {
|
|
311
329
|
for (const request of Array.isArray(wire?.requests) ? wire.requests : []) {
|
|
312
330
|
if (request?.piece !== pieceIndex) continue;
|
|
313
|
-
holders.get(Math.floor(request.offset / BLOCK_BYTES))
|
|
331
|
+
const holding = holders.get(Math.floor(request.offset / BLOCK_BYTES));
|
|
332
|
+
if (!holding) continue;
|
|
333
|
+
holding.wires.add(wire);
|
|
334
|
+
holding.since = Math.min(holding.since, Number.isFinite(request.askedAt) ? request.askedAt : Number.NEGATIVE_INFINITY);
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
337
|
|
|
317
|
-
let duplicated = 0, unplaced = 0;
|
|
338
|
+
let duplicated = 0, unplaced = 0, dueAt = null;
|
|
318
339
|
for (const block of missing) {
|
|
319
|
-
const held = holders.get(block);
|
|
340
|
+
const { wires: held, since } = holders.get(block);
|
|
320
341
|
// Not in flight anywhere: the library's own picker asks for it; a block
|
|
321
342
|
// nobody has been asked for is not a tail held by a slow peer.
|
|
322
343
|
if (held.size === 0 || held.size >= maxCopies) continue;
|
|
344
|
+
if (now - since < minAgeMs) {
|
|
345
|
+
dueAt = Math.min(dueAt ?? Number.POSITIVE_INFINITY, since + minAgeMs);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
323
348
|
let placed = false;
|
|
324
349
|
for (const wire of candidates) {
|
|
325
350
|
if (held.has(wire)) continue;
|
|
@@ -343,7 +368,7 @@ export function duplicateTailFor(torrent, pieceIndex, { maxCopies = 2 } = {}) {
|
|
|
343
368
|
}
|
|
344
369
|
if (!placed) unplaced += 1;
|
|
345
370
|
}
|
|
346
|
-
return { duplicated, missing: missing.length, wires: candidates.length, unplaced };
|
|
371
|
+
return { duplicated, missing: missing.length, wires: candidates.length, unplaced, dueAt };
|
|
347
372
|
}
|
|
348
373
|
|
|
349
374
|
/**
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
* peer becomes able or unable to give the piece; so it is looked at on those —
|
|
19
19
|
* a block arriving, a wire unchoking, a peer stating a piece, a wire joining
|
|
20
20
|
* or closing, a piece verified, a new map. Several in one turn of the event
|
|
21
|
-
* loop are one look. A
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* case nothing else happens in
|
|
21
|
+
* loop are one look. A block is asked again only once its request has been out
|
|
22
|
+
* a wire's queue time (`duplicateTailFor`), so a block not yet late is looked
|
|
23
|
+
* at again when it becomes late, and a late one no other wire could take is
|
|
24
|
+
* looked at again a queue time later — once, in case nothing else happens in
|
|
25
|
+
* between. WebTorrent sizes a wire's queue to one second of its speed
|
|
26
|
+
* (`PIPELINE_MAX_DURATION`).
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
29
|
import { duplicateTailFor } from "./fastest-wires.js";
|
|
@@ -41,12 +43,15 @@ export class TailRepeat {
|
|
|
41
43
|
#clear;
|
|
42
44
|
#scheduled = false;
|
|
43
45
|
#timer = null;
|
|
46
|
+
/** When the armed look runs, wall clock ms. */
|
|
47
|
+
#timerAt = Number.POSITIVE_INFINITY;
|
|
44
48
|
#disposed = false;
|
|
45
49
|
/** @type {Map<object, { onBlock: (index: number) => void, onPeer: () => void }>} */
|
|
46
50
|
#wires = new Map();
|
|
47
51
|
#onWire;
|
|
48
52
|
#onPoke;
|
|
49
53
|
#log;
|
|
54
|
+
#now;
|
|
50
55
|
/**
|
|
51
56
|
* Grow-only, for the log: block copies asked, looks taken, and blocks that
|
|
52
57
|
* arrived after their piece was complete — what the copies cost.
|
|
@@ -62,9 +67,11 @@ export class TailRepeat {
|
|
|
62
67
|
* @param {(work: () => void, ms: number) => unknown} [params.wake]
|
|
63
68
|
* @param {(handle: unknown) => void} [params.clear]
|
|
64
69
|
* @param {(line: string) => void} [params.log]
|
|
70
|
+
* @param {() => number} [params.now]
|
|
65
71
|
*/
|
|
66
|
-
constructor({ torrent, requiredPieces, duplicate = duplicateTailFor, schedule = setImmediate, wake = setTimeout, clear = clearTimeout, log = null }) {
|
|
72
|
+
constructor({ torrent, requiredPieces, duplicate = duplicateTailFor, schedule = setImmediate, wake = setTimeout, clear = clearTimeout, log = null, now = Date.now }) {
|
|
67
73
|
this.#log = log;
|
|
74
|
+
this.#now = now;
|
|
68
75
|
this.#torrent = torrent;
|
|
69
76
|
this.#requiredPieces = requiredPieces;
|
|
70
77
|
this.#duplicate = duplicate;
|
|
@@ -95,6 +102,19 @@ export class TailRepeat {
|
|
|
95
102
|
this.#timer = null;
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
/** One look armed at a time, at the earliest moment asked for. */
|
|
106
|
+
#lookAgainAt(at, now) {
|
|
107
|
+
if (!Number.isFinite(at) || at >= this.#timerAt) return;
|
|
108
|
+
if (this.#timer !== null) this.#clear(this.#timer);
|
|
109
|
+
this.#timerAt = at;
|
|
110
|
+
this.#timer = this.#wake(() => {
|
|
111
|
+
this.#timer = null;
|
|
112
|
+
this.#timerAt = Number.POSITIVE_INFINITY;
|
|
113
|
+
this.poke();
|
|
114
|
+
}, Math.max(0, at - now));
|
|
115
|
+
this.#timer?.unref?.();
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
#watch(wire) {
|
|
99
119
|
if (!wire || this.#wires.has(wire) || typeof wire.on !== "function") return;
|
|
100
120
|
const listeners = {
|
|
@@ -128,14 +148,16 @@ export class TailRepeat {
|
|
|
128
148
|
this.#scheduled = false;
|
|
129
149
|
if (this.#disposed) return;
|
|
130
150
|
this.totals.looks += 1;
|
|
131
|
-
|
|
151
|
+
const now = this.#now();
|
|
152
|
+
let unplaced = 0, dueAt = Number.POSITIVE_INFINITY;
|
|
132
153
|
const asked = [];
|
|
133
154
|
for (const { from, to } of this.#requiredPieces()) {
|
|
134
155
|
for (let index = from; index <= to; index += 1) {
|
|
135
156
|
if (this.#torrent.bitfield?.get?.(index)) continue;
|
|
136
|
-
const result = this.#duplicate(this.#torrent, index);
|
|
157
|
+
const result = this.#duplicate(this.#torrent, index, { minAgeMs: WIRE_QUEUE_SECONDS * 1000, now });
|
|
137
158
|
this.totals.duplicated += result.duplicated;
|
|
138
159
|
unplaced += result.unplaced;
|
|
160
|
+
if (Number.isFinite(result.dueAt)) dueAt = Math.min(dueAt, result.dueAt);
|
|
139
161
|
if (result.duplicated > 0) asked.push(`${index}:${result.duplicated}/${result.missing}`);
|
|
140
162
|
}
|
|
141
163
|
}
|
|
@@ -143,12 +165,7 @@ export class TailRepeat {
|
|
|
143
165
|
this.#log?.(`tail: asked a second peer for the last blocks of piece:blocks/missing ${asked.join(" ")}; ` +
|
|
144
166
|
`${this.totals.duplicated} block copies asked so far, ${this.totals.lateBlocks} block(s) arrived after their piece was complete`);
|
|
145
167
|
}
|
|
146
|
-
if (unplaced > 0
|
|
147
|
-
|
|
148
|
-
this.#timer = null;
|
|
149
|
-
this.poke();
|
|
150
|
-
}, WIRE_QUEUE_SECONDS * 1000);
|
|
151
|
-
this.#timer?.unref?.();
|
|
152
|
-
}
|
|
168
|
+
if (unplaced > 0) dueAt = Math.min(dueAt, now + WIRE_QUEUE_SECONDS * 1000);
|
|
169
|
+
this.#lookAgainAt(dueAt, now);
|
|
153
170
|
}
|
|
154
171
|
}
|