@torrent-tv/proxy 2.96.5 → 2.96.6
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
package/server.js
CHANGED
|
@@ -422,6 +422,7 @@ export async function startProxyServer({
|
|
|
422
422
|
const segmentSourceRanges = new Map();
|
|
423
423
|
const downloadMaps = new DownloadMaps({
|
|
424
424
|
publish: (map) => torrentPool.setPriorityMap(map),
|
|
425
|
+
log: (line) => logger.info(line),
|
|
425
426
|
resolvePlayback: async (map) => {
|
|
426
427
|
if (map.zones.length === 0) return [];
|
|
427
428
|
const params = await containerOver(map);
|
|
@@ -476,7 +476,7 @@ export class TorrentWorkerClient {
|
|
|
476
476
|
* @returns {Promise<void>}
|
|
477
477
|
*/
|
|
478
478
|
async setPriorityMap({ sourceKey, fileIndex, zones, durationSeconds }) {
|
|
479
|
-
|
|
479
|
+
return this.#caller.call(Command.PRIORITY_MAP, {
|
|
480
480
|
sourceKey,
|
|
481
481
|
fileIndex,
|
|
482
482
|
zones,
|
|
@@ -168,7 +168,7 @@ export class WorkerTorrentPool {
|
|
|
168
168
|
if (!sourceKey) {
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
-
|
|
171
|
+
return this.#client.setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones });
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
@@ -459,12 +459,17 @@ async function runCommand(command, params, id) {
|
|
|
459
459
|
const zones = Array.isArray(params.zones) ? params.zones : [];
|
|
460
460
|
// Nor may one for a file held whole, which has nothing left to fetch.
|
|
461
461
|
const torrent = await knownTorrent(params.sourceKey);
|
|
462
|
-
if (torrent) {
|
|
463
|
-
|
|
464
|
-
pool.applyPriorityMap(torrent, params.fileIndex, zones, params.durationSeconds);
|
|
465
|
-
torrent.emit("priority-map-changed", params.fileIndex);
|
|
462
|
+
if (!torrent) {
|
|
463
|
+
return { appliedMs: null };
|
|
466
464
|
}
|
|
467
|
-
|
|
465
|
+
// What applying a map costs this thread, told to whoever sent it: every
|
|
466
|
+
// zone becomes a window of the register and the swarm is told again
|
|
467
|
+
// (torrent-tv/meta#166).
|
|
468
|
+
const startedAt = performance.now();
|
|
469
|
+
ensureArrivalsWired(params.sourceKey, torrent);
|
|
470
|
+
pool.applyPriorityMap(torrent, params.fileIndex, zones, params.durationSeconds);
|
|
471
|
+
torrent.emit("priority-map-changed", params.fileIndex);
|
|
472
|
+
return { appliedMs: performance.now() - startedAt };
|
|
468
473
|
}
|
|
469
474
|
|
|
470
475
|
case Command.READ_RANGE: {
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How often what the publications of one file cost is said, at most: the
|
|
3
|
+
* cadence of the proxy's other once-a-minute readings.
|
|
4
|
+
*/
|
|
5
|
+
const REPORT_EVERY_MS = 60_000;
|
|
6
|
+
|
|
1
7
|
/** One published download map per file, including download-only metadata reads. */
|
|
2
8
|
export class DownloadMaps {
|
|
3
9
|
#files = new Map();
|
|
4
10
|
#publish;
|
|
5
11
|
#resolvePlayback;
|
|
6
12
|
#nextEpoch = 0;
|
|
13
|
+
#log;
|
|
14
|
+
#now;
|
|
7
15
|
|
|
8
|
-
constructor({ publish, resolvePlayback = async map => map.zones }) {
|
|
16
|
+
constructor({ publish, resolvePlayback = async map => map.zones, log = null, now = Date.now }) {
|
|
9
17
|
if (typeof publish !== "function") throw new TypeError("Download map publication is required.");
|
|
10
18
|
this.#publish = publish;
|
|
11
19
|
this.#resolvePlayback = resolvePlayback;
|
|
20
|
+
this.#log = typeof log === "function" ? log : null;
|
|
21
|
+
this.#now = now;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
async playback({ sourceKey, fileIndex, durationSeconds, zones }) {
|
|
@@ -197,11 +207,130 @@ export class DownloadMaps {
|
|
|
197
207
|
}
|
|
198
208
|
}
|
|
199
209
|
const publication = { sourceKey: file.sourceKey, fileIndex: file.fileIndex, durationSeconds: file.durationSeconds, zones };
|
|
210
|
+
// SENT ONLY WHEN THE TORRENT WOULD ACT ON IT DIFFERENTLY: other bytes,
|
|
211
|
+
// another level or order of claim, or deadlines in another order. A map was
|
|
212
|
+
// sent after every read of the file's statements, changed or not, and the
|
|
213
|
+
// read waited for it: field 2026-10-10, 48-64 such reads per stretch of a
|
|
214
|
+
// two-hour AVI, 28-54 s before its copy began, with 5-12 thousand zones in
|
|
215
|
+
// each map (torrent-tv/meta#166). A deadline worked out again on a later
|
|
216
|
+
// clock moves every zone of a viewer playing on by the same amount and
|
|
217
|
+
// orders nothing differently, so the deadlines' order is compared and not
|
|
218
|
+
// their values.
|
|
219
|
+
const shape = shapeOf(publication);
|
|
220
|
+
if (file.shape && sameShape(file.shape, shape)) {
|
|
221
|
+
this.#statsOf(file).unchanged += 1;
|
|
222
|
+
this.#report(file);
|
|
223
|
+
return Promise.resolve();
|
|
224
|
+
}
|
|
225
|
+
file.shape = shape;
|
|
200
226
|
const pending = file.pending.then(() => {
|
|
201
|
-
if (this.#files.get(`${file.sourceKey}:${file.fileIndex}`)
|
|
227
|
+
if (this.#files.get(`${file.sourceKey}:${file.fileIndex}`) !== file) return undefined;
|
|
228
|
+
const sentAt = this.#now();
|
|
229
|
+
return Promise.resolve(this.#publish(publication)).then(reply => {
|
|
230
|
+
// A torrent not yet in the thread applies nothing, so this map was not
|
|
231
|
+
// delivered and the next one, however alike, has to be.
|
|
232
|
+
if (reply?.appliedMs === null && file.shape === shape) file.shape = null;
|
|
233
|
+
// The window open now, which a report since this map was asked for may have started.
|
|
234
|
+
const stats = this.#statsOf(file);
|
|
235
|
+
stats.sent += 1;
|
|
236
|
+
stats.zones.push(zones.length);
|
|
237
|
+
stats.deliveredMs.push(this.#now() - sentAt);
|
|
238
|
+
if (Number.isFinite(reply?.appliedMs)) stats.appliedMs.push(reply.appliedMs);
|
|
239
|
+
this.#report(file);
|
|
240
|
+
return reply;
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
// A failed publication must not prevent the next state change from being
|
|
244
|
+
// sent, and leaves nothing to compare the next one against.
|
|
245
|
+
file.pending = pending.catch(() => {
|
|
246
|
+
if (file.shape === shape) file.shape = null;
|
|
202
247
|
});
|
|
203
|
-
// A failed publication must not prevent the next state change from being sent.
|
|
204
|
-
file.pending = pending.catch(() => undefined);
|
|
205
248
|
return pending;
|
|
206
249
|
}
|
|
250
|
+
|
|
251
|
+
#statsOf(file) {
|
|
252
|
+
file.stats ??= { since: this.#now(), sent: 0, unchanged: 0, zones: [], deliveredMs: [], appliedMs: [] };
|
|
253
|
+
return file.stats;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** What this file's publications cost, said once a minute at most and only when one was asked for. */
|
|
257
|
+
#report(file) {
|
|
258
|
+
const stats = file.stats;
|
|
259
|
+
const now = this.#now();
|
|
260
|
+
if (!this.#log || !stats || now - stats.since < REPORT_EVERY_MS) return;
|
|
261
|
+
const total = values => Math.round(values.reduce((sum, value) => sum + value, 0));
|
|
262
|
+
this.#log(`download map ${file.sourceKey.slice(0, 48)}:${file.fileIndex}: ${stats.sent} sent and ` +
|
|
263
|
+
`${stats.unchanged} unchanged not sent in ${((now - stats.since) / 1000).toFixed(1)}s; ` +
|
|
264
|
+
`zones median ${middle(stats.zones)} max ${largest(stats.zones)}; ` +
|
|
265
|
+
`delivered median ${middle(stats.deliveredMs)}ms max ${largest(stats.deliveredMs)}ms (${total(stats.deliveredMs)}ms in all), ` +
|
|
266
|
+
`applied in the torrent thread median ${middle(stats.appliedMs)}ms max ${largest(stats.appliedMs)}ms ` +
|
|
267
|
+
`(${total(stats.appliedMs)}ms in all)`);
|
|
268
|
+
file.stats = { since: now, sent: 0, unchanged: 0, zones: [], deliveredMs: [], appliedMs: [] };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** How many numbers describe one zone's part in what the torrent does. */
|
|
273
|
+
const ZONE_FIELDS = 7;
|
|
274
|
+
|
|
275
|
+
/** @param {boolean | undefined} value */
|
|
276
|
+
function flag(value) {
|
|
277
|
+
return value === true ? 2 : value === false ? 1 : 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** @param {{ deadlineAt?: number }} zone */
|
|
281
|
+
function deadlineOf(zone) {
|
|
282
|
+
return Number.isFinite(zone.deadlineAt) ? zone.deadlineAt : Number.POSITIVE_INFINITY;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* What the torrent acts on in a publication, packed for comparison: each
|
|
287
|
+
* zone's bytes, priority, words of level, order of claim, and the RANK of its
|
|
288
|
+
* deadline among the map's zones rather than the deadline itself. A zone with
|
|
289
|
+
* no deadline carries the time it is wanted within, which the torrent turns
|
|
290
|
+
* into a deadline on its own clock when the map is applied.
|
|
291
|
+
*
|
|
292
|
+
* @param {{ durationSeconds: number, zones: object[] }} publication
|
|
293
|
+
* @returns {{ durationSeconds: number, fields: Float64Array }}
|
|
294
|
+
*/
|
|
295
|
+
function shapeOf({ durationSeconds, zones }) {
|
|
296
|
+
const byDeadline = zones.map((_zone, index) => index).sort((left, right) => {
|
|
297
|
+
const first = deadlineOf(zones[left]);
|
|
298
|
+
const second = deadlineOf(zones[right]);
|
|
299
|
+
return first === second ? left - right : first < second ? -1 : 1;
|
|
300
|
+
});
|
|
301
|
+
const fields = new Float64Array(zones.length * ZONE_FIELDS);
|
|
302
|
+
byDeadline.forEach((index, rank) => {
|
|
303
|
+
fields[index * ZONE_FIELDS + 5] = rank;
|
|
304
|
+
});
|
|
305
|
+
zones.forEach((zone, index) => {
|
|
306
|
+
const at = index * ZONE_FIELDS;
|
|
307
|
+
fields[at] = Number.isFinite(zone.byteStart) ? zone.byteStart : -1;
|
|
308
|
+
fields[at + 1] = Number.isFinite(zone.byteEnd) ? zone.byteEnd : -1;
|
|
309
|
+
fields[at + 2] = Number.isFinite(zone.priority) ? zone.priority : -1;
|
|
310
|
+
fields[at + 3] = flag(zone.urgent) * 27 + flag(zone.deferred) * 9 + flag(zone.behind) * 3 + flag(zone.downloadOnly);
|
|
311
|
+
fields[at + 4] = Number.isSafeInteger(zone.order) ? zone.order : 0;
|
|
312
|
+
fields[at + 6] = Number.isFinite(zone.deadlineAt) ? -2 : Number.isFinite(zone.withinSeconds) ? zone.withinSeconds : -1;
|
|
313
|
+
});
|
|
314
|
+
return { durationSeconds: Number.isFinite(durationSeconds) ? durationSeconds : -1, fields };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** @param {{ durationSeconds: number, fields: Float64Array }} left @param {{ durationSeconds: number, fields: Float64Array }} right */
|
|
318
|
+
function sameShape(left, right) {
|
|
319
|
+
if (left.durationSeconds !== right.durationSeconds || left.fields.length !== right.fields.length) return false;
|
|
320
|
+
for (let index = 0; index < left.fields.length; index++) {
|
|
321
|
+
if (left.fields[index] !== right.fields[index]) return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @param {number[]} values */
|
|
327
|
+
function middle(values) {
|
|
328
|
+
if (values.length === 0) return "n/a";
|
|
329
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
330
|
+
return Math.round(sorted[Math.floor(sorted.length / 2)]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** @param {number[]} values */
|
|
334
|
+
function largest(values) {
|
|
335
|
+
return values.length === 0 ? "n/a" : Math.round(Math.max(...values));
|
|
207
336
|
}
|