@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.51",
3
+ "version": "2.107.53",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -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 (`ViewerRequests` `sourceService`).
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
- return this.#host.viewers.leaves(output, consumerId);
440
+ const removed = this.#host.viewers.leaves(output, consumerId);
441
+ if (removed) this.#host.outputViewerDeparted?.(viewer, output);
442
+ return removed;
437
443
  }
438
444
  }