@torrent-tv/proxy 2.107.52 → 2.107.54
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 +8 -6
- package/routes/api/transcode-sessions/net-report/post.js +18 -0
- package/routes/api/transcode-sessions/progress/get.js +9 -1
- package/services/encode/EncodeRun.js +13 -3
- package/services/encode/RunProgress.js +11 -2
- package/services/encode/SpeedReadings.js +10 -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 +235 -317
- package/services/server/wire-outputs.js +17 -0
- package/services/torrent/worker/worker.js +27 -3
- package/services/viewer/OutputForecastGeometry.js +87 -0
- package/services/viewer/Viewer.js +117 -17
- package/services/viewer/ViewerPlaybackState.js +233 -0
- package/services/viewer/playback-readiness.js +25 -3
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
|
|
|
@@ -68,12 +70,10 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
68
70
|
// local lookup. Without the await the reply was the pending promise itself,
|
|
69
71
|
// which serialises to `{}` — the empty stats seen in the field 2026-08-02.
|
|
70
72
|
const statsStartedAt = performance.now();
|
|
71
|
-
let statsCompletedAt;
|
|
72
73
|
let stats;
|
|
73
74
|
try {
|
|
74
75
|
stats = await torrentPool.getFileStats(torrent, fileIndex);
|
|
75
76
|
} finally {
|
|
76
|
-
statsCompletedAt = performance.now();
|
|
77
77
|
logger.info(`[stats] ${sourceKey.slice(0, 8)} getFileStatsMs=${(performance.now() - statsStartedAt).toFixed(1)}`);
|
|
78
78
|
}
|
|
79
79
|
if (stats?.pending || !stats) {
|
|
@@ -155,13 +155,15 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
155
155
|
if (consumerId && Number.isSafeInteger(generation)) {
|
|
156
156
|
viewerRequests?.noteWaitSourceSnapshot?.({ consumerId, generation, sourceKey, fileIndex,
|
|
157
157
|
fileName: torrent.files?.[fileIndex]?.name ?? requestedFileName, fileLength: requestedFileLength,
|
|
158
|
-
stats, ...earlyLink });
|
|
158
|
+
stats, ...earlyLink, ...(viewerToken ? { viewerToken } : {}) });
|
|
159
159
|
}
|
|
160
160
|
const responsePreparedAtProxyMs = performance.now();
|
|
161
161
|
return reply.send({ ...stats, sourceSnapshotTransport: {
|
|
162
|
+
clockEpoch: performance.timeOrigin,
|
|
163
|
+
snapshotClockEpoch: stats.sourceCapture?.clockEpoch ?? null,
|
|
162
164
|
requestReceivedAtProxyMs,
|
|
163
|
-
snapshotStartedAtProxyMs:
|
|
164
|
-
snapshotCompletedAtProxyMs:
|
|
165
|
+
snapshotStartedAtProxyMs: stats.sourceCapture?.validity === "valid" ? stats.sourceCapture.startedAt : null,
|
|
166
|
+
snapshotCompletedAtProxyMs: stats.sourceCapture?.validity === "valid" ? stats.sourceCapture.completedAt : null,
|
|
165
167
|
responsePreparedAtProxyMs
|
|
166
168
|
} });
|
|
167
169
|
}
|
|
@@ -117,12 +117,30 @@ export async function handleApiTranscodeSessionNetReportPost(req, reply,
|
|
|
117
117
|
linkMbps: Number.isFinite(linkMbps) && linkMbps > 0 ? linkMbps : undefined,
|
|
118
118
|
linkSampleMbps: Number.isFinite(linkSampleMbps) && linkSampleMbps > 0 ? linkSampleMbps : undefined,
|
|
119
119
|
linkSampleAt: Number.isFinite(linkSampleAt) && linkSampleAt > 0 ? linkSampleAt : undefined,
|
|
120
|
+
linkSampleAgeMs: Number.isFinite(body.linkSampleAgeMs) && body.linkSampleAgeMs >= 0
|
|
121
|
+
? body.linkSampleAgeMs : undefined,
|
|
120
122
|
appendSessionId: sessionId,
|
|
121
123
|
appendEpoch: typeof body.appendEpoch === "string" ? body.appendEpoch : undefined,
|
|
122
124
|
appendEpochStartedAt: Number.isFinite(body.appendEpochStartedAt) && body.appendEpochStartedAt > 0
|
|
123
125
|
? body.appendEpochStartedAt : undefined,
|
|
124
126
|
appendSnapshotAt: Number.isFinite(body.appendSnapshotAt) && body.appendSnapshotAt > 0
|
|
125
127
|
? body.appendSnapshotAt : undefined,
|
|
128
|
+
captureClockEpoch: Number.isFinite(body.captureClockEpoch) ? body.captureClockEpoch : undefined,
|
|
129
|
+
captureEpoch: typeof body.captureEpoch === "string" ? body.captureEpoch : undefined,
|
|
130
|
+
captureEpochStartedAtBrowser: Number.isFinite(body.captureEpochStartedAtBrowser)
|
|
131
|
+
? body.captureEpochStartedAtBrowser : undefined,
|
|
132
|
+
captureRevision: Number.isSafeInteger(body.captureRevision) ? body.captureRevision : undefined,
|
|
133
|
+
captureStartedAtBrowser: Number.isFinite(body.captureStartedAtBrowser) ? body.captureStartedAtBrowser : undefined,
|
|
134
|
+
captureCompletedAtBrowser: Number.isFinite(body.captureCompletedAtBrowser)
|
|
135
|
+
? body.captureCompletedAtBrowser : undefined,
|
|
136
|
+
linkSampleStartedAtBrowser: Number.isFinite(body.linkSampleStartedAtBrowser)
|
|
137
|
+
? body.linkSampleStartedAtBrowser : undefined,
|
|
138
|
+
linkSampleCompletedAtBrowser: Number.isFinite(body.linkSampleCompletedAtBrowser)
|
|
139
|
+
? body.linkSampleCompletedAtBrowser : undefined,
|
|
140
|
+
linkSampleActionId: typeof body.linkSampleActionId === "string" ? body.linkSampleActionId : undefined,
|
|
141
|
+
linkSampleGeneration: Number.isSafeInteger(body.linkSampleGeneration) ? body.linkSampleGeneration : undefined,
|
|
142
|
+
linkSampleClockEpoch: Number.isFinite(body.linkSampleClockEpoch) ? body.linkSampleClockEpoch : undefined,
|
|
143
|
+
linkSampleRevision: Number.isSafeInteger(body.linkSampleRevision) ? body.linkSampleRevision : undefined,
|
|
126
144
|
appendService: body.appendService && typeof body.appendService === "object" ? body.appendService : undefined,
|
|
127
145
|
fragmentService: body.fragmentService && typeof body.fragmentService === "object" ? body.fragmentService : undefined,
|
|
128
146
|
bufferedAheadSec,
|
|
@@ -19,13 +19,21 @@ export async function handleApiTranscodeSessionsProgressGet(req, reply, { viewer
|
|
|
19
19
|
// quality change the stream on screen is another session — a different one
|
|
20
20
|
// for each viewer who changed.
|
|
21
21
|
const consumerId = typeof req.query?.consumer === "string" ? req.query.consumer : "";
|
|
22
|
-
const
|
|
22
|
+
const stated = Number(req.query?.generation);
|
|
23
|
+
const generation = Number.isSafeInteger(stated) && stated >= 0 ? stated : Number.NaN;
|
|
24
|
+
const progress = await viewerRequests.getSessionProgress(sessionId, consumerId, generation);
|
|
25
|
+
if (progress?.superseded === true) {
|
|
26
|
+
return reply.code(409).send({ error: "Viewer action was superseded." });
|
|
27
|
+
}
|
|
23
28
|
if (!progress) {
|
|
24
29
|
return reply.code(404).send({ error: "Transcode session was not found." });
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
const responsePreparedAtProxyMs = performance.now();
|
|
28
33
|
progress.snapshotTransport = {
|
|
34
|
+
clockEpoch: performance.timeOrigin,
|
|
35
|
+
snapshotClockEpoch: progress.playbackReadiness?.snapshotClockEpoch ?? null,
|
|
36
|
+
generation: Number.isSafeInteger(generation) ? generation : null,
|
|
29
37
|
requestReceivedAtProxyMs,
|
|
30
38
|
snapshotAtProxyMs: progress.playbackReadiness?.snapshotAtProxyMs ?? null,
|
|
31
39
|
responsePreparedAtProxyMs
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
* owns the output. This carries a decision out and reports what happened.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
import { performance } from "node:perf_hooks";
|
|
41
|
+
|
|
40
42
|
import { ENCODE_RUN_EVENT, ENCODE_RUN_STATE, INITIAL_RUN_STATE, nextState } from "./encode-run-state.js";
|
|
41
43
|
import { classifyEncodeExit, ENCODE_EXIT } from "./encode-exit.js";
|
|
42
44
|
import { workDeltaBetween } from "./encoder-readings.js";
|
|
@@ -228,6 +230,7 @@ export class EncodeRun {
|
|
|
228
230
|
* @param {(args: string[]) => import("node:child_process").ChildProcess} params.spawn
|
|
229
231
|
* @param {{ info: (line: string) => void, warn: (line: string) => void, error?: (line: string) => void }} params.logger
|
|
230
232
|
* @param {() => number} [params.now]
|
|
233
|
+
* @param {() => number} [params.monotonicNow]
|
|
231
234
|
* @param {(ended: RunEnded) => void} [params.onEnded]
|
|
232
235
|
* @param {(reading: { speed: number, at: number, workSeconds: number, workingSeconds: number }) => void} [params.onSpeedMeasured] -
|
|
233
236
|
* Told each time this run has measured its processing speed, once per
|
|
@@ -291,6 +294,7 @@ export class EncodeRun {
|
|
|
291
294
|
indexOfName,
|
|
292
295
|
startSeconds = 0,
|
|
293
296
|
totalSeconds = null,
|
|
297
|
+
monotonicNow,
|
|
294
298
|
because = "no reason was given"
|
|
295
299
|
}) {
|
|
296
300
|
this.address = address;
|
|
@@ -302,10 +306,11 @@ export class EncodeRun {
|
|
|
302
306
|
this.spawnProcess = spawn;
|
|
303
307
|
this.logger = logger;
|
|
304
308
|
this.now = typeof now === "function" ? now : Date.now;
|
|
305
|
-
this.
|
|
309
|
+
this.monotonicNow = typeof monotonicNow === "function" ? monotonicNow : () => performance.now();
|
|
310
|
+
this.progress = new RunProgress({ startSeconds, totalSeconds, now: this.now, monotonicNow: this.monotonicNow });
|
|
306
311
|
/** Where this run's time went: its own work, or waiting, or stopped. */
|
|
307
312
|
this.clock = new RunClock({ now: this.now });
|
|
308
|
-
this.#pieceSample = { at: this.now(), producedSeconds: 0, workingMs: 0 };
|
|
313
|
+
this.#pieceSample = { at: this.now(), monotonicAt: this.monotonicNow(), producedSeconds: 0, workingMs: 0 };
|
|
309
314
|
this.onEnded = typeof onEnded === "function" ? onEnded : () => {};
|
|
310
315
|
this.onProgress = typeof onProgress === "function" ? onProgress : () => {};
|
|
311
316
|
this.onProduced = typeof onProduced === "function" ? onProduced : () => {};
|
|
@@ -463,6 +468,7 @@ export class EncodeRun {
|
|
|
463
468
|
this.#pieceSample = {
|
|
464
469
|
...this.#workSample,
|
|
465
470
|
at: this.now(),
|
|
471
|
+
monotonicAt: this.monotonicNow(),
|
|
466
472
|
workingMs: this.clock.workingMs()
|
|
467
473
|
};
|
|
468
474
|
}
|
|
@@ -600,6 +606,7 @@ export class EncodeRun {
|
|
|
600
606
|
const at = this.now();
|
|
601
607
|
this.#workSample = {
|
|
602
608
|
at,
|
|
609
|
+
monotonicAt: this.progress.updatedAtMonotonic,
|
|
603
610
|
producedSeconds: Math.max(0, this.progress.processedSeconds - this.progress.startPositionSeconds),
|
|
604
611
|
workingMs: this.clock.workingMs(at)
|
|
605
612
|
};
|
|
@@ -766,7 +773,10 @@ export class EncodeRun {
|
|
|
766
773
|
}
|
|
767
774
|
const work = workDeltaBetween(this.#pieceSample, sample);
|
|
768
775
|
if (work !== null) {
|
|
769
|
-
this.#speedReading = { speed: work.workSeconds / work.workingSeconds, at: sample.at, ...work
|
|
776
|
+
this.#speedReading = { speed: work.workSeconds / work.workingSeconds, at: sample.at, ...work,
|
|
777
|
+
captureOwner: "proxy-encoder", captureClock: "performance", captureClockEpoch: performance.timeOrigin,
|
|
778
|
+
captureStartedAtMonotonic: this.#pieceSample.monotonicAt,
|
|
779
|
+
captureCompletedAtMonotonic: sample.monotonicAt };
|
|
770
780
|
this.#pieceSample = sample;
|
|
771
781
|
this.#speedMeasurementPending = false;
|
|
772
782
|
this.onSpeedMeasured(this.speedReading);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @file Progress reported by one encoder run, placed on the source timeline.
|
|
3
5
|
*
|
|
@@ -22,15 +24,18 @@ function parseTimestamp(value) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export class RunProgress {
|
|
25
|
-
constructor({ startSeconds = 0, totalSeconds = null, now = Date.now } = {}) {
|
|
27
|
+
constructor({ startSeconds = 0, totalSeconds = null, now = Date.now, monotonicNow = () => performance.now() } = {}) {
|
|
26
28
|
this.startPositionSeconds = Number.isFinite(startSeconds) ? Math.max(0, startSeconds) : 0;
|
|
27
29
|
this.processedSeconds = this.startPositionSeconds;
|
|
28
30
|
this.totalSeconds = Number.isFinite(totalSeconds) && totalSeconds > 0 ? totalSeconds : null;
|
|
29
31
|
this.percent = null;
|
|
30
32
|
this.remainingSeconds = this.totalSeconds;
|
|
31
33
|
this.updatedAt = now();
|
|
34
|
+
this.updatedAtMonotonic = monotonicNow();
|
|
35
|
+
this.clockEpoch = performance.timeOrigin;
|
|
32
36
|
this.lastLoggedAt = 0;
|
|
33
37
|
this.now = now;
|
|
38
|
+
this.monotonicNow = monotonicNow;
|
|
34
39
|
this.#derive();
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -42,6 +47,7 @@ export class RunProgress {
|
|
|
42
47
|
this.processedSeconds = this.startPositionSeconds + Math.max(0, relative);
|
|
43
48
|
}
|
|
44
49
|
this.updatedAt = this.now();
|
|
50
|
+
this.updatedAtMonotonic = this.monotonicNow();
|
|
45
51
|
this.#derive();
|
|
46
52
|
return this.snapshot();
|
|
47
53
|
}
|
|
@@ -61,7 +67,9 @@ export class RunProgress {
|
|
|
61
67
|
totalSeconds: this.totalSeconds,
|
|
62
68
|
percent: this.percent,
|
|
63
69
|
remainingSeconds: this.remainingSeconds,
|
|
64
|
-
updatedAt: this.updatedAt
|
|
70
|
+
updatedAt: this.updatedAt,
|
|
71
|
+
updatedAtMonotonic: this.updatedAtMonotonic,
|
|
72
|
+
clockEpoch: this.clockEpoch
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -77,3 +85,4 @@ export class RunProgress {
|
|
|
77
85
|
this.remainingSeconds = Math.max(0, this.totalSeconds - this.processedSeconds);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
88
|
+
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export class SpeedReadings {
|
|
17
|
-
/** @type {WeakMap<object, Array<{ speed: number, at: number, workSeconds?: number, workingSeconds?: number
|
|
17
|
+
/** @type {WeakMap<object, Array<{ speed: number, at: number, workSeconds?: number, workingSeconds?: number,
|
|
18
|
+
* captureOwner?: string, captureClock?: string, captureClockEpoch?: number,
|
|
19
|
+
* captureStartedAtMonotonic?: number, captureCompletedAtMonotonic?: number }>>} */
|
|
18
20
|
#byOutput = new WeakMap();
|
|
19
21
|
#keepMs;
|
|
20
22
|
|
|
@@ -25,7 +27,9 @@ export class SpeedReadings {
|
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* @param {object} output
|
|
28
|
-
* @param {{ speed: number, at: number, workSeconds?: number, workingSeconds?: number
|
|
30
|
+
* @param {{ speed: number, at: number, workSeconds?: number, workingSeconds?: number,
|
|
31
|
+
* captureOwner?: string, captureClock?: string, captureClockEpoch?: number,
|
|
32
|
+
* captureStartedAtMonotonic?: number, captureCompletedAtMonotonic?: number } | null} reading
|
|
29
33
|
*/
|
|
30
34
|
note(output, reading) {
|
|
31
35
|
if (!output || !(reading?.speed > 0) || !Number.isFinite(reading.at)) return;
|
|
@@ -34,6 +38,10 @@ export class SpeedReadings {
|
|
|
34
38
|
let place = readings.length;
|
|
35
39
|
while (place > 0 && readings[place - 1].at > reading.at) place -= 1;
|
|
36
40
|
const sample = { speed: reading.speed, at: reading.at };
|
|
41
|
+
for (const key of ["captureOwner", "captureClock", "captureClockEpoch", "captureStartedAtMonotonic",
|
|
42
|
+
"captureCompletedAtMonotonic"]) {
|
|
43
|
+
if (reading[key] !== undefined) sample[key] = reading[key];
|
|
44
|
+
}
|
|
37
45
|
if (Number.isFinite(reading.workSeconds) && reading.workSeconds > 0 &&
|
|
38
46
|
Number.isFinite(reading.workingSeconds) && reading.workingSeconds > 0) {
|
|
39
47
|
Object.assign(sample, { workSeconds: reading.workSeconds, workingSeconds: reading.workingSeconds });
|
|
@@ -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
|
}
|