@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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.85.0",
3
+ "version": "2.85.1",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -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 {{ hlsSessionManager: import("../../../../services/hls-session-manager.js").HlsSessionManager }} deps
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, { hlsSessionManager }) {
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 = hlsSessionManager.recordNetReport(sessionId, {
70
- linkMbps: Number.isFinite(linkMbps) && linkMbps > 0 ? linkMbps : undefined,
71
- waiting,
72
- bufferedAheadSec,
73
- consumerId,
74
- playing,
75
- onScreen,
76
- inPictureInPicture,
77
- positionSeconds:
78
- Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : undefined
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
- handleApiTranscodeSessionNetReportPost(req, reply, { hlsSessionManager })
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.netReport;
3139
- if (report === null || now - report.at > LINK_REPORT_FRESH_MS) {
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 = this.#activeVariant(named);
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 || now - report.at > LINK_REPORT_FRESH_MS) {
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(this.#activeVariant(base));
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 || now - report.at > LINK_REPORT_FRESH_MS) {
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 || now - report.at > LINK_REPORT_FRESH_MS) {
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(this.#activeVariant(base))) {
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(this.#activeVariant(base).id);
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(this.#activeVariant(base, consumerId));
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 !== this.#activeVariant(base, consumerId).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 = this.#activeVariant(base, consumerId);
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 = this.#activeVariant(named, consumerId);
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 statement reaches the person it is about, and readings
6
- * that have gone stale stop deciding for the people still here.
7
- *
8
- * It lived in the session manager, which is a description of an OUTPUT, and the
9
- * two questions were tangled there: whether this is a measurement of a LINK,
10
- * and whether this is a statement by a VIEWER. Required to be both, a page that
11
- * had measured nothing could say nothing — see `Viewer.report`.
12
- *
13
- * Given the session and the registry as plain arguments, so nothing here knows
14
- * how a session is found, what a variant is, or that there is an encoder.
15
- */
16
-
17
- import { viewersOf } from "./Viewer.js";
18
-
19
- /**
20
- * Take one report.
21
- *
22
- * @param {object} params
23
- * @param {{ of: (session: object, consumerId: string) => import("./Viewer.js").Viewer }} params.viewers
24
- * @param {object} params.session - The output this viewer has on screen.
25
- * @param {string} params.consumerId - Who is reporting; empty when the page
26
- * does not name itself, and then every such page shares one viewer, which is
27
- * exactly what one unnamed browser is.
28
- * @param {object} params.report - What they said about themselves.
29
- * @param {number} params.now
30
- * @param {number} params.linkFreshMs - How long a link reading describes the
31
- * link it was measured on.
32
- * @returns {true}
33
- */
34
- export function takeViewerReport({ viewers, session, consumerId, report, now, linkFreshMs }) {
35
- viewers.of(session, consumerId).report(report, now);
36
- forgetStaleLinkReadings({ session, now, linkFreshMs });
37
- return true;
38
- }
39
-
40
- /**
41
- * Drop link readings older than the time they describe.
42
- *
43
- * ONLY THE READING EXPIRES. Whether the person is still watching is a different
44
- * question with its own answer — their connection — and a viewer who has simply
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
+ }