@torrent-tv/proxy 2.107.51 → 2.107.53
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 +1 -1
- package/routes/api/sources/files/get.js +6 -4
- package/routes/api/sources/stats/get.js +4 -2
- package/services/encode/quality/HostLoad.js +1 -1
- package/services/server/ForecastDiagnostics.js +23 -0
- package/services/server/OutputLifecycle.js +7 -1
- package/services/server/ViewerRequests.js +80 -288
- package/services/server/wire-outputs.js +17 -0
- package/services/torrent/download/TailRepeat.js +155 -0
- package/services/torrent/torrent-pool.js +6 -4
- package/services/torrent/worker/WebTorrentTailRepeatAdapter.js +216 -0
- package/services/viewer/OutputForecastGeometry.js +87 -0
- package/services/viewer/ViewerPlaybackState.js +215 -0
- package/services/torrent/worker/fastest-wires.js +0 -398
- package/services/torrent/worker/tail-repeat.js +0 -171
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Ask a second peer for the last blocks of a piece somebody is waiting
|
|
3
|
-
* for, when one slow peer holds them (torrent-tv/meta#178).
|
|
4
|
-
*
|
|
5
|
-
* Field r40 (Home Assistant, 2026-10-14, a cold 1080p MKV): the first run's
|
|
6
|
-
* input waited 10.0 s, last for piece 8, whose slowest block was 4.5 s in
|
|
7
|
-
* flight from one peer; pieces 14-16 waited 10.0-11.5 s on one peer each. A
|
|
8
|
-
* block is reserved for one wire, and the piece completes when its slowest
|
|
9
|
-
* holder delivers (`duplicateTailFor`). Nothing had asked a second peer since
|
|
10
|
-
* the blocked-read path that did so was removed (`0e38ec3`, torrent-tv/meta#95).
|
|
11
|
-
*
|
|
12
|
-
* Which pieces: those of the urgent first class the swarm was last told
|
|
13
|
-
* (`SwarmSelection.requiredPieces`) — what somebody waits for now. A map that
|
|
14
|
-
* withdraws them, or an input that is ready, takes them out, and nothing more
|
|
15
|
-
* is asked for them.
|
|
16
|
-
*
|
|
17
|
-
* When: a tail changes when a block arrives, a wire's queue moves on, or a
|
|
18
|
-
* peer becomes able or unable to give the piece; so it is looked at on those —
|
|
19
|
-
* a block arriving, a wire unchoking, a peer stating a piece, a wire joining
|
|
20
|
-
* or closing, a piece verified, a new map. Several in one turn of the event
|
|
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`).
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { duplicateTailFor } from "./fastest-wires.js";
|
|
30
|
-
|
|
31
|
-
/** WebTorrent's `PIPELINE_MAX_DURATION`: a wire is given one second of its speed to fetch. */
|
|
32
|
-
const WIRE_QUEUE_SECONDS = 1;
|
|
33
|
-
|
|
34
|
-
/** What a wire says, besides a block arriving, that can change who could be asked for a piece. */
|
|
35
|
-
const WIRE_EVENTS = ["unchoke", "have", "bitfield"];
|
|
36
|
-
|
|
37
|
-
export class TailRepeat {
|
|
38
|
-
#torrent;
|
|
39
|
-
#requiredPieces;
|
|
40
|
-
#duplicate;
|
|
41
|
-
#schedule;
|
|
42
|
-
#wake;
|
|
43
|
-
#clear;
|
|
44
|
-
#scheduled = false;
|
|
45
|
-
#timer = null;
|
|
46
|
-
/** When the armed look runs, wall clock ms. */
|
|
47
|
-
#timerAt = Number.POSITIVE_INFINITY;
|
|
48
|
-
#disposed = false;
|
|
49
|
-
/** @type {Map<object, { onBlock: (index: number) => void, onPeer: () => void }>} */
|
|
50
|
-
#wires = new Map();
|
|
51
|
-
#onWire;
|
|
52
|
-
#onPoke;
|
|
53
|
-
#log;
|
|
54
|
-
#now;
|
|
55
|
-
/**
|
|
56
|
-
* Grow-only, for the log: block copies asked, looks taken, and blocks that
|
|
57
|
-
* arrived after their piece was complete — what the copies cost.
|
|
58
|
-
*/
|
|
59
|
-
totals = { duplicated: 0, looks: 0, lateBlocks: 0 };
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @param {object} params
|
|
63
|
-
* @param {object} params.torrent
|
|
64
|
-
* @param {() => Array<{ from: number, to: number }>} params.requiredPieces
|
|
65
|
-
* @param {typeof duplicateTailFor} [params.duplicate]
|
|
66
|
-
* @param {(work: () => void) => void} [params.schedule] - Runs work once this turn of the event loop is over.
|
|
67
|
-
* @param {(work: () => void, ms: number) => unknown} [params.wake]
|
|
68
|
-
* @param {(handle: unknown) => void} [params.clear]
|
|
69
|
-
* @param {(line: string) => void} [params.log]
|
|
70
|
-
* @param {() => number} [params.now]
|
|
71
|
-
*/
|
|
72
|
-
constructor({ torrent, requiredPieces, duplicate = duplicateTailFor, schedule = setImmediate, wake = setTimeout, clear = clearTimeout, log = null, now = Date.now }) {
|
|
73
|
-
this.#log = log;
|
|
74
|
-
this.#now = now;
|
|
75
|
-
this.#torrent = torrent;
|
|
76
|
-
this.#requiredPieces = requiredPieces;
|
|
77
|
-
this.#duplicate = duplicate;
|
|
78
|
-
this.#schedule = schedule;
|
|
79
|
-
this.#wake = wake;
|
|
80
|
-
this.#clear = clear;
|
|
81
|
-
this.#onWire = (wire) => this.#watch(wire);
|
|
82
|
-
this.#onPoke = () => this.poke();
|
|
83
|
-
torrent.on?.("wire", this.#onWire);
|
|
84
|
-
torrent.on?.("verified", this.#onPoke);
|
|
85
|
-
for (const wire of Array.isArray(torrent.wires) ? torrent.wires : []) this.#watch(wire);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Look at the tails once this turn of the event loop is over. */
|
|
89
|
-
poke() {
|
|
90
|
-
if (this.#scheduled || this.#disposed) return;
|
|
91
|
-
this.#scheduled = true;
|
|
92
|
-
this.#schedule(() => this.#look());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Stop watching: the torrent is going. */
|
|
96
|
-
dispose() {
|
|
97
|
-
this.#disposed = true;
|
|
98
|
-
this.#torrent.removeListener?.("wire", this.#onWire);
|
|
99
|
-
this.#torrent.removeListener?.("verified", this.#onPoke);
|
|
100
|
-
for (const wire of [...this.#wires.keys()]) this.#forget(wire);
|
|
101
|
-
if (this.#timer !== null) this.#clear(this.#timer);
|
|
102
|
-
this.#timer = null;
|
|
103
|
-
}
|
|
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
|
-
|
|
118
|
-
#watch(wire) {
|
|
119
|
-
if (!wire || this.#wires.has(wire) || typeof wire.on !== "function") return;
|
|
120
|
-
const listeners = {
|
|
121
|
-
onBlock: (index) => {
|
|
122
|
-
// A block of a piece already verified is a copy that came second; the
|
|
123
|
-
// block that completes a piece arrives before the piece is verified.
|
|
124
|
-
if (Number.isInteger(index) && this.#torrent.bitfield?.get?.(index)) this.totals.lateBlocks += 1;
|
|
125
|
-
this.poke();
|
|
126
|
-
},
|
|
127
|
-
onPeer: () => this.poke()
|
|
128
|
-
};
|
|
129
|
-
this.#wires.set(wire, listeners);
|
|
130
|
-
wire.on("piece", listeners.onBlock);
|
|
131
|
-
for (const event of WIRE_EVENTS) wire.on(event, listeners.onPeer);
|
|
132
|
-
wire.once?.("close", () => {
|
|
133
|
-
this.#forget(wire);
|
|
134
|
-
this.poke();
|
|
135
|
-
});
|
|
136
|
-
this.poke();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
#forget(wire) {
|
|
140
|
-
const listeners = this.#wires.get(wire);
|
|
141
|
-
if (!listeners) return;
|
|
142
|
-
wire.removeListener?.("piece", listeners.onBlock);
|
|
143
|
-
for (const event of WIRE_EVENTS) wire.removeListener?.(event, listeners.onPeer);
|
|
144
|
-
this.#wires.delete(wire);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
#look() {
|
|
148
|
-
this.#scheduled = false;
|
|
149
|
-
if (this.#disposed) return;
|
|
150
|
-
this.totals.looks += 1;
|
|
151
|
-
const now = this.#now();
|
|
152
|
-
let unplaced = 0, dueAt = Number.POSITIVE_INFINITY;
|
|
153
|
-
const asked = [];
|
|
154
|
-
for (const { from, to } of this.#requiredPieces()) {
|
|
155
|
-
for (let index = from; index <= to; index += 1) {
|
|
156
|
-
if (this.#torrent.bitfield?.get?.(index)) continue;
|
|
157
|
-
const result = this.#duplicate(this.#torrent, index, { minAgeMs: WIRE_QUEUE_SECONDS * 1000, now });
|
|
158
|
-
this.totals.duplicated += result.duplicated;
|
|
159
|
-
unplaced += result.unplaced;
|
|
160
|
-
if (Number.isFinite(result.dueAt)) dueAt = Math.min(dueAt, result.dueAt);
|
|
161
|
-
if (result.duplicated > 0) asked.push(`${index}:${result.duplicated}/${result.missing}`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (asked.length > 0) {
|
|
165
|
-
this.#log?.(`tail: asked a second peer for the last blocks of piece:blocks/missing ${asked.join(" ")}; ` +
|
|
166
|
-
`${this.totals.duplicated} block copies asked so far, ${this.totals.lateBlocks} block(s) arrived after their piece was complete`);
|
|
167
|
-
}
|
|
168
|
-
if (unplaced > 0) dueAt = Math.min(dueAt, now + WIRE_QUEUE_SECONDS * 1000);
|
|
169
|
-
this.#lookAgainAt(dueAt, now);
|
|
170
|
-
}
|
|
171
|
-
}
|