@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
package/package.json
CHANGED
|
@@ -33,6 +33,11 @@ export async function handleApiSourceFilesGet(req, reply, { sourceRegistry, torr
|
|
|
33
33
|
return reply.code(404).send({ error: "Source key was not found." });
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const consumerId = typeof req.query?.consumerId === "string" ? req.query.consumerId.trim() : "";
|
|
37
|
+
const generationRaw = typeof req.query?.generation === "string" ? req.query.generation : "";
|
|
38
|
+
const generation = generationRaw !== "" && /^\d+$/.test(generationRaw) ? Number(generationRaw) : null;
|
|
39
|
+
const viewerToken = viewerRequests?.viewerTokenOf?.(consumerId) ?? null;
|
|
40
|
+
|
|
36
41
|
let cancelled;
|
|
37
42
|
const waitPromise = new Promise(resolve => { cancelled = () => resolve(CANCELLED); });
|
|
38
43
|
reply.raw?.once?.("close", cancelled);
|
|
@@ -71,10 +76,7 @@ export async function handleApiSourceFilesGet(req, reply, { sourceRegistry, torr
|
|
|
71
76
|
if (file.excludedReason) logger.info(`contents request=${req.id ?? "unstated"} source=${sourceKey} ` +
|
|
72
77
|
`file=${file.fileIndex} name=${JSON.stringify(file.relativePath)} bytes=${file.length} excluded=${file.excludedReason}`);
|
|
73
78
|
}
|
|
74
|
-
const consumerId = typeof req.query?.consumerId === "string" ? req.query.consumerId.trim() : "";
|
|
75
79
|
if (consumerId) viewers?.visibleFiles(consumerId, sourceKey, contents.items.map(item => item.fileIndex));
|
|
76
|
-
const generationRaw = typeof req.query?.generation === "string" ? req.query.generation : "";
|
|
77
|
-
const generation = generationRaw !== "" && /^\d+$/.test(generationRaw) ? Number(generationRaw) : null;
|
|
78
80
|
const linkMbps = Number(req.query?.linkMbps);
|
|
79
81
|
const linkSampleAgeMs = Number(req.query?.linkSampleAgeMs);
|
|
80
82
|
const earlyLink = Number.isFinite(linkMbps) && linkMbps > 0
|
|
@@ -84,7 +86,7 @@ export async function handleApiSourceFilesGet(req, reply, { sourceRegistry, torr
|
|
|
84
86
|
if (!consumerId || !Number.isSafeInteger(generation) || file.kind !== "video" || file.excludedReason) return file;
|
|
85
87
|
viewerRequests?.noteWaitFileSnapshot?.({ consumerId, generation, sourceKey,
|
|
86
88
|
fileIndex: file.fileIndex, fileName: file.relativePath ?? file.name, fileLength: file.length,
|
|
87
|
-
...earlyLink });
|
|
89
|
+
...earlyLink, ...(viewerToken ? { viewerToken } : {}) });
|
|
88
90
|
return file;
|
|
89
91
|
});
|
|
90
92
|
return reply.send({
|
|
@@ -34,6 +34,7 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
34
34
|
const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
|
|
35
35
|
const fileIndex = fileIndexRaw !== "" && /^\d+$/.test(fileIndexRaw) ? Number(fileIndexRaw) : null;
|
|
36
36
|
const consumerId = typeof req.query.consumer === "string" ? req.query.consumer : "";
|
|
37
|
+
const viewerToken = viewerRequests?.viewerTokenOf?.(consumerId) ?? null;
|
|
37
38
|
const generationRaw = typeof req.query.generation === "string" ? req.query.generation : "";
|
|
38
39
|
const generation = generationRaw !== "" && /^\d+$/.test(generationRaw) ? Number(generationRaw) : null;
|
|
39
40
|
const requestedFileName = typeof req.query.fileName === "string" ? req.query.fileName : "";
|
|
@@ -47,7 +48,8 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
47
48
|
const noteInitialSnapshot = () => {
|
|
48
49
|
if (consumerId && Number.isSafeInteger(generation) && Number.isSafeInteger(fileIndex)) {
|
|
49
50
|
viewerRequests?.noteWaitFileSnapshot?.({ consumerId, generation, sourceKey, fileIndex,
|
|
50
|
-
fileName: requestedFileName, fileLength: requestedFileLength, ...earlyLink
|
|
51
|
+
fileName: requestedFileName, fileLength: requestedFileLength, ...earlyLink,
|
|
52
|
+
...(viewerToken ? { viewerToken } : {}) });
|
|
51
53
|
}
|
|
52
54
|
};
|
|
53
55
|
|
|
@@ -155,7 +157,7 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
155
157
|
if (consumerId && Number.isSafeInteger(generation)) {
|
|
156
158
|
viewerRequests?.noteWaitSourceSnapshot?.({ consumerId, generation, sourceKey, fileIndex,
|
|
157
159
|
fileName: torrent.files?.[fileIndex]?.name ?? requestedFileName, fileLength: requestedFileLength,
|
|
158
|
-
stats, ...earlyLink });
|
|
160
|
+
stats, ...earlyLink, ...(viewerToken ? { viewerToken } : {}) });
|
|
159
161
|
}
|
|
160
162
|
const responsePreparedAtProxyMs = performance.now();
|
|
161
163
|
return reply.send({ ...stats, sourceSnapshotTransport: {
|
|
@@ -325,7 +325,7 @@ export class HostLoad {
|
|
|
325
325
|
* What the swarm can give every torrent, in bytes per second: the median of
|
|
326
326
|
* what it carried while our selection held no connection back, but never
|
|
327
327
|
* less than the median of what it carried in all — the same choice the
|
|
328
|
-
* start forecast makes for one source (`
|
|
328
|
+
* start forecast makes for one source (`ViewerPlaybackState` `sourceService`).
|
|
329
329
|
* Time our selection held a connection back measures our asking, not the
|
|
330
330
|
* swarm, so holding the speculative levels back does not lower it
|
|
331
331
|
* (torrent-tv/meta#180). Null before any interval was read.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Aggregates playback-forecast work for the existing once-a-minute log. */
|
|
2
|
+
export class ForecastDiagnostics {
|
|
3
|
+
#logger;
|
|
4
|
+
#costs = { since: Date.now(), polls: 0, ms: 0, maxMs: 0, times: new Map() };
|
|
5
|
+
|
|
6
|
+
constructor({ logger }) {
|
|
7
|
+
this.#logger = logger;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
note(ms, reasons) {
|
|
11
|
+
const costs = this.#costs;
|
|
12
|
+
costs.polls += 1;
|
|
13
|
+
costs.ms += ms;
|
|
14
|
+
costs.maxMs = Math.max(costs.maxMs, ms);
|
|
15
|
+
for (const reason of reasons) costs.times.set(reason, (costs.times.get(reason) ?? 0) + 1);
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
if (now - costs.since < 60_000) return;
|
|
18
|
+
this.#logger.info(`start forecasts in ${((now - costs.since) / 1000).toFixed(1)}s: ${costs.polls}, ` +
|
|
19
|
+
`${Math.round(costs.ms)}ms in all, ${(costs.ms / costs.polls).toFixed(1)}ms each, longest ${Math.round(costs.maxMs)}ms; ` +
|
|
20
|
+
`times of a track's pieces: ${[...costs.times].map(([reason, count]) => `${reason} ${count}`).join(", ")}`);
|
|
21
|
+
Object.assign(costs, { since: now, polls: 0, ms: 0, maxMs: 0, times: new Map() });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -73,6 +73,7 @@ export class OutputLifecycle {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
this.#host.invalidateWaits(session);
|
|
76
|
+
this.#host.forgetForecastOutput?.(session);
|
|
76
77
|
this.#host.outputs.delete(sessionId);
|
|
77
78
|
this.#host.outputTimes.logIndexAccuracy(session);
|
|
78
79
|
|
|
@@ -406,7 +407,9 @@ export class OutputLifecycle {
|
|
|
406
407
|
}
|
|
407
408
|
// Presence ends synchronously, before disposal waits for a child process.
|
|
408
409
|
// Late reports and preparations must not rejoin this viewer meanwhile.
|
|
410
|
+
const viewer = this.#host.viewers.get(consumerId);
|
|
409
411
|
const outputs = this.#host.viewers.hasGone(consumerId);
|
|
412
|
+
this.#host.viewerDeparted?.(viewer);
|
|
410
413
|
for (const outputId of outputs) {
|
|
411
414
|
await this.releaseSessionConsumer(outputId, consumerId, because);
|
|
412
415
|
}
|
|
@@ -428,11 +431,14 @@ export class OutputLifecycle {
|
|
|
428
431
|
if (!output) {
|
|
429
432
|
return false;
|
|
430
433
|
}
|
|
434
|
+
const viewer = this.#host.viewers.get(consumerId);
|
|
431
435
|
this.#host.releasePlaybackStorageDemand?.(output.outputKey ?? "", consumerId);
|
|
432
436
|
// Nothing to release in the encoding: it holds one map per output, built
|
|
433
437
|
// from where the viewers are, and the map that arrives next simply does not
|
|
434
438
|
// have this one in it. A name to release was the last place a viewer
|
|
435
439
|
// appeared inside the encoding at all.
|
|
436
|
-
|
|
440
|
+
const removed = this.#host.viewers.leaves(output, consumerId);
|
|
441
|
+
if (removed) this.#host.outputViewerDeparted?.(viewer, output);
|
|
442
|
+
return removed;
|
|
437
443
|
}
|
|
438
444
|
}
|