@torrent-tv/proxy 2.85.0 → 2.85.1
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 +6 -0
- package/package.json +1 -1
- package/routes/api/transcode-sessions/net-report/post.js +22 -12
- package/server.js +7 -1
- package/services/hls-session-manager.js +14 -84
- package/services/viewer/Viewer.js +34 -0
- package/services/viewer/active-output.js +54 -0
- package/services/viewer/report-intake.js +44 -63
- package/test/auto-quality-step.test.js +518 -514
- package/test/viewer-states.test.js +45 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.85.1
|
|
2
|
+
|
|
3
|
+
- **Chore**: NOTHING IS ADDED TO THE SESSION MANAGER, and 2.85.0 added a delegation and an import to it. Both are gone, and what they were there for went with them: taking in a viewer's report is the VIEWER layer's own operation now (`services/viewer/report-intake.js`), the route calls it directly with the live sessions and the registry of viewers, and the manager has no method for it at all. Which output a viewer has on screen moved out with it (`services/viewer/active-output.js`) — it is a question about a person, not about an encoder, and eight call sites now ask it outward.
|
|
4
|
+
- **Chore**: HOW LONG A LINK READING DESCRIBES A LINK is answered by the reading, once (`Viewer.linkReading`). It was a constant in the session manager consulted in four places, and three of those were conditions over a figure that had already passed that very filter — unreachable by construction. Nothing nulls a stale reading any more either: expiry is answered when it is read, so the write that existed only to make a later read correct is gone, and a viewer who has simply stopped reporting is untouched.
|
|
5
|
+
- **Chore**: The session manager is 103 lines shorter for it, and gained one import and eight calls outward.
|
|
6
|
+
|
|
1
7
|
## 2.85.0
|
|
2
8
|
|
|
3
9
|
- **Fix**: A VIEWER WHO HAS SAID NOTHING IS NOT ASSUMED TO BE WATCHING. `playing` started true on a viewer this proxy had never heard from, and the position is carried forward by the clock at that rate — so a page whose tab was hidden for 145 seconds, while it deliberately held start-up until it was shown, was placed **145.979 s** into a film it had not begun. Field 2026-09-14: that is the exact `-ss` the soundtrack's encoder was given, the browser was asking for segment #0, the map ranked #0 last of a hundred (`p1:#0..#13`), and the request was held 60 145 ms and answered 503. The moment the viewer left and the invented clock stopped, the plan asked for exactly the segment they had been asking for all along. A statement about somebody else's machine is theirs to make: until they make it the rate is zero.
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recordViewerReport } from "../../../../services/viewer/report-intake.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Accept a viewer link report for a transcode session (adaptive bitrate).
|
|
3
5
|
* The browser measures its own data-channel throughput per segment fetch and
|
|
@@ -17,10 +19,13 @@
|
|
|
17
19
|
*
|
|
18
20
|
* @param {import("fastify").FastifyRequest} req
|
|
19
21
|
* @param {import("fastify").FastifyReply} reply
|
|
20
|
-
* @param {{
|
|
22
|
+
* @param {{ sessions: { get: (id: string) => object | undefined }, viewers: object }} deps -
|
|
23
|
+
* The live sessions and the registry of viewers. The VIEWER layer takes the
|
|
24
|
+
* statement from here; this route's whole job is turning a request into that
|
|
25
|
+
* one call and its answer into a status code.
|
|
21
26
|
* @returns {Promise<void>}
|
|
22
27
|
*/
|
|
23
|
-
export async function handleApiTranscodeSessionNetReportPost(req, reply, {
|
|
28
|
+
export async function handleApiTranscodeSessionNetReportPost(req, reply, { sessions, viewers }) {
|
|
24
29
|
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
25
30
|
const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
|
|
26
31
|
// WHAT THIS REPORT IS. A statement by one viewer about itself: where the
|
|
@@ -66,16 +71,21 @@ export async function handleApiTranscodeSessionNetReportPost(req, reply, { hlsSe
|
|
|
66
71
|
const inPictureInPicture =
|
|
67
72
|
typeof body.inPictureInPicture === "boolean" ? body.inPictureInPicture : undefined;
|
|
68
73
|
const positionSeconds = Number(body.positionSeconds);
|
|
69
|
-
const recorded =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
const recorded = recordViewerReport({
|
|
75
|
+
sessions,
|
|
76
|
+
viewers,
|
|
77
|
+
sessionId,
|
|
78
|
+
report: {
|
|
79
|
+
linkMbps: Number.isFinite(linkMbps) && linkMbps > 0 ? linkMbps : undefined,
|
|
80
|
+
bufferedAheadSec,
|
|
81
|
+
consumerId,
|
|
82
|
+
playing,
|
|
83
|
+
waiting,
|
|
84
|
+
onScreen,
|
|
85
|
+
inPictureInPicture,
|
|
86
|
+
positionSeconds:
|
|
87
|
+
Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : undefined
|
|
88
|
+
}
|
|
79
89
|
});
|
|
80
90
|
if (!recorded) {
|
|
81
91
|
return reply.code(404).send({ error: "Transcode session was not found." });
|
package/server.js
CHANGED
|
@@ -393,7 +393,13 @@ export async function startProxyServer({
|
|
|
393
393
|
handleApiTranscodeSessionsProgressGet(req, reply, { hlsSessionManager })
|
|
394
394
|
);
|
|
395
395
|
app.post("/api/transcode-sessions/:sessionId/net-report", async (req, reply) =>
|
|
396
|
-
|
|
396
|
+
// A viewer's statement about itself goes to the VIEWER layer, not through
|
|
397
|
+
// the session manager: what it needs is the live sessions and the registry
|
|
398
|
+
// of viewers, and nothing about encoding.
|
|
399
|
+
handleApiTranscodeSessionNetReportPost(req, reply, {
|
|
400
|
+
sessions: hlsSessionManager.sessionsById,
|
|
401
|
+
viewers: hlsSessionManager.viewers
|
|
402
|
+
})
|
|
397
403
|
);
|
|
398
404
|
app.post("/api/transcode-sessions/:sessionId/fragment-far", async (req, reply) =>
|
|
399
405
|
handleApiTranscodeSessionFragmentFarPost(req, reply, { hlsSessionManager })
|
|
@@ -88,8 +88,8 @@ import {
|
|
|
88
88
|
// — did not move when the code did.
|
|
89
89
|
export { ffmpegSeconds, onKeyframeGridFor, seekLandingOffsetFor, segmentCutTimesFrom };
|
|
90
90
|
import { viewersOf } from "./viewer/Viewer.js";
|
|
91
|
+
import { activeOutputFor } from "./viewer/active-output.js";
|
|
91
92
|
import { earliestViewerSecondsOn, viewerSecondsOn, viewerSegmentsOn } from "./viewer/positions.js";
|
|
92
|
-
import { takeViewerReport } from "./viewer/report-intake.js";
|
|
93
93
|
import { Viewers } from "./viewer/Viewers.js";
|
|
94
94
|
import { LiveOutputs } from "./output/LiveOutputs.js";
|
|
95
95
|
import { variantHeightsFor } from "./output/ladder.js";
|
|
@@ -524,7 +524,6 @@ const BUDGET_DOWNLOAD_OK_FACTOR = 1.0;
|
|
|
524
524
|
// instead. Which of the two, and whether a viewer's own pick may be moved at
|
|
525
525
|
// all, is decided where the viewer's choice lives: in the browser, which
|
|
526
526
|
// honours the request only in automatic mode.
|
|
527
|
-
const LINK_REPORT_FRESH_MS = 30_000;
|
|
528
527
|
// Usable share of the reported link (protocol overhead + measurement noise).
|
|
529
528
|
const LINK_SAFETY = 0.8;
|
|
530
529
|
// Deficit must persist this long before acting (absorbs one slow segment).
|
|
@@ -3058,36 +3057,6 @@ export class HlsSessionManager {
|
|
|
3058
3057
|
*
|
|
3059
3058
|
* @returns {Promise<void>}
|
|
3060
3059
|
*/
|
|
3061
|
-
/**
|
|
3062
|
-
* Record the latest viewer link report for a session (adaptive bitrate).
|
|
3063
|
-
* Returns false for an unknown/disposed session.
|
|
3064
|
-
*
|
|
3065
|
-
* Kept per viewer. A browser that does not say who it is lands under one
|
|
3066
|
-
* shared key, which is exactly the old behaviour for that browser and no
|
|
3067
|
-
* worse: with one viewer the two are the same thing.
|
|
3068
|
-
*
|
|
3069
|
-
* @param {string} sessionId
|
|
3070
|
-
* @param {{ linkMbps: number, bufferedAheadSec: number, consumerId?: string, positionSeconds?: number }} report
|
|
3071
|
-
* @returns {boolean}
|
|
3072
|
-
*/
|
|
3073
|
-
recordNetReport(sessionId, report) {
|
|
3074
|
-
const named = this.sessionsById.get(sessionId);
|
|
3075
|
-
if (!named || named.state === "disposed") {
|
|
3076
|
-
return false;
|
|
3077
|
-
}
|
|
3078
|
-
const consumerId = typeof report?.consumerId === "string" ? report.consumerId : "";
|
|
3079
|
-
return takeViewerReport({
|
|
3080
|
-
viewers: this.viewers,
|
|
3081
|
-
// The stream on screen is the reporter's own: with two viewers on two
|
|
3082
|
-
// rungs, one report says nothing about the other's encoder.
|
|
3083
|
-
session: this.#activeVariant(named, consumerId),
|
|
3084
|
-
consumerId,
|
|
3085
|
-
report,
|
|
3086
|
-
now: Date.now(),
|
|
3087
|
-
linkFreshMs: LINK_REPORT_FRESH_MS
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
3060
|
/**
|
|
3092
3061
|
* Record where one viewer of this session is, and answer with the furthest
|
|
3093
3062
|
* any of them has reached.
|
|
@@ -3135,8 +3104,8 @@ export class HlsSessionManager {
|
|
|
3135
3104
|
#worstNetReport(session, now) {
|
|
3136
3105
|
let worst = null;
|
|
3137
3106
|
for (const viewer of viewersOf(session).values()) {
|
|
3138
|
-
const report = viewer.
|
|
3139
|
-
if (report === null
|
|
3107
|
+
const report = viewer.linkReading(now);
|
|
3108
|
+
if (report === null) {
|
|
3140
3109
|
continue;
|
|
3141
3110
|
}
|
|
3142
3111
|
if (worst === null) {
|
|
@@ -3186,7 +3155,7 @@ export class HlsSessionManager {
|
|
|
3186
3155
|
// run and its own position, and that is exactly the pair this report exists
|
|
3187
3156
|
// to tell apart. Answering an audio report from the picture's records would
|
|
3188
3157
|
// state, confidently, something about the wrong stream.
|
|
3189
|
-
const onScreen =
|
|
3158
|
+
const onScreen = activeOutputFor({ base: named, sessions: this.sessionsById, viewers: this.viewers });
|
|
3190
3159
|
const session = track === "audio"
|
|
3191
3160
|
? ([...this.liveOutputs.familyOf(onScreen)].find((member) => member.audioOnly === true) ?? onScreen)
|
|
3192
3161
|
: onScreen;
|
|
@@ -3275,7 +3244,7 @@ export class HlsSessionManager {
|
|
|
3275
3244
|
*/
|
|
3276
3245
|
async #checkLinkBudget(session, now) {
|
|
3277
3246
|
const report = this.#worstNetReport(session, now);
|
|
3278
|
-
if (!report
|
|
3247
|
+
if (!report) {
|
|
3279
3248
|
session.linkSlowSince = 0; // no fresh data — old clients / stopped reporter
|
|
3280
3249
|
return false;
|
|
3281
3250
|
}
|
|
@@ -4666,7 +4635,7 @@ export class HlsSessionManager {
|
|
|
4666
4635
|
}
|
|
4667
4636
|
return false;
|
|
4668
4637
|
}
|
|
4669
|
-
const playing = this.liveOutputs.variantHeightOf(
|
|
4638
|
+
const playing = this.liveOutputs.variantHeightOf(activeOutputFor({ base, sessions: this.sessionsById, viewers: this.viewers }));
|
|
4670
4639
|
if (height === playing) {
|
|
4671
4640
|
return false;
|
|
4672
4641
|
}
|
|
@@ -4765,7 +4734,7 @@ export class HlsSessionManager {
|
|
|
4765
4734
|
return false;
|
|
4766
4735
|
}
|
|
4767
4736
|
const report = this.#worstNetReport(session, now);
|
|
4768
|
-
if (!report
|
|
4737
|
+
if (!report) {
|
|
4769
4738
|
// Nothing fresh measures the link, so it has no opinion either way — the
|
|
4770
4739
|
// same silence that stops #checkLinkBudget from acting.
|
|
4771
4740
|
return true;
|
|
@@ -4793,7 +4762,7 @@ export class HlsSessionManager {
|
|
|
4793
4762
|
// The SLOWEST link among the viewers: a step up has to be carried by all of
|
|
4794
4763
|
// them, not by whichever reported last.
|
|
4795
4764
|
const report = this.#worstNetReport(session, now);
|
|
4796
|
-
if (!report
|
|
4765
|
+
if (!report) {
|
|
4797
4766
|
return true;
|
|
4798
4767
|
}
|
|
4799
4768
|
return report.linkMbps * LINK_SAFETY >= wantedMbps;
|
|
@@ -7167,7 +7136,7 @@ export class HlsSessionManager {
|
|
|
7167
7136
|
if (!ask) {
|
|
7168
7137
|
return 0;
|
|
7169
7138
|
}
|
|
7170
|
-
if (ask.height === this.liveOutputs.variantHeightOf(
|
|
7139
|
+
if (ask.height === this.liveOutputs.variantHeightOf(activeOutputFor({ base, sessions: this.sessionsById, viewers: this.viewers }))) {
|
|
7171
7140
|
base.qualityAsk = null; // the viewer is there; nothing left to ask for
|
|
7172
7141
|
return 0;
|
|
7173
7142
|
}
|
|
@@ -7182,45 +7151,6 @@ export class HlsSessionManager {
|
|
|
7182
7151
|
return ask.height;
|
|
7183
7152
|
}
|
|
7184
7153
|
|
|
7185
|
-
/**
|
|
7186
|
-
* The variant of a session that the viewer is watching right now.
|
|
7187
|
-
*
|
|
7188
|
-
* Every request that names the base session — seek, progress, link report,
|
|
7189
|
-
* release — means the stream the viewer has on screen, and after a quality
|
|
7190
|
-
* change that is another session. The browser is not told about the swap: it
|
|
7191
|
-
* holds one session id for the whole file, which is what keeps the switch out
|
|
7192
|
-
* of the state machine on that side.
|
|
7193
|
-
*
|
|
7194
|
-
* Kept per viewer, because one picture is shared by everyone watching it and
|
|
7195
|
-
* a quality step is a session of its own: with one answer for the session, a
|
|
7196
|
-
* step taken by one viewer would move the other one's stream, and that
|
|
7197
|
-
* viewer's next seek would be forwarded to a rung they never chose. The
|
|
7198
|
-
* session's own field remains the answer for a viewer who cannot name
|
|
7199
|
-
* themselves, and the last one anybody moved to.
|
|
7200
|
-
*
|
|
7201
|
-
* @param {HlsSession} base
|
|
7202
|
-
* @param {string} [consumerId]
|
|
7203
|
-
* @returns {HlsSession}
|
|
7204
|
-
*/
|
|
7205
|
-
#activeVariant(base, consumerId = "") {
|
|
7206
|
-
const named = consumerId ? base.viewers?.get(consumerId)?.activeVariantId ?? null : null;
|
|
7207
|
-
const activeId = named ?? base.activeVariantId;
|
|
7208
|
-
if (!activeId || activeId === base.id) {
|
|
7209
|
-
return base;
|
|
7210
|
-
}
|
|
7211
|
-
const active = this.sessionsById.get(activeId);
|
|
7212
|
-
if (!active || active.state === "disposed") {
|
|
7213
|
-
if (named) {
|
|
7214
|
-
this.viewers.of(base, consumerId).activeVariantId = null;
|
|
7215
|
-
}
|
|
7216
|
-
if (base.activeVariantId === activeId) {
|
|
7217
|
-
base.activeVariantId = base.id;
|
|
7218
|
-
}
|
|
7219
|
-
return base;
|
|
7220
|
-
}
|
|
7221
|
-
return active;
|
|
7222
|
-
}
|
|
7223
|
-
|
|
7224
7154
|
/**
|
|
7225
7155
|
* Every session of this family that a live viewer has on screen.
|
|
7226
7156
|
*
|
|
@@ -7245,7 +7175,7 @@ export class HlsSessionManager {
|
|
|
7245
7175
|
onScreen.add(viewer.activeVariantId);
|
|
7246
7176
|
}
|
|
7247
7177
|
if (onScreen.size === 0) {
|
|
7248
|
-
onScreen.add(
|
|
7178
|
+
onScreen.add(activeOutputFor({ base, sessions: this.sessionsById, viewers: this.viewers }).id);
|
|
7249
7179
|
}
|
|
7250
7180
|
return onScreen;
|
|
7251
7181
|
}
|
|
@@ -7278,7 +7208,7 @@ export class HlsSessionManager {
|
|
|
7278
7208
|
if (Number.isInteger(wantedIndex) && wantedIndex >= 0) {
|
|
7279
7209
|
return this.#segmentStartTime(base, wantedIndex);
|
|
7280
7210
|
}
|
|
7281
|
-
return viewerSecondsOn(
|
|
7211
|
+
return viewerSecondsOn(activeOutputFor({ base, consumerId, sessions: this.sessionsById, viewers: this.viewers }));
|
|
7282
7212
|
}
|
|
7283
7213
|
|
|
7284
7214
|
/**
|
|
@@ -7883,7 +7813,7 @@ export class HlsSessionManager {
|
|
|
7883
7813
|
// buys it an encoder, and both halves are said here: a warmed rung is one
|
|
7884
7814
|
// this person is watching for as long as the warm-up lasts, which is why
|
|
7885
7815
|
// two encoders run through it.
|
|
7886
|
-
if (variant.id !==
|
|
7816
|
+
if (variant.id !== activeOutputFor({ base, consumerId, sessions: this.sessionsById, viewers: this.viewers }).id) {
|
|
7887
7817
|
this.viewers.of(variant, consumerId).moveTo(this.#segmentStartTime(base, index));
|
|
7888
7818
|
this.planEncodersSoon();
|
|
7889
7819
|
}
|
|
@@ -7908,7 +7838,7 @@ export class HlsSessionManager {
|
|
|
7908
7838
|
* @returns {void}
|
|
7909
7839
|
*/
|
|
7910
7840
|
#noteVariantActive(base, variant, wantedIndex = -1, consumerId = "") {
|
|
7911
|
-
const previous =
|
|
7841
|
+
const previous = activeOutputFor({ base, consumerId, sessions: this.sessionsById, viewers: this.viewers });
|
|
7912
7842
|
if (previous.id === variant.id) {
|
|
7913
7843
|
// The rung on screen asking for more of itself, which it does every few
|
|
7914
7844
|
// seconds. Nothing is being decided here — and deciding anything was the
|
|
@@ -9431,7 +9361,7 @@ export class HlsSessionManager {
|
|
|
9431
9361
|
// change is another session. Touching the named one as well is what keeps
|
|
9432
9362
|
// the family alive: only the ACTIVE variant gets segment requests, so
|
|
9433
9363
|
// without this the base session would idle out from under its own variants.
|
|
9434
|
-
const session =
|
|
9364
|
+
const session = activeOutputFor({ base: named, consumerId, sessions: this.sessionsById, viewers: this.viewers });
|
|
9435
9365
|
session.lastAccessedAt = Date.now();
|
|
9436
9366
|
const warmupTotalSeconds = this.startupWaitMs / 1000;
|
|
9437
9367
|
const warmupElapsedSeconds = Math.max(0, (Date.now() - session.startedAt) / 1000);
|
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
* question, and it reads a union of viewers rather than any one of them.
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* How long a link measurement describes the link it was taken on.
|
|
45
|
+
*
|
|
46
|
+
* A reading older than this says nothing about now: the viewer may have seeked,
|
|
47
|
+
* changed network, or stopped. Thirty seconds is three of the page's own
|
|
48
|
+
* reporting intervals, so a live viewer always has a fresh one.
|
|
49
|
+
*/
|
|
50
|
+
export const LINK_READING_FRESH_MS = 30_000;
|
|
51
|
+
|
|
43
52
|
export class Viewer {
|
|
44
53
|
/**
|
|
45
54
|
* @param {string} id - The consumer id the browser sends with every request
|
|
@@ -321,6 +330,31 @@ export class Viewer {
|
|
|
321
330
|
*
|
|
322
331
|
* @returns {boolean}
|
|
323
332
|
*/
|
|
333
|
+
/**
|
|
334
|
+
* What this viewer's link was last measured to carry, or null when nothing
|
|
335
|
+
* recent measures it.
|
|
336
|
+
*
|
|
337
|
+
* HOW LONG A READING DESCRIBES A LINK is a fact about the reading, so it is
|
|
338
|
+
* answered here and nowhere else. It used to be a constant in the session
|
|
339
|
+
* manager, consulted in four places — once where a reading is chosen, and
|
|
340
|
+
* three more times over a figure that had already passed that very filter,
|
|
341
|
+
* which made those three conditions unreachable.
|
|
342
|
+
*
|
|
343
|
+
* A viewer who has stopped reporting is NOT thereby gone: only the reading
|
|
344
|
+
* expires. Answering both from one place is what stopped a soundtrack's
|
|
345
|
+
* encoder on 2026-09-05.
|
|
346
|
+
*
|
|
347
|
+
* @param {number} [now]
|
|
348
|
+
* @returns {{ linkMbps: number, bufferedAheadSec: number, positionSeconds: number | null, at: number } | null}
|
|
349
|
+
*/
|
|
350
|
+
linkReading(now = Date.now()) {
|
|
351
|
+
const reading = this.netReport;
|
|
352
|
+
if (reading === null || now - reading.at > LINK_READING_FRESH_MS) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return reading;
|
|
356
|
+
}
|
|
357
|
+
|
|
324
358
|
/**
|
|
325
359
|
* Whether this viewer wants film NOW — either watching it or waiting for it.
|
|
326
360
|
*
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which output a viewer has on screen.
|
|
3
|
+
*
|
|
4
|
+
* Every request that names the picture's session — a seek, a progress poll, a
|
|
5
|
+
* link report, a release — means the stream the viewer is actually watching,
|
|
6
|
+
* and after a quality change that is another session. The browser is never told
|
|
7
|
+
* about the swap: it holds one session id for the whole file, which is what
|
|
8
|
+
* keeps the switch out of the state machine on that side.
|
|
9
|
+
*
|
|
10
|
+
* KEPT PER VIEWER, because one picture is shared by everyone watching it and a
|
|
11
|
+
* quality step is a session of its own. With one answer for the session, a step
|
|
12
|
+
* taken by one viewer moved the other one's stream, and that viewer's next seek
|
|
13
|
+
* was forwarded to a rung they never chose. The session's own field remains the
|
|
14
|
+
* answer for a viewer who cannot name themselves, and is the last rung anybody
|
|
15
|
+
* moved to.
|
|
16
|
+
*
|
|
17
|
+
* It is handed the picture, a name, the live sessions and the registry of
|
|
18
|
+
* viewers, and it HOLDS none of them — but it is not read-only: finding a rung
|
|
19
|
+
* that has been disposed, it puts both remembers back on the picture, or the
|
|
20
|
+
* next question gets the same dead answer. It lives in the VIEWER layer because
|
|
21
|
+
* the question is about a person — which of several equally live outputs is
|
|
22
|
+
* theirs — and it knows nothing of encoders, files or heights.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} params
|
|
27
|
+
* @param {object} params.base - The picture's own session.
|
|
28
|
+
* @param {string} [params.consumerId] - Whose screen. Empty asks about the
|
|
29
|
+
* session's own last answer, which is what an unnamed browser gets.
|
|
30
|
+
* @param {{ get: (id: string) => object | undefined }} params.sessions - Every live session, by id.
|
|
31
|
+
* @param {{ of: (session: object, consumerId: string) => { activeVariantId: string | null } }} params.viewers
|
|
32
|
+
* @returns {object} A live session, never a disposed one.
|
|
33
|
+
*/
|
|
34
|
+
export function activeOutputFor({ base, consumerId = "", sessions, viewers }) {
|
|
35
|
+
const named = consumerId ? base.viewers?.get(consumerId)?.activeVariantId ?? null : null;
|
|
36
|
+
const activeId = named ?? base.activeVariantId;
|
|
37
|
+
if (!activeId || activeId === base.id) {
|
|
38
|
+
return base;
|
|
39
|
+
}
|
|
40
|
+
const active = sessions.get(activeId);
|
|
41
|
+
if (!active || active.state === "disposed") {
|
|
42
|
+
// The rung is gone, so the viewer is watching the picture again. Said in
|
|
43
|
+
// both places that remember it, or the next question gets the same dead
|
|
44
|
+
// answer.
|
|
45
|
+
if (named) {
|
|
46
|
+
viewers.of(base, consumerId).activeVariantId = null;
|
|
47
|
+
}
|
|
48
|
+
if (base.activeVariantId === activeId) {
|
|
49
|
+
base.activeVariantId = base.id;
|
|
50
|
+
}
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
return active;
|
|
54
|
+
}
|
|
@@ -1,63 +1,44 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A viewer's statement about itself, taken in.
|
|
3
|
-
*
|
|
4
|
-
* The VIEWER layer's own entry point, and the whole of what happens when a page
|
|
5
|
-
* says where it is: the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param {
|
|
24
|
-
* @param {object} params.
|
|
25
|
-
* @param {
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @param {object} params.report - What they said about themselves.
|
|
29
|
-
* @param {number} params.now
|
|
30
|
-
* @
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* stopped reporting is not thereby gone. Answering both from this one place is
|
|
46
|
-
* what stopped a soundtrack's encoder on 2026-09-05, seconds after it started
|
|
47
|
-
* and before it had made the `init.mp4` the picture could not be played
|
|
48
|
-
* without.
|
|
49
|
-
*
|
|
50
|
-
* @param {object} params
|
|
51
|
-
* @param {object} params.session
|
|
52
|
-
* @param {number} params.now
|
|
53
|
-
* @param {number} params.linkFreshMs
|
|
54
|
-
* @returns {void}
|
|
55
|
-
*/
|
|
56
|
-
export function forgetStaleLinkReadings({ session, now, linkFreshMs }) {
|
|
57
|
-
for (const viewer of viewersOf(session).values()) {
|
|
58
|
-
const reading = viewer.netReport;
|
|
59
|
-
if (reading !== null && now - reading.at > linkFreshMs) {
|
|
60
|
-
viewer.netReport = null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file A viewer's statement about itself, taken in.
|
|
3
|
+
*
|
|
4
|
+
* The VIEWER layer's own entry point, and the whole of what happens when a page
|
|
5
|
+
* says where it is: the output the reporter has on screen is resolved, the
|
|
6
|
+
* statement reaches the person it is about, and readings that have gone stale
|
|
7
|
+
* stop deciding for the people still here.
|
|
8
|
+
*
|
|
9
|
+
* It lived in the session manager, which is a description of an OUTPUT, and the
|
|
10
|
+
* two questions were tangled there: whether this is a measurement of a LINK,
|
|
11
|
+
* and whether this is a statement by a VIEWER. Required to be both, a page that
|
|
12
|
+
* had measured nothing could say nothing — see `Viewer.report`.
|
|
13
|
+
*
|
|
14
|
+
* Given the sessions and the registry as plain values, so nothing here knows
|
|
15
|
+
* how a session is found, what a variant is, or that there is an encoder.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { activeOutputFor } from "./active-output.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Take one report from one viewer.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} params
|
|
24
|
+
* @param {{ get: (id: string) => object | undefined }} params.sessions - Every live session, by id.
|
|
25
|
+
* @param {{ of: (session: object, consumerId: string) => import("./Viewer.js").Viewer }} params.viewers
|
|
26
|
+
* @param {string} params.sessionId - The session the page addresses, which is
|
|
27
|
+
* always the picture's: the browser is not told which rung it is on.
|
|
28
|
+
* @param {object} params.report - What they said about themselves.
|
|
29
|
+
* @param {number} [params.now]
|
|
30
|
+
* @returns {boolean} False when no such session is live, which is the page
|
|
31
|
+
* reporting into one that has been disposed.
|
|
32
|
+
*/
|
|
33
|
+
export function recordViewerReport({ sessions, viewers, sessionId, report, now = Date.now() }) {
|
|
34
|
+
const named = sessions.get(sessionId);
|
|
35
|
+
if (!named || named.state === "disposed") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const consumerId = typeof report?.consumerId === "string" ? report.consumerId : "";
|
|
39
|
+
// The stream on screen is the reporter's own: with two viewers on two rungs,
|
|
40
|
+
// one report says nothing about the other's encoder.
|
|
41
|
+
const session = activeOutputFor({ base: named, consumerId, sessions, viewers });
|
|
42
|
+
viewers.of(session, consumerId).report(report, now);
|
|
43
|
+
return true;
|
|
44
|
+
}
|