@torrent-tv/proxy 2.85.1 → 2.86.0
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/services/hls-session-manager.js +18 -99
- package/services/viewer/Viewer.js +20 -26
- package/services/viewer/audio-start.js +45 -0
- package/services/viewer/link-readings.js +56 -0
- package/services/viewer/report-intake.js +4 -3
- package/test/auto-quality-step.test.js +15 -18
- package/test/viewer-states.test.js +36 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.86.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: A LINK READING NO LONGER EXPIRES, and the thirty seconds that made it expire were a chosen number holding two facts at once: how fast the link is, and whether we are still hearing from the page. The speed persists — a link does not stop being what it was measured to be because nobody measured it for a minute, which is why the page keeps its own last figure rather than reporting nothing. It is measured once when the connection comes up and corrected by every transfer after that: one estimate, continuously refined, never a fact with a deadline. Whether a reading is still somebody's is PRESENCE, which has its own owner — a viewer who has gone is removed from the output, so the readings weighed for a decision are only those of people still there.
|
|
4
|
+
- **Chore**: The worst reading across the viewers of one output moved to the viewer's own layer (`services/viewer/link-readings.js`), and skips anybody not present. With the expiry gone, `#linkCouldCarry` no longer takes a clock it cannot use.
|
|
5
|
+
- **Chore**: Where a soundtrack must begin moved out too (`services/viewer/audio-start.js`) — it is a question about where the earliest PERSON stands across every rung of a picture, decided one parameter of an output, and had no business being answered inside the session manager. The manager is 199 lines shorter than before this work for both moves, and every line it gained is an import or a call outward.
|
|
6
|
+
|
|
1
7
|
## 2.85.1
|
|
2
8
|
|
|
3
9
|
- **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.
|
package/package.json
CHANGED
|
@@ -89,7 +89,9 @@ import {
|
|
|
89
89
|
export { ffmpegSeconds, onKeyframeGridFor, seekLandingOffsetFor, segmentCutTimesFrom };
|
|
90
90
|
import { viewersOf } from "./viewer/Viewer.js";
|
|
91
91
|
import { activeOutputFor } from "./viewer/active-output.js";
|
|
92
|
-
import {
|
|
92
|
+
import { audioStartSecondsFor } from "./viewer/audio-start.js";
|
|
93
|
+
import { worstLinkReading } from "./viewer/link-readings.js";
|
|
94
|
+
import { viewerSecondsOn, viewerSegmentsOn } from "./viewer/positions.js";
|
|
93
95
|
import { Viewers } from "./viewer/Viewers.js";
|
|
94
96
|
import { LiveOutputs } from "./output/LiveOutputs.js";
|
|
95
97
|
import { variantHeightsFor } from "./output/ladder.js";
|
|
@@ -3086,44 +3088,6 @@ export class HlsSessionManager {
|
|
|
3086
3088
|
this.viewers.of(session, consumerId).seen();
|
|
3087
3089
|
}
|
|
3088
3090
|
|
|
3089
|
-
/**
|
|
3090
|
-
* The worst of what the viewers of this session report, as one reading.
|
|
3091
|
-
*
|
|
3092
|
-
* The budget asks one question — is anybody failing to keep up — so both
|
|
3093
|
-
* terms are the worst case: the slowest link and the emptiest buffer, which
|
|
3094
|
-
* may belong to different people. That is deliberate. Taking the last report
|
|
3095
|
-
* instead meant a session with two viewers acted on whichever of them
|
|
3096
|
-
* happened to report most recently.
|
|
3097
|
-
*
|
|
3098
|
-
* @param {HlsSession} session
|
|
3099
|
-
* @param {number} now
|
|
3100
|
-
* @returns {{ linkMbps: number, bufferedAheadSec: number, at: number, viewers: number } | null}
|
|
3101
|
-
* Null when nothing fresh measures the link, which is the silence every
|
|
3102
|
-
* caller already treats as "no opinion".
|
|
3103
|
-
*/
|
|
3104
|
-
#worstNetReport(session, now) {
|
|
3105
|
-
let worst = null;
|
|
3106
|
-
for (const viewer of viewersOf(session).values()) {
|
|
3107
|
-
const report = viewer.linkReading(now);
|
|
3108
|
-
if (report === null) {
|
|
3109
|
-
continue;
|
|
3110
|
-
}
|
|
3111
|
-
if (worst === null) {
|
|
3112
|
-
worst = {
|
|
3113
|
-
linkMbps: report.linkMbps,
|
|
3114
|
-
bufferedAheadSec: report.bufferedAheadSec,
|
|
3115
|
-
at: report.at,
|
|
3116
|
-
viewers: 1
|
|
3117
|
-
};
|
|
3118
|
-
continue;
|
|
3119
|
-
}
|
|
3120
|
-
worst.linkMbps = Math.min(worst.linkMbps, report.linkMbps);
|
|
3121
|
-
worst.bufferedAheadSec = Math.min(worst.bufferedAheadSec, report.bufferedAheadSec);
|
|
3122
|
-
worst.at = Math.max(worst.at, report.at);
|
|
3123
|
-
worst.viewers += 1;
|
|
3124
|
-
}
|
|
3125
|
-
return worst;
|
|
3126
|
-
}
|
|
3127
3091
|
|
|
3128
3092
|
/**
|
|
3129
3093
|
* Answer the player's report that a delivered fragment sits far from the edge
|
|
@@ -3243,9 +3207,9 @@ export class HlsSessionManager {
|
|
|
3243
3207
|
* @returns {Promise<boolean>}
|
|
3244
3208
|
*/
|
|
3245
3209
|
async #checkLinkBudget(session, now) {
|
|
3246
|
-
const report =
|
|
3210
|
+
const report = worstLinkReading(session);
|
|
3247
3211
|
if (!report) {
|
|
3248
|
-
session.linkSlowSince = 0; //
|
|
3212
|
+
session.linkSlowSince = 0; // nobody present has measured their link
|
|
3249
3213
|
return false;
|
|
3250
3214
|
}
|
|
3251
3215
|
if (report.bufferedAheadSec >= LINK_LOW_BUFFER_SEC) {
|
|
@@ -4689,7 +4653,7 @@ export class HlsSessionManager {
|
|
|
4689
4653
|
// top offered height has no next rung at all, and answering "nothing to
|
|
4690
4654
|
// step to, so yes" is how a cap came off a link measured at a fifth of
|
|
4691
4655
|
// what the picture needs.
|
|
4692
|
-
if (!this.#linkCouldCarry(session, this.#peakMbpsForHeight(this.liveOutputs.pictureOf(session), current)
|
|
4656
|
+
if (!this.#linkCouldCarry(session, this.#peakMbpsForHeight(this.liveOutputs.pictureOf(session), current))) {
|
|
4693
4657
|
return;
|
|
4694
4658
|
}
|
|
4695
4659
|
session.budgetUpSince = 0;
|
|
@@ -4733,10 +4697,10 @@ export class HlsSessionManager {
|
|
|
4733
4697
|
if (session.linkSlowSince !== 0 || session.budgetSlowSince !== 0) {
|
|
4734
4698
|
return false;
|
|
4735
4699
|
}
|
|
4736
|
-
const report =
|
|
4700
|
+
const report = worstLinkReading(session);
|
|
4737
4701
|
if (!report) {
|
|
4738
|
-
// Nothing
|
|
4739
|
-
//
|
|
4702
|
+
// Nothing measures the link, so it has no opinion either way — the same
|
|
4703
|
+
// silence that stops #checkLinkBudget from acting.
|
|
4740
4704
|
return true;
|
|
4741
4705
|
}
|
|
4742
4706
|
const base = this.liveOutputs.pictureOf(session);
|
|
@@ -4744,24 +4708,23 @@ export class HlsSessionManager {
|
|
|
4744
4708
|
if (next === undefined) {
|
|
4745
4709
|
return true; // nothing to step to; only the cap decision is left
|
|
4746
4710
|
}
|
|
4747
|
-
return this.#linkCouldCarry(session, this.#peakMbpsForHeight(base, next)
|
|
4711
|
+
return this.#linkCouldCarry(session, this.#peakMbpsForHeight(base, next));
|
|
4748
4712
|
}
|
|
4749
4713
|
|
|
4750
4714
|
/**
|
|
4751
4715
|
* Whether the viewer's measured link can carry a given number of Mbit/s.
|
|
4752
4716
|
*
|
|
4753
|
-
* A link nobody has measured
|
|
4717
|
+
* A link nobody has ever measured has no opinion either way — the same
|
|
4754
4718
|
* silence that stops `#checkLinkBudget` from acting — so it answers yes.
|
|
4755
4719
|
*
|
|
4756
4720
|
* @param {HlsSession} session
|
|
4757
4721
|
* @param {number} wantedMbps
|
|
4758
|
-
* @param {number} now
|
|
4759
4722
|
* @returns {boolean}
|
|
4760
4723
|
*/
|
|
4761
|
-
#linkCouldCarry(session, wantedMbps
|
|
4724
|
+
#linkCouldCarry(session, wantedMbps) {
|
|
4762
4725
|
// The SLOWEST link among the viewers: a step up has to be carried by all of
|
|
4763
4726
|
// them, not by whichever reported last.
|
|
4764
|
-
const report =
|
|
4727
|
+
const report = worstLinkReading(session);
|
|
4765
4728
|
if (!report) {
|
|
4766
4729
|
return true;
|
|
4767
4730
|
}
|
|
@@ -7280,54 +7243,6 @@ export class HlsSessionManager {
|
|
|
7280
7243
|
return { earliestPosition, deepestBuffer, viewers };
|
|
7281
7244
|
}
|
|
7282
7245
|
|
|
7283
|
-
/**
|
|
7284
|
-
* Where a soundtrack must begin: at the earliest film anybody is waiting on.
|
|
7285
|
-
*
|
|
7286
|
-
* ASKED OF THE MAP, NOT OF A VIEWER. Sound is produced for the same reason a
|
|
7287
|
-
* picture is, and the map already states it — in seconds of film, merged over
|
|
7288
|
-
* everybody watching, with the map's own order saying which of them is
|
|
7289
|
-
* earliest. Reading it off a viewer put a person inside the encoding layer,
|
|
7290
|
-
* and then every question about WHICH person had to be answered a second time
|
|
7291
|
-
* here: the earliest of two, the one on this rung, the one who has not
|
|
7292
|
-
* reported yet.
|
|
7293
|
-
*
|
|
7294
|
-
* The apparatus this replaced is worth naming so it is not rebuilt: three
|
|
7295
|
-
* position sources ranked by priority, a function saying which had answered,
|
|
7296
|
-
* a subtraction of the deepest reported buffer, and a special case for a
|
|
7297
|
-
* session nobody had asked anything of. Each repaired a quantity that meant
|
|
7298
|
-
* two things. It means one thing now.
|
|
7299
|
-
*
|
|
7300
|
-
* One segment back, and that is not a margin: a cut grid places the
|
|
7301
|
-
* soundtrack's own boundaries where it will, so the piece holding a moment of
|
|
7302
|
-
* film begins at or before it.
|
|
7303
|
-
*
|
|
7304
|
-
* @param {HlsSession} base
|
|
7305
|
-
* @returns {number} Seconds.
|
|
7306
|
-
*/
|
|
7307
|
-
#audioStartSecondsFor(base) {
|
|
7308
|
-
// WHERE THE EARLIEST OF THEM STANDS, across every rung of this picture. A
|
|
7309
|
-
// track begun at the leader has nothing to give the viewer behind them, and
|
|
7310
|
-
// two viewers of one film may be on different rungs.
|
|
7311
|
-
//
|
|
7312
|
-
// NO SUBTRACTION, and that is the whole of the change. A viewer's position
|
|
7313
|
-
// is where their PICTURE is, so the sound belongs exactly there — while the
|
|
7314
|
-
// apparatus this replaced turned a request edge back into a playhead with
|
|
7315
|
-
// three ranked sources, a name for which had answered, a subtraction of the
|
|
7316
|
-
// deepest reported buffer, and a special case for a session nobody had
|
|
7317
|
-
// asked anything of. Field 2026-08-31: a page opened at 588 s started its
|
|
7318
|
-
// sound at 460 s, 131 seconds nobody would hear
|
|
7319
|
-
// (`research/cold-open-audio-start-2026-08-31.md`).
|
|
7320
|
-
//
|
|
7321
|
-
// A fact about people, deciding one parameter of an output. It places no
|
|
7322
|
-
// encoder: where an encoder works is the priority map's answer.
|
|
7323
|
-
const earliest = earliestViewerSecondsOn(this.liveOutputs.familyOf(base));
|
|
7324
|
-
const opened = Number(base.progress?.startPositionSeconds);
|
|
7325
|
-
const from = earliest ?? (Number.isFinite(opened) ? opened : 0);
|
|
7326
|
-
// One segment back, and that is not a margin: a cut grid places the
|
|
7327
|
-
// soundtrack's own boundaries where it will, so the piece holding a moment
|
|
7328
|
-
// of film begins at or before it.
|
|
7329
|
-
return Math.max(0, from - this.segmentDurationSec);
|
|
7330
|
-
}
|
|
7331
7246
|
|
|
7332
7247
|
|
|
7333
7248
|
/**
|
|
@@ -8263,7 +8178,11 @@ export class HlsSessionManager {
|
|
|
8263
8178
|
// who seeked since then is somewhere else entirely — so an old one is
|
|
8264
8179
|
// ignored and the whole look-ahead is subtracted instead, which cannot
|
|
8265
8180
|
// leave the run ahead of them.
|
|
8266
|
-
startPositionSeconds:
|
|
8181
|
+
startPositionSeconds: audioStartSecondsFor({
|
|
8182
|
+
family: this.liveOutputs.familyOf(base),
|
|
8183
|
+
openedAtSeconds: base.progress?.startPositionSeconds,
|
|
8184
|
+
segmentSeconds: this.segmentDurationSec
|
|
8185
|
+
}),
|
|
8267
8186
|
segmentFormatId: base.segmentFormat.id,
|
|
8268
8187
|
// Cut where the picture is cut. Two streams meant to be played together
|
|
8269
8188
|
// have to be divided at the same times, and the grid is the base's — the
|
|
@@ -40,15 +40,6 @@
|
|
|
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
|
-
|
|
52
43
|
export class Viewer {
|
|
53
44
|
/**
|
|
54
45
|
* @param {string} id - The consumer id the browser sends with every request
|
|
@@ -331,28 +322,31 @@ export class Viewer {
|
|
|
331
322
|
* @returns {boolean}
|
|
332
323
|
*/
|
|
333
324
|
/**
|
|
334
|
-
* What this viewer's link was last measured to carry, or null when
|
|
335
|
-
*
|
|
325
|
+
* What this viewer's link was last measured to carry, or null when it has
|
|
326
|
+
* never been measured.
|
|
336
327
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* which made those three conditions unreachable.
|
|
328
|
+
* IT DOES NOT EXPIRE, and the reasoning is the correction of 2026-09-14. A
|
|
329
|
+
* reading older than a chosen thirty seconds used to be discarded here, which
|
|
330
|
+
* put two facts under one number: how fast the link is, and whether we are
|
|
331
|
+
* still hearing from this page.
|
|
342
332
|
*
|
|
343
|
-
* A
|
|
344
|
-
*
|
|
345
|
-
*
|
|
333
|
+
* The speed persists. A link does not stop being what it was measured to be
|
|
334
|
+
* because nobody measured it for a minute, and the page says as much by
|
|
335
|
+
* keeping its own last figure for ever rather than reporting nothing. It is
|
|
336
|
+
* measured once when the connection comes up and corrected by every transfer
|
|
337
|
+
* afterwards — one estimate, continuously refined, never a fact with a
|
|
338
|
+
* deadline.
|
|
339
|
+
*
|
|
340
|
+
* Whether the reading is still somebody's is PRESENCE, which has its own
|
|
341
|
+
* owner: a viewer who has gone is removed from the output, and the readings
|
|
342
|
+
* walked for a decision are only those of people still there. Answering both
|
|
343
|
+
* questions in one place is what stopped a soundtrack's encoder on
|
|
344
|
+
* 2026-09-05.
|
|
346
345
|
*
|
|
347
|
-
* @param {number} [now]
|
|
348
346
|
* @returns {{ linkMbps: number, bufferedAheadSec: number, positionSeconds: number | null, at: number } | null}
|
|
349
347
|
*/
|
|
350
|
-
linkReading(
|
|
351
|
-
|
|
352
|
-
if (reading === null || now - reading.at > LINK_READING_FRESH_MS) {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
return reading;
|
|
348
|
+
linkReading() {
|
|
349
|
+
return this.netReport;
|
|
356
350
|
}
|
|
357
351
|
|
|
358
352
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a soundtrack must begin.
|
|
3
|
+
*
|
|
4
|
+
* ASKED OF THE PEOPLE, NOT OF A SESSION. Sound is produced for the same reason
|
|
5
|
+
* a picture is — somebody is going to hear it — so the question is where the
|
|
6
|
+
* earliest of them stands, across every rung of that picture: a track begun at
|
|
7
|
+
* the leader has nothing to give the viewer behind them, and two viewers of one
|
|
8
|
+
* film may be watching at different qualities.
|
|
9
|
+
*
|
|
10
|
+
* NO SUBTRACTION, and that is the whole of what this replaced: three position
|
|
11
|
+
* sources ranked by priority, a second function naming which had answered, a
|
|
12
|
+
* subtraction of the deepest reported buffer, and a special case for a session
|
|
13
|
+
* nobody had asked anything of. Each repaired a quantity that meant two things.
|
|
14
|
+
* A viewer's position is where their PICTURE is, so the sound belongs exactly
|
|
15
|
+
* there. Field 2026-08-31, what the subtraction cost: a page opened at 588 s
|
|
16
|
+
* started its sound at 460 s — 131 seconds nobody would hear
|
|
17
|
+
* (`research/cold-open-audio-start-2026-08-31.md`).
|
|
18
|
+
*
|
|
19
|
+
* It decides ONE PARAMETER of an output and places no encoder: where an encoder
|
|
20
|
+
* works is the priority map's answer. Pure over plain values — the outputs of
|
|
21
|
+
* one picture, where it was opened, and how long a piece is — so it knows
|
|
22
|
+
* nothing of ffmpeg, cut grids or the disk.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { earliestViewerSecondsOn } from "./positions.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} params
|
|
29
|
+
* @param {Iterable<object>} params.family - Every output of one picture: the
|
|
30
|
+
* picture itself and its quality steps. The viewers are spread across them.
|
|
31
|
+
* @param {number} params.openedAtSeconds - Where the picture was opened, which
|
|
32
|
+
* is the answer while nobody has stated a position yet.
|
|
33
|
+
* @param {number} params.segmentSeconds - How long one piece is.
|
|
34
|
+
* @returns {number} Seconds, never negative.
|
|
35
|
+
*/
|
|
36
|
+
export function audioStartSecondsFor({ family, openedAtSeconds, segmentSeconds }) {
|
|
37
|
+
const earliest = earliestViewerSecondsOn(family);
|
|
38
|
+
const opened = Number(openedAtSeconds);
|
|
39
|
+
const from = earliest ?? (Number.isFinite(opened) ? opened : 0);
|
|
40
|
+
// ONE PIECE BACK, and that is not a margin: a cut grid places the
|
|
41
|
+
// soundtrack's own boundaries where it will, so the piece holding a moment of
|
|
42
|
+
// film begins at or before it.
|
|
43
|
+
const back = Number.isFinite(segmentSeconds) && segmentSeconds > 0 ? segmentSeconds : 0;
|
|
44
|
+
return Math.max(0, from - back);
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What the viewers of one output say about their links, as one reading.
|
|
3
|
+
*
|
|
4
|
+
* The budget asks one question — is anybody failing to keep up — so both terms
|
|
5
|
+
* are the worst case: the slowest link and the emptiest buffer, which may
|
|
6
|
+
* belong to different people. Taking the last report instead meant a session
|
|
7
|
+
* with two viewers acted on whichever of them happened to report most recently.
|
|
8
|
+
*
|
|
9
|
+
* NOTHING HERE EXPIRES, and that is the correction of 2026-09-14. A reading
|
|
10
|
+
* used to be discarded once it was older than a chosen thirty seconds, which
|
|
11
|
+
* conflated two facts under one number: how fast the link is, and whether we
|
|
12
|
+
* are still hearing from the page. The first persists — a link does not stop
|
|
13
|
+
* being what it was measured to be because nobody measured it for a minute,
|
|
14
|
+
* which is why the page itself keeps its last figure for ever. The second is
|
|
15
|
+
* PRESENCE, and presence has its own owner: a viewer who has gone is removed
|
|
16
|
+
* from the output, so what is walked here is only people who are still here.
|
|
17
|
+
*
|
|
18
|
+
* The reading is corrected continuously while material crosses the link, which
|
|
19
|
+
* is the whole shape: measured once when the connection comes up, corrected by
|
|
20
|
+
* every transfer after it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { viewersOf } from "./Viewer.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} output - The session whose viewers are asked.
|
|
27
|
+
* @returns {{ linkMbps: number, bufferedAheadSec: number, at: number, viewers: number } | null}
|
|
28
|
+
* Null when nobody present has ever measured their link, which every caller
|
|
29
|
+
* already treats as "no opinion either way".
|
|
30
|
+
*/
|
|
31
|
+
export function worstLinkReading(output) {
|
|
32
|
+
let worst = null;
|
|
33
|
+
for (const viewer of viewersOf(output).values()) {
|
|
34
|
+
if (!viewer.isPresent()) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const reading = viewer.linkReading();
|
|
38
|
+
if (reading === null) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (worst === null) {
|
|
42
|
+
worst = {
|
|
43
|
+
linkMbps: reading.linkMbps,
|
|
44
|
+
bufferedAheadSec: reading.bufferedAheadSec,
|
|
45
|
+
at: reading.at,
|
|
46
|
+
viewers: 1
|
|
47
|
+
};
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
worst.linkMbps = Math.min(worst.linkMbps, reading.linkMbps);
|
|
51
|
+
worst.bufferedAheadSec = Math.min(worst.bufferedAheadSec, reading.bufferedAheadSec);
|
|
52
|
+
worst.at = Math.max(worst.at, reading.at);
|
|
53
|
+
worst.viewers += 1;
|
|
54
|
+
}
|
|
55
|
+
return worst;
|
|
56
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* @file A viewer's statement about itself, taken in.
|
|
3
3
|
*
|
|
4
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
|
|
7
|
-
*
|
|
5
|
+
* says where it is: the output the reporter has on screen is resolved, and the
|
|
6
|
+
* statement reaches the person it is about. Nothing expires here — a link
|
|
7
|
+
* reading is the last thing known about that link, and whether it is still
|
|
8
|
+
* somebody's is presence's answer (`Viewer.linkReading`).
|
|
8
9
|
*
|
|
9
10
|
* It lived in the session manager, which is a description of an OUTPUT, and the
|
|
10
11
|
* two questions were tangled there: whether this is a measurement of a LINK,
|
|
@@ -345,7 +345,7 @@ test("with two viewers the budget acts on the WORST link, not on whoever reporte
|
|
|
345
345
|
);
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
test("a
|
|
348
|
+
test("a reading stops counting when the person leaves, not when it gets old", async (t) => {
|
|
349
349
|
const { manager, session, dirPath } = await managerWithSession({ transcodeVideo: false });
|
|
350
350
|
t.after(async () => {
|
|
351
351
|
await manager.disposeAll();
|
|
@@ -353,17 +353,15 @@ test("a report from a viewer who has left stops counting", async (t) => {
|
|
|
353
353
|
});
|
|
354
354
|
|
|
355
355
|
await produceSegments(session, 2_000_000);
|
|
356
|
-
// A
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
at: Date.now() - 120_000
|
|
366
|
-
};
|
|
356
|
+
// A LINK READING DOES NOT EXPIRE. It is the last thing known about that link,
|
|
357
|
+
// and how fast a link is does not change because nobody measured it for a
|
|
358
|
+
// minute — the page keeps its own last figure for the same reason. What ends
|
|
359
|
+
// a reading is the person leaving, which is presence, and presence is the
|
|
360
|
+
// connection: silence removes nobody (2026-09-05, a soundtrack's encoder
|
|
361
|
+
// stopped because those two questions had one answer).
|
|
362
|
+
const gone = viewerOf(session, "gone");
|
|
363
|
+
gone.report({ linkMbps: 1.0, bufferedAheadSec: 1.5, positionSeconds: 40, playing: true }, Date.now() - 120_000);
|
|
364
|
+
gone.gone = true;
|
|
367
365
|
recordViewerReport({
|
|
368
366
|
sessions: manager.sessionsById,
|
|
369
367
|
viewers: manager.viewers,
|
|
@@ -375,13 +373,12 @@ test("a report from a viewer who has left stops counting", async (t) => {
|
|
|
375
373
|
await manager.runQualityBudgetOnce();
|
|
376
374
|
|
|
377
375
|
assert.equal(session.viewers.size, 2, "the viewer is still known — silence is not leaving");
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
// else's report was a second owner of the same fact.
|
|
376
|
+
// Their reading is not deleted anywhere: it is simply not theirs to give any
|
|
377
|
+
// more, because they are not here. Nothing walks it for a decision.
|
|
381
378
|
assert.equal(
|
|
382
|
-
viewerOf(session, "gone").linkReading(
|
|
383
|
-
|
|
384
|
-
"
|
|
379
|
+
viewerOf(session, "gone").linkReading()?.linkMbps,
|
|
380
|
+
1.0,
|
|
381
|
+
"their last reading stands as the last thing known about that link"
|
|
385
382
|
);
|
|
386
383
|
assert.equal(session.qualityAsk, null, "the viewer who is here can carry the picture");
|
|
387
384
|
});
|
|
@@ -144,16 +144,46 @@ test("the intake hands the statement to the viewer on the output they are watchi
|
|
|
144
144
|
assert.equal(picture.viewers?.has("one") ?? false, false);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
test("a reading
|
|
147
|
+
test("a link reading does not expire — presence is what decides whose it is", () => {
|
|
148
148
|
const viewer = new Viewer("one", AT);
|
|
149
149
|
viewer.report({ linkMbps: 8, bufferedAheadSec: 10, positionSeconds: 5, playing: true }, AT);
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
// A link does not stop being what it was measured to be because nobody
|
|
152
|
+
// measured it for a while. The page says as much by keeping its own last
|
|
153
|
+
// figure rather than reporting nothing.
|
|
154
|
+
assert.equal(viewer.linkReading()?.linkMbps, 8);
|
|
155
|
+
assert.equal(viewer.linkReading()?.linkMbps, 8, "and an hour later it is still the last thing known");
|
|
156
|
+
|
|
157
|
+
// What ends it is the person leaving, which is a different fact with a
|
|
158
|
+
// different owner.
|
|
155
159
|
assert.equal(viewer.isPresent(), true);
|
|
156
|
-
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("the worst reading is taken across the people who are still here", async () => {
|
|
163
|
+
const { worstLinkReading } = await import("../services/viewer/link-readings.js");
|
|
164
|
+
const output = {};
|
|
165
|
+
const slow = new Viewer("slow", AT);
|
|
166
|
+
slow.outputs.add("out");
|
|
167
|
+
const fast = new Viewer("fast", AT);
|
|
168
|
+
fast.outputs.add("out");
|
|
169
|
+
output.viewers = new Map([
|
|
170
|
+
["slow", slow],
|
|
171
|
+
["fast", fast]
|
|
172
|
+
]);
|
|
173
|
+
slow.report({ linkMbps: 2, bufferedAheadSec: 3, playing: true }, AT);
|
|
174
|
+
fast.report({ linkMbps: 40, bufferedAheadSec: 90, playing: true }, AT);
|
|
175
|
+
|
|
176
|
+
// The slowest link and the emptiest buffer, which may belong to two people.
|
|
177
|
+
const worst = worstLinkReading(output);
|
|
178
|
+
assert.equal(worst?.linkMbps, 2);
|
|
179
|
+
assert.equal(worst?.bufferedAheadSec, 3);
|
|
180
|
+
assert.equal(worst?.viewers, 2);
|
|
181
|
+
|
|
182
|
+
// The slow one leaves. Their reading goes with them, however recent it was.
|
|
183
|
+
slow.gone = true;
|
|
184
|
+
const left = worstLinkReading(output);
|
|
185
|
+
assert.equal(left?.linkMbps, 40);
|
|
186
|
+
assert.equal(left?.viewers, 1);
|
|
157
187
|
});
|
|
158
188
|
|
|
159
189
|
test("a report into a session that is gone is refused rather than invented", async () => {
|