@torrent-tv/proxy 2.87.11 → 2.87.12
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 +5 -0
- package/package.json +1 -1
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/transcode-sessions/post.js +13 -0
- package/server.js +725 -725
- package/services/encode/EncodeAdmission.js +45 -1
- package/services/encode/OutputOpening.js +4 -1
- package/services/encode/quality/EncodeCost.js +31 -0
- package/services/encode/quality/output-format.js +58 -9
- package/services/media/audio-inventory.js +245 -245
- package/services/server/ViewerRequests.js +25 -1
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/wire-outputs.js +3 -0
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/pool-adapter.js +365 -365
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/viewer/Viewer.js +8 -1
- package/test/audio-inventory.test.js +212 -212
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
|
@@ -1,365 +1,365 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file `TorrentPool`'s interface, served from the worker thread.
|
|
3
|
-
*
|
|
4
|
-
* The routes, the planner, the health report and the session manager all reach
|
|
5
|
-
* for a torrent pool and use it the same handful of ways. Rather than rewrite
|
|
6
|
-
* every one of them to thread a `sourceKey` through and await what used to be
|
|
7
|
-
* immediate, this presents the shape they already expect and does the thread
|
|
8
|
-
* hop behind it. Swapping the implementation is then a one-line change at
|
|
9
|
-
* construction, and the call sites are untouched — which is what keeps a change
|
|
10
|
-
* of this size reviewable.
|
|
11
|
-
*
|
|
12
|
-
* Two accommodations are needed, and both are deliberate:
|
|
13
|
-
*
|
|
14
|
-
* - **`prioritizeByteRange` stays synchronous.** It returns nothing the caller
|
|
15
|
-
* inspects, so the command is dispatched and not awaited. Awaiting it would
|
|
16
|
-
* mean touching every call site for no observable gain.
|
|
17
|
-
* - **`getTorrent` needs a `sourceKey`.** Torrent objects cannot cross a
|
|
18
|
-
* thread, so the worker keys them. Callers that have one pass it; the rest
|
|
19
|
-
* get one derived from the source itself, so the identity stays stable
|
|
20
|
-
* across calls for the same torrent.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { TorrentWorkerClient } from "./client.js";
|
|
24
|
-
import { deriveSourceKey } from "../../../utils/torrent-source-key.js";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* A torrent pool whose work happens on another thread.
|
|
28
|
-
*
|
|
29
|
-
* See `protocol.js` for why: the torrent was taking ~85% of the main thread and
|
|
30
|
-
* everything owed to a viewer queued behind it.
|
|
31
|
-
*/
|
|
32
|
-
export class WorkerTorrentPool {
|
|
33
|
-
#client;
|
|
34
|
-
/** Stand-ins by source key, so repeat calls return the same object. */
|
|
35
|
-
#torrents = new Map();
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @param {{ memoryBytes?: number, stateDir?: string }} [options]
|
|
39
|
-
*/
|
|
40
|
-
constructor(options = {}) {
|
|
41
|
-
this.#client = new TorrentWorkerClient(options);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Load (or join) a torrent and return a stand-in for it.
|
|
46
|
-
*
|
|
47
|
-
* @param {"magnet" | "torrent"} sourceType
|
|
48
|
-
* @param {string} source
|
|
49
|
-
* @returns {Promise<object>}
|
|
50
|
-
*/
|
|
51
|
-
async getTorrent(sourceType, source) {
|
|
52
|
-
const sourceKey = await deriveSourceKey(sourceType, source);
|
|
53
|
-
const existing = this.#torrents.get(sourceKey);
|
|
54
|
-
if (existing) {
|
|
55
|
-
return existing;
|
|
56
|
-
}
|
|
57
|
-
const torrent = await this.#client.getTorrent({ sourceKey, sourceType, source });
|
|
58
|
-
this.#torrents.set(sourceKey, torrent);
|
|
59
|
-
return torrent;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Claim a file for reading; the returned function releases it.
|
|
64
|
-
*
|
|
65
|
-
* Synchronous by design — see the file header.
|
|
66
|
-
*
|
|
67
|
-
* @param {object} torrent - A stand-in from {@link getTorrent}.
|
|
68
|
-
* @param {number} fileIndex
|
|
69
|
-
* @returns {() => void}
|
|
70
|
-
*/
|
|
71
|
-
/** What the spilled pieces weigh on the torrent thread, as last revised. */
|
|
72
|
-
get spilledBytes() {
|
|
73
|
-
return this.#client.spilledBytes;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Say how much disk those spilled pieces may take between them.
|
|
78
|
-
*
|
|
79
|
-
* @param {number} bytes
|
|
80
|
-
* @returns {Promise<number>} What they hold now.
|
|
81
|
-
*/
|
|
82
|
-
allowSpillBytes(bytes) {
|
|
83
|
-
return this.#client.allowSpillBytes(bytes);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get memoryClaim() {
|
|
87
|
-
return this.#client.memoryClaim ?? { held: 0, wanted: 0 };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
allowMemoryBytes(bytes) {
|
|
91
|
-
return this.#client.allowMemoryBytes(bytes);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
get wholeFileBytes() {
|
|
95
|
-
return this.#client.wholeFileBytes ?? 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
allowWholeFileBytes(bytes) {
|
|
99
|
-
return this.#client.allowWholeFileBytes(bytes);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Bytes every torrent here has moved.
|
|
104
|
-
*
|
|
105
|
-
* @returns {Promise<{ downloaded: number, uploaded: number }>}
|
|
106
|
-
*/
|
|
107
|
-
async getTorrentTotals() {
|
|
108
|
-
return this.#client.getTorrentTotals();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Live download figures for the progress display.
|
|
113
|
-
*
|
|
114
|
-
* @param {object} torrent
|
|
115
|
-
* @param {number | null} [fileIndex]
|
|
116
|
-
* @param {{ resumeAnchorByteStart?: number | null }} [options]
|
|
117
|
-
* @returns {Promise<object | null>}
|
|
118
|
-
*/
|
|
119
|
-
async getFileStats(torrent, fileIndex = null, options = {}) {
|
|
120
|
-
const sourceKey = torrent?.sourceKey;
|
|
121
|
-
if (!sourceKey) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
return this.#client.getFileStats({
|
|
125
|
-
sourceKey,
|
|
126
|
-
fileIndex,
|
|
127
|
-
resumeAnchorByteStart: options?.resumeAnchorByteStart ?? null
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Fetch one whole file using only the room the viewer's own reading leaves.
|
|
133
|
-
*
|
|
134
|
-
* For a soundtrack or subtitle file shipped beside the picture: small next to
|
|
135
|
-
* the film, and having it on disk is what turns a later switch into a local
|
|
136
|
-
* read instead of a wait on the swarm.
|
|
137
|
-
*
|
|
138
|
-
* @param {object} torrent
|
|
139
|
-
* @param {number} fileIndex
|
|
140
|
-
* @returns {Promise<boolean>} Whether a fill was started by this call.
|
|
141
|
-
*/
|
|
142
|
-
async fillFileInBackground(torrent, fileIndex) {
|
|
143
|
-
const sourceKey = torrent?.sourceKey;
|
|
144
|
-
if (!sourceKey) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
const answer = await this.#client.fillFile({ sourceKey, fileIndex });
|
|
148
|
-
return answer?.started === true;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Ask the torrent to fetch all remaining files at conditional TAIL urgency.
|
|
153
|
-
*
|
|
154
|
-
* @param {object} torrent
|
|
155
|
-
* @returns {Promise<boolean>}
|
|
156
|
-
*/
|
|
157
|
-
async fillTorrent(torrent) {
|
|
158
|
-
const sourceKey = torrent?.sourceKey;
|
|
159
|
-
if (!sourceKey) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
const answer = await this.#client.fillTorrent(sourceKey);
|
|
163
|
-
return answer?.started === true;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Start fetching the region a viewer is about to resume at. Named in seconds
|
|
171
|
-
* here; the worker turns it into bytes, where the file's duration is readable.
|
|
172
|
-
*
|
|
173
|
-
* @param {object} torrent
|
|
174
|
-
* @param {number} fileIndex
|
|
175
|
-
* @param {number} positionSeconds
|
|
176
|
-
* @returns {Promise<boolean>}
|
|
177
|
-
*/
|
|
178
|
-
async warmResumePosition(torrent, fileIndex, positionSeconds, durationSeconds) {
|
|
179
|
-
const sourceKey = torrent?.sourceKey;
|
|
180
|
-
if (!sourceKey) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
const answer = await this.#client.warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds });
|
|
184
|
-
return answer?.started === true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Reorder piece selection around a read position.
|
|
194
|
-
*
|
|
195
|
-
* Synchronous by design — see the file header.
|
|
196
|
-
*
|
|
197
|
-
* @param {object} torrent
|
|
198
|
-
* @param {number} fileIndex
|
|
199
|
-
* @param {number} byteStart
|
|
200
|
-
* @param {number} [windowBytes]
|
|
201
|
-
* @param {{ wholeFileRead?: boolean }} [options]
|
|
202
|
-
* @returns {void}
|
|
203
|
-
*/
|
|
204
|
-
prioritizeByteRange(torrent, fileIndex, byteStart, windowBytes, options) {
|
|
205
|
-
const sourceKey = torrent?.sourceKey;
|
|
206
|
-
if (!sourceKey) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
void this.#client
|
|
210
|
-
.prioritizeByteRange({
|
|
211
|
-
sourceKey,
|
|
212
|
-
fileIndex,
|
|
213
|
-
byteStart,
|
|
214
|
-
windowBytes,
|
|
215
|
-
wholeFileRead: options?.wholeFileRead === true
|
|
216
|
-
})
|
|
217
|
-
.catch(() => undefined);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Hand the download the priority map for one file.
|
|
222
|
-
*
|
|
223
|
-
* The map is republished whenever it changes, so a call that fails costs a
|
|
224
|
-
* moment rather than correctness — which is why the caller lets it go rather
|
|
225
|
-
* than retrying.
|
|
226
|
-
*
|
|
227
|
-
* @param {{ sourceKey: string, fileIndex: number, durationSeconds: number, zones: object[] }} params
|
|
228
|
-
* @returns {Promise<void>}
|
|
229
|
-
*/
|
|
230
|
-
async setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones }) {
|
|
231
|
-
if (!sourceKey) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
await this.#client.setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Pre-fetch the head and tail the codec probe needs.
|
|
239
|
-
*
|
|
240
|
-
* Takes an options object, matching `TorrentPool.prefetchFileEdges` — this
|
|
241
|
-
* adapter exists to present that same interface. It previously declared
|
|
242
|
-
* positional parameters instead, so the planner's options object arrived as
|
|
243
|
-
* `headBytes` and only worked because it was passed along far enough to be
|
|
244
|
-
* destructured at the far end. Anyone calling it as documented got the
|
|
245
|
-
* defaults instead of the sizes they asked for.
|
|
246
|
-
*
|
|
247
|
-
* @param {object} torrent
|
|
248
|
-
* @param {number} fileIndex
|
|
249
|
-
* @param {{ headBytes?: number, tailBytes?: number, timeoutMs?: number }} [options]
|
|
250
|
-
* @returns {Promise<unknown>}
|
|
251
|
-
*/
|
|
252
|
-
/**
|
|
253
|
-
* One byte range of one file, as bytes, on THIS thread.
|
|
254
|
-
*
|
|
255
|
-
* What the media layer is built from: a container takes `readRange(start,
|
|
256
|
-
* end)` and nothing else, so with this it can be parsed here instead of in
|
|
257
|
-
* the torrent thread. It used to be parsed there for one stated reason —
|
|
258
|
-
* "the main thread cannot open a read stream on one of its files", which is
|
|
259
|
-
* true of WebTorrent's own API and not of the bytes: the pieces live in
|
|
260
|
-
* shared memory and this read is the same one that serves every segment,
|
|
261
|
-
* so it waits for what has not arrived and steers the swarm toward it.
|
|
262
|
-
*
|
|
263
|
-
* **It COPIES, deliberately.** The zero-copy path — positions across the
|
|
264
|
-
* channel, the piece pinned until the far side says it is done — exists for
|
|
265
|
-
* 10 MB segments on the critical path, where the copy was measured at
|
|
266
|
-
* 18.84 ms. A container header is 64-256 KB and read once per file, about
|
|
267
|
-
* 0.12 ms, against a parse of 0.8 s and a swarm wait of up to a minute. The
|
|
268
|
-
* pinning protocol would buy nothing and is not on this path.
|
|
269
|
-
*
|
|
270
|
-
* @param {object} torrent
|
|
271
|
-
* @param {number} fileIndex
|
|
272
|
-
* @param {number} start - First byte, inclusive.
|
|
273
|
-
* @param {number} end - Last byte, inclusive.
|
|
274
|
-
* @returns {Promise<Buffer | null>} Null when the read failed or was cut
|
|
275
|
-
* short, which a container reads as "this file does not say".
|
|
276
|
-
*/
|
|
277
|
-
/**
|
|
278
|
-
* The byte ranges of one file the torrent holds WHOLE.
|
|
279
|
-
*
|
|
280
|
-
* What the subtitle walk decides from: it may read only what is already
|
|
281
|
-
* downloaded, and a list taken once per pass replaces a question per cluster.
|
|
282
|
-
*
|
|
283
|
-
* @param {object} torrent
|
|
284
|
-
* @param {number} fileIndex
|
|
285
|
-
* @returns {Promise<Array<[number, number]>>}
|
|
286
|
-
*/
|
|
287
|
-
async heldRangesOf(torrent, fileIndex) {
|
|
288
|
-
const sourceKey = torrent?.sourceKey;
|
|
289
|
-
return sourceKey ? this.#client.heldRanges({ sourceKey, fileIndex }) : [];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Bytes of a range the torrent already holds, never fetched.
|
|
294
|
-
*
|
|
295
|
-
* Not `readRangeOf`: that one declares demand and steers the swarm, which is
|
|
296
|
-
* right for a viewer waiting on a segment and wrong for a walk that must pull
|
|
297
|
-
* nothing the viewer is not waiting for.
|
|
298
|
-
*
|
|
299
|
-
* @param {object} torrent
|
|
300
|
-
* @param {number} fileIndex
|
|
301
|
-
* @param {number} start
|
|
302
|
-
* @param {number} end - Inclusive.
|
|
303
|
-
* @returns {Promise<Buffer | null>}
|
|
304
|
-
*/
|
|
305
|
-
async readHeldOf(torrent, fileIndex, start, end) {
|
|
306
|
-
const sourceKey = torrent?.sourceKey;
|
|
307
|
-
return sourceKey ? this.#client.readHeld({ sourceKey, fileIndex, start, end }) : null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async readRangeOf(torrent, fileIndex, start, end) {
|
|
311
|
-
const sourceKey = torrent?.sourceKey;
|
|
312
|
-
if (!sourceKey || !(end >= start) || !(start >= 0)) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
const stream = this.#client.createReadStream({ sourceKey, fileIndex, start, end });
|
|
316
|
-
const chunks = [];
|
|
317
|
-
let total = 0;
|
|
318
|
-
try {
|
|
319
|
-
const reader = stream.getReader();
|
|
320
|
-
for (;;) {
|
|
321
|
-
const { done, value } = await reader.read();
|
|
322
|
-
if (done) {
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
chunks.push(Buffer.from(value));
|
|
326
|
-
total += value.length;
|
|
327
|
-
}
|
|
328
|
-
} catch {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
return total > 0 ? Buffer.concat(chunks, total) : null;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async prefetchFileEdges(torrent, fileIndex, options = {}) {
|
|
335
|
-
const sourceKey = torrent?.sourceKey;
|
|
336
|
-
if (!sourceKey) {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
return this.#client.prefetchFileEdges({ sourceKey, fileIndex, options });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Which films this proxy holds right now, and how much of each.
|
|
344
|
-
*
|
|
345
|
-
* What content affinity is decided from: a viewer of a film somebody here is
|
|
346
|
-
* already downloading costs this proxy the encode and nothing else, while the
|
|
347
|
-
* same viewer sent anywhere else starts the download from nothing.
|
|
348
|
-
*
|
|
349
|
-
* @returns {Promise<{ infoHash: string, progress: number, bytes: number }[]>}
|
|
350
|
-
*/
|
|
351
|
-
async heldTorrents() {
|
|
352
|
-
const answer = await this.#client.heldTorrents();
|
|
353
|
-
return Array.isArray(answer?.held) ? answer.held : [];
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Shut the torrent client down and stop the thread.
|
|
358
|
-
*
|
|
359
|
-
* @returns {Promise<void>}
|
|
360
|
-
*/
|
|
361
|
-
async destroyAll() {
|
|
362
|
-
this.#torrents.clear();
|
|
363
|
-
await this.#client.destroyAll();
|
|
364
|
-
}
|
|
365
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file `TorrentPool`'s interface, served from the worker thread.
|
|
3
|
+
*
|
|
4
|
+
* The routes, the planner, the health report and the session manager all reach
|
|
5
|
+
* for a torrent pool and use it the same handful of ways. Rather than rewrite
|
|
6
|
+
* every one of them to thread a `sourceKey` through and await what used to be
|
|
7
|
+
* immediate, this presents the shape they already expect and does the thread
|
|
8
|
+
* hop behind it. Swapping the implementation is then a one-line change at
|
|
9
|
+
* construction, and the call sites are untouched — which is what keeps a change
|
|
10
|
+
* of this size reviewable.
|
|
11
|
+
*
|
|
12
|
+
* Two accommodations are needed, and both are deliberate:
|
|
13
|
+
*
|
|
14
|
+
* - **`prioritizeByteRange` stays synchronous.** It returns nothing the caller
|
|
15
|
+
* inspects, so the command is dispatched and not awaited. Awaiting it would
|
|
16
|
+
* mean touching every call site for no observable gain.
|
|
17
|
+
* - **`getTorrent` needs a `sourceKey`.** Torrent objects cannot cross a
|
|
18
|
+
* thread, so the worker keys them. Callers that have one pass it; the rest
|
|
19
|
+
* get one derived from the source itself, so the identity stays stable
|
|
20
|
+
* across calls for the same torrent.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { TorrentWorkerClient } from "./client.js";
|
|
24
|
+
import { deriveSourceKey } from "../../../utils/torrent-source-key.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A torrent pool whose work happens on another thread.
|
|
28
|
+
*
|
|
29
|
+
* See `protocol.js` for why: the torrent was taking ~85% of the main thread and
|
|
30
|
+
* everything owed to a viewer queued behind it.
|
|
31
|
+
*/
|
|
32
|
+
export class WorkerTorrentPool {
|
|
33
|
+
#client;
|
|
34
|
+
/** Stand-ins by source key, so repeat calls return the same object. */
|
|
35
|
+
#torrents = new Map();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{ memoryBytes?: number, stateDir?: string }} [options]
|
|
39
|
+
*/
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.#client = new TorrentWorkerClient(options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load (or join) a torrent and return a stand-in for it.
|
|
46
|
+
*
|
|
47
|
+
* @param {"magnet" | "torrent"} sourceType
|
|
48
|
+
* @param {string} source
|
|
49
|
+
* @returns {Promise<object>}
|
|
50
|
+
*/
|
|
51
|
+
async getTorrent(sourceType, source) {
|
|
52
|
+
const sourceKey = await deriveSourceKey(sourceType, source);
|
|
53
|
+
const existing = this.#torrents.get(sourceKey);
|
|
54
|
+
if (existing) {
|
|
55
|
+
return existing;
|
|
56
|
+
}
|
|
57
|
+
const torrent = await this.#client.getTorrent({ sourceKey, sourceType, source });
|
|
58
|
+
this.#torrents.set(sourceKey, torrent);
|
|
59
|
+
return torrent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Claim a file for reading; the returned function releases it.
|
|
64
|
+
*
|
|
65
|
+
* Synchronous by design — see the file header.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} torrent - A stand-in from {@link getTorrent}.
|
|
68
|
+
* @param {number} fileIndex
|
|
69
|
+
* @returns {() => void}
|
|
70
|
+
*/
|
|
71
|
+
/** What the spilled pieces weigh on the torrent thread, as last revised. */
|
|
72
|
+
get spilledBytes() {
|
|
73
|
+
return this.#client.spilledBytes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Say how much disk those spilled pieces may take between them.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} bytes
|
|
80
|
+
* @returns {Promise<number>} What they hold now.
|
|
81
|
+
*/
|
|
82
|
+
allowSpillBytes(bytes) {
|
|
83
|
+
return this.#client.allowSpillBytes(bytes);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get memoryClaim() {
|
|
87
|
+
return this.#client.memoryClaim ?? { held: 0, wanted: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
allowMemoryBytes(bytes) {
|
|
91
|
+
return this.#client.allowMemoryBytes(bytes);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get wholeFileBytes() {
|
|
95
|
+
return this.#client.wholeFileBytes ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
allowWholeFileBytes(bytes) {
|
|
99
|
+
return this.#client.allowWholeFileBytes(bytes);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bytes every torrent here has moved.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<{ downloaded: number, uploaded: number }>}
|
|
106
|
+
*/
|
|
107
|
+
async getTorrentTotals() {
|
|
108
|
+
return this.#client.getTorrentTotals();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Live download figures for the progress display.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} torrent
|
|
115
|
+
* @param {number | null} [fileIndex]
|
|
116
|
+
* @param {{ resumeAnchorByteStart?: number | null }} [options]
|
|
117
|
+
* @returns {Promise<object | null>}
|
|
118
|
+
*/
|
|
119
|
+
async getFileStats(torrent, fileIndex = null, options = {}) {
|
|
120
|
+
const sourceKey = torrent?.sourceKey;
|
|
121
|
+
if (!sourceKey) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return this.#client.getFileStats({
|
|
125
|
+
sourceKey,
|
|
126
|
+
fileIndex,
|
|
127
|
+
resumeAnchorByteStart: options?.resumeAnchorByteStart ?? null
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Fetch one whole file using only the room the viewer's own reading leaves.
|
|
133
|
+
*
|
|
134
|
+
* For a soundtrack or subtitle file shipped beside the picture: small next to
|
|
135
|
+
* the film, and having it on disk is what turns a later switch into a local
|
|
136
|
+
* read instead of a wait on the swarm.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} torrent
|
|
139
|
+
* @param {number} fileIndex
|
|
140
|
+
* @returns {Promise<boolean>} Whether a fill was started by this call.
|
|
141
|
+
*/
|
|
142
|
+
async fillFileInBackground(torrent, fileIndex) {
|
|
143
|
+
const sourceKey = torrent?.sourceKey;
|
|
144
|
+
if (!sourceKey) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const answer = await this.#client.fillFile({ sourceKey, fileIndex });
|
|
148
|
+
return answer?.started === true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ask the torrent to fetch all remaining files at conditional TAIL urgency.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} torrent
|
|
155
|
+
* @returns {Promise<boolean>}
|
|
156
|
+
*/
|
|
157
|
+
async fillTorrent(torrent) {
|
|
158
|
+
const sourceKey = torrent?.sourceKey;
|
|
159
|
+
if (!sourceKey) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const answer = await this.#client.fillTorrent(sourceKey);
|
|
163
|
+
return answer?.started === true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Start fetching the region a viewer is about to resume at. Named in seconds
|
|
171
|
+
* here; the worker turns it into bytes, where the file's duration is readable.
|
|
172
|
+
*
|
|
173
|
+
* @param {object} torrent
|
|
174
|
+
* @param {number} fileIndex
|
|
175
|
+
* @param {number} positionSeconds
|
|
176
|
+
* @returns {Promise<boolean>}
|
|
177
|
+
*/
|
|
178
|
+
async warmResumePosition(torrent, fileIndex, positionSeconds, durationSeconds) {
|
|
179
|
+
const sourceKey = torrent?.sourceKey;
|
|
180
|
+
if (!sourceKey) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const answer = await this.#client.warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds });
|
|
184
|
+
return answer?.started === true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reorder piece selection around a read position.
|
|
194
|
+
*
|
|
195
|
+
* Synchronous by design — see the file header.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} torrent
|
|
198
|
+
* @param {number} fileIndex
|
|
199
|
+
* @param {number} byteStart
|
|
200
|
+
* @param {number} [windowBytes]
|
|
201
|
+
* @param {{ wholeFileRead?: boolean }} [options]
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
prioritizeByteRange(torrent, fileIndex, byteStart, windowBytes, options) {
|
|
205
|
+
const sourceKey = torrent?.sourceKey;
|
|
206
|
+
if (!sourceKey) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
void this.#client
|
|
210
|
+
.prioritizeByteRange({
|
|
211
|
+
sourceKey,
|
|
212
|
+
fileIndex,
|
|
213
|
+
byteStart,
|
|
214
|
+
windowBytes,
|
|
215
|
+
wholeFileRead: options?.wholeFileRead === true
|
|
216
|
+
})
|
|
217
|
+
.catch(() => undefined);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Hand the download the priority map for one file.
|
|
222
|
+
*
|
|
223
|
+
* The map is republished whenever it changes, so a call that fails costs a
|
|
224
|
+
* moment rather than correctness — which is why the caller lets it go rather
|
|
225
|
+
* than retrying.
|
|
226
|
+
*
|
|
227
|
+
* @param {{ sourceKey: string, fileIndex: number, durationSeconds: number, zones: object[] }} params
|
|
228
|
+
* @returns {Promise<void>}
|
|
229
|
+
*/
|
|
230
|
+
async setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones }) {
|
|
231
|
+
if (!sourceKey) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await this.#client.setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Pre-fetch the head and tail the codec probe needs.
|
|
239
|
+
*
|
|
240
|
+
* Takes an options object, matching `TorrentPool.prefetchFileEdges` — this
|
|
241
|
+
* adapter exists to present that same interface. It previously declared
|
|
242
|
+
* positional parameters instead, so the planner's options object arrived as
|
|
243
|
+
* `headBytes` and only worked because it was passed along far enough to be
|
|
244
|
+
* destructured at the far end. Anyone calling it as documented got the
|
|
245
|
+
* defaults instead of the sizes they asked for.
|
|
246
|
+
*
|
|
247
|
+
* @param {object} torrent
|
|
248
|
+
* @param {number} fileIndex
|
|
249
|
+
* @param {{ headBytes?: number, tailBytes?: number, timeoutMs?: number }} [options]
|
|
250
|
+
* @returns {Promise<unknown>}
|
|
251
|
+
*/
|
|
252
|
+
/**
|
|
253
|
+
* One byte range of one file, as bytes, on THIS thread.
|
|
254
|
+
*
|
|
255
|
+
* What the media layer is built from: a container takes `readRange(start,
|
|
256
|
+
* end)` and nothing else, so with this it can be parsed here instead of in
|
|
257
|
+
* the torrent thread. It used to be parsed there for one stated reason —
|
|
258
|
+
* "the main thread cannot open a read stream on one of its files", which is
|
|
259
|
+
* true of WebTorrent's own API and not of the bytes: the pieces live in
|
|
260
|
+
* shared memory and this read is the same one that serves every segment,
|
|
261
|
+
* so it waits for what has not arrived and steers the swarm toward it.
|
|
262
|
+
*
|
|
263
|
+
* **It COPIES, deliberately.** The zero-copy path — positions across the
|
|
264
|
+
* channel, the piece pinned until the far side says it is done — exists for
|
|
265
|
+
* 10 MB segments on the critical path, where the copy was measured at
|
|
266
|
+
* 18.84 ms. A container header is 64-256 KB and read once per file, about
|
|
267
|
+
* 0.12 ms, against a parse of 0.8 s and a swarm wait of up to a minute. The
|
|
268
|
+
* pinning protocol would buy nothing and is not on this path.
|
|
269
|
+
*
|
|
270
|
+
* @param {object} torrent
|
|
271
|
+
* @param {number} fileIndex
|
|
272
|
+
* @param {number} start - First byte, inclusive.
|
|
273
|
+
* @param {number} end - Last byte, inclusive.
|
|
274
|
+
* @returns {Promise<Buffer | null>} Null when the read failed or was cut
|
|
275
|
+
* short, which a container reads as "this file does not say".
|
|
276
|
+
*/
|
|
277
|
+
/**
|
|
278
|
+
* The byte ranges of one file the torrent holds WHOLE.
|
|
279
|
+
*
|
|
280
|
+
* What the subtitle walk decides from: it may read only what is already
|
|
281
|
+
* downloaded, and a list taken once per pass replaces a question per cluster.
|
|
282
|
+
*
|
|
283
|
+
* @param {object} torrent
|
|
284
|
+
* @param {number} fileIndex
|
|
285
|
+
* @returns {Promise<Array<[number, number]>>}
|
|
286
|
+
*/
|
|
287
|
+
async heldRangesOf(torrent, fileIndex) {
|
|
288
|
+
const sourceKey = torrent?.sourceKey;
|
|
289
|
+
return sourceKey ? this.#client.heldRanges({ sourceKey, fileIndex }) : [];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Bytes of a range the torrent already holds, never fetched.
|
|
294
|
+
*
|
|
295
|
+
* Not `readRangeOf`: that one declares demand and steers the swarm, which is
|
|
296
|
+
* right for a viewer waiting on a segment and wrong for a walk that must pull
|
|
297
|
+
* nothing the viewer is not waiting for.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} torrent
|
|
300
|
+
* @param {number} fileIndex
|
|
301
|
+
* @param {number} start
|
|
302
|
+
* @param {number} end - Inclusive.
|
|
303
|
+
* @returns {Promise<Buffer | null>}
|
|
304
|
+
*/
|
|
305
|
+
async readHeldOf(torrent, fileIndex, start, end) {
|
|
306
|
+
const sourceKey = torrent?.sourceKey;
|
|
307
|
+
return sourceKey ? this.#client.readHeld({ sourceKey, fileIndex, start, end }) : null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async readRangeOf(torrent, fileIndex, start, end) {
|
|
311
|
+
const sourceKey = torrent?.sourceKey;
|
|
312
|
+
if (!sourceKey || !(end >= start) || !(start >= 0)) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const stream = this.#client.createReadStream({ sourceKey, fileIndex, start, end });
|
|
316
|
+
const chunks = [];
|
|
317
|
+
let total = 0;
|
|
318
|
+
try {
|
|
319
|
+
const reader = stream.getReader();
|
|
320
|
+
for (;;) {
|
|
321
|
+
const { done, value } = await reader.read();
|
|
322
|
+
if (done) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
chunks.push(Buffer.from(value));
|
|
326
|
+
total += value.length;
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return total > 0 ? Buffer.concat(chunks, total) : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async prefetchFileEdges(torrent, fileIndex, options = {}) {
|
|
335
|
+
const sourceKey = torrent?.sourceKey;
|
|
336
|
+
if (!sourceKey) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return this.#client.prefetchFileEdges({ sourceKey, fileIndex, options });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Which films this proxy holds right now, and how much of each.
|
|
344
|
+
*
|
|
345
|
+
* What content affinity is decided from: a viewer of a film somebody here is
|
|
346
|
+
* already downloading costs this proxy the encode and nothing else, while the
|
|
347
|
+
* same viewer sent anywhere else starts the download from nothing.
|
|
348
|
+
*
|
|
349
|
+
* @returns {Promise<{ infoHash: string, progress: number, bytes: number }[]>}
|
|
350
|
+
*/
|
|
351
|
+
async heldTorrents() {
|
|
352
|
+
const answer = await this.#client.heldTorrents();
|
|
353
|
+
return Array.isArray(answer?.held) ? answer.held : [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Shut the torrent client down and stop the thread.
|
|
358
|
+
*
|
|
359
|
+
* @returns {Promise<void>}
|
|
360
|
+
*/
|
|
361
|
+
async destroyAll() {
|
|
362
|
+
this.#torrents.clear();
|
|
363
|
+
await this.#client.destroyAll();
|
|
364
|
+
}
|
|
365
|
+
}
|