@torrent-tv/proxy 2.76.6 → 2.78.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 +1558 -1538
- package/bin/cli.js +14 -1
- package/package.json +1 -1
- package/services/data-channel-handler.js +83 -3
- package/services/encode/DemandMap.js +187 -0
- package/services/encode/EncodePlan.js +283 -272
- package/services/encode/SegmentDemand.js +0 -0
- package/services/hls-session-manager.js +476 -179
- package/services/orchestrators/EncodeOrchestrator.js +13 -3
- package/services/viewer/Viewer.js +214 -145
- package/services/viewer/Viewers.js +213 -124
- package/test/behind-head-repair.test.js +2 -2
- package/test/demand-map.test.js +102 -0
- package/test/encode-plan.test.js +44 -2
- package/test/failed-start-breaker.test.js +151 -0
- package/test/orchestrator-wired.test.js +190 -164
- package/test/quality-variants.test.js +2 -2
- package/test/run-intervals.test.js +67 -303
- package/test/seek-target-not-superseded.test.js +2 -2
- package/test/sidecar-naming.test.js +120 -120
- package/test/stale-request-after-seek.test.js +2 -2
- package/test/two-viewers-one-picture.test.js +2 -2
- package/test/viewer-outputs.test.js +7 -7
- package/test/viewer-presence.test.js +119 -0
- package/test/viewer.test.js +27 -10
|
@@ -121,9 +121,14 @@ export class EncodeOrchestrator {
|
|
|
121
121
|
* @param {string} params.address
|
|
122
122
|
* @param {number} params.from
|
|
123
123
|
* @param {number} params.to
|
|
124
|
+
* @param {number} [params.priority] - Higher is sooner. One viewer states
|
|
125
|
+
* several stretches at once — what must be ready before they set off, what
|
|
126
|
+
* is reachable while they watch it, the rest of the track — and the filling
|
|
127
|
+
* takes them in this order. Absent means one undifferentiated want, which
|
|
128
|
+
* is what a caller that knows only a position states.
|
|
124
129
|
*/
|
|
125
|
-
want({ claimant, address, from, to }) {
|
|
126
|
-
this.demand.state({ claimant, address, from, to, statedAt: this.now() });
|
|
130
|
+
want({ claimant, address, from, to, priority = 0 }) {
|
|
131
|
+
this.demand.state({ claimant, address, from, to, priority, statedAt: this.now() });
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
/**
|
|
@@ -196,9 +201,14 @@ export class EncodeOrchestrator {
|
|
|
196
201
|
});
|
|
197
202
|
}
|
|
198
203
|
}
|
|
204
|
+
// Carrying the priority through, because the filling takes the work in that
|
|
205
|
+
// order: what a viewer must have before they set off comes before what is
|
|
206
|
+
// merely in front of them, which comes before the rest of the track. Passed
|
|
207
|
+
// as a plain number so the plan stays arithmetic.
|
|
199
208
|
const windows = this.demand.windowsOn(address).map((window) => ({
|
|
200
209
|
from: window.from,
|
|
201
|
-
to: window.to
|
|
210
|
+
to: window.to,
|
|
211
|
+
priority: Number(window.priority) || 0
|
|
202
212
|
}));
|
|
203
213
|
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
204
214
|
const actions = planEncoders({
|
|
@@ -1,145 +1,214 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One person watching, and everything that is true of them alone.
|
|
3
|
-
*
|
|
4
|
-
* A viewer is not a property of the material. What they are listening to, which
|
|
5
|
-
* quality step they have on screen, what is being prepared for them, where they
|
|
6
|
-
* are and what their link can carry — none of it changes a byte of what any
|
|
7
|
-
* encoder produces, and none of it belongs to a session, which is a description
|
|
8
|
-
* of an OUTPUT.
|
|
9
|
-
*
|
|
10
|
-
* It was six parallel maps hung on the session, each keyed by consumer id:
|
|
11
|
-
* `audioChoiceByConsumer`, `activeVariantByConsumer`,
|
|
12
|
-
* `warmingVariantByConsumer`, `warmingAudioByConsumer`, `consumerHeads`,
|
|
13
|
-
* `netReports`. Six places to remember to update and six to remember to forget,
|
|
14
|
-
* and the forgetting was already wrong — releasing a consumer emptied none of
|
|
15
|
-
* them, so a viewer who had left went on counting as wanting their soundtrack
|
|
16
|
-
* until their head expired, and their entries stayed for the life of the
|
|
17
|
-
* session.
|
|
18
|
-
*
|
|
19
|
-
* One object, one map, one thing to remove.
|
|
20
|
-
*
|
|
21
|
-
* **
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* from
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file One person watching, and everything that is true of them alone.
|
|
3
|
+
*
|
|
4
|
+
* A viewer is not a property of the material. What they are listening to, which
|
|
5
|
+
* quality step they have on screen, what is being prepared for them, where they
|
|
6
|
+
* are and what their link can carry — none of it changes a byte of what any
|
|
7
|
+
* encoder produces, and none of it belongs to a session, which is a description
|
|
8
|
+
* of an OUTPUT.
|
|
9
|
+
*
|
|
10
|
+
* It was six parallel maps hung on the session, each keyed by consumer id:
|
|
11
|
+
* `audioChoiceByConsumer`, `activeVariantByConsumer`,
|
|
12
|
+
* `warmingVariantByConsumer`, `warmingAudioByConsumer`, `consumerHeads`,
|
|
13
|
+
* `netReports`. Six places to remember to update and six to remember to forget,
|
|
14
|
+
* and the forgetting was already wrong — releasing a consumer emptied none of
|
|
15
|
+
* them, so a viewer who had left went on counting as wanting their soundtrack
|
|
16
|
+
* until their head expired, and their entries stayed for the life of the
|
|
17
|
+
* session.
|
|
18
|
+
*
|
|
19
|
+
* One object, one map, one thing to remove.
|
|
20
|
+
*
|
|
21
|
+
* **TWO INDEPENDENT FACTS, NOT ONE.** A viewer is somewhere, and a viewer is
|
|
22
|
+
* either still here or gone. Until 2026-09-05 both were answered by one field —
|
|
23
|
+
* the position, which is written only when a segment is requested — so a viewer
|
|
24
|
+
* who had just arrived counted as absent, and an output all of whose viewers
|
|
25
|
+
* count as absent has every encoder on it stopped. That is exactly what
|
|
26
|
+
* happened on 2026-09-05: a soundtrack's encoder was stopped 1.25 s after it
|
|
27
|
+
* started, having produced nothing, its `init.mp4` was therefore never made,
|
|
28
|
+
* and the picture could not be played without it. The browser could not rescue
|
|
29
|
+
* itself either, because the only thing that would have marked the viewer
|
|
30
|
+
* present was a request for a segment — which needs the `init.mp4` that the
|
|
31
|
+
* stopped encoder was going to make.
|
|
32
|
+
*
|
|
33
|
+
* So: **position is known from the moment a viewer arrives** — it is in the
|
|
34
|
+
* request that created the output, as a time on the source, and it is either
|
|
35
|
+
* zero or what the address bar carried. And **presence is a fact of the
|
|
36
|
+
* connection**, not of the last file asked for.
|
|
37
|
+
*
|
|
38
|
+
* **Nothing here knows about ffmpeg, the disk or the torrent.** A viewer states
|
|
39
|
+
* what they want and where they are; what to make of that is the orchestrator's
|
|
40
|
+
* question, and it reads a union of viewers rather than any one of them.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export class Viewer {
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} id - The consumer id the browser sends with every request
|
|
46
|
+
* that means "this viewer".
|
|
47
|
+
* @param {number} [now] - When they arrived. Presence starts here, so a
|
|
48
|
+
* viewer counts as watching from the instant they are known.
|
|
49
|
+
*/
|
|
50
|
+
constructor(id, now = Date.now()) {
|
|
51
|
+
this.id = String(id ?? "");
|
|
52
|
+
/**
|
|
53
|
+
* Which soundtrack this viewer is listening to and whether their browser
|
|
54
|
+
* needs it re-encoded. Theirs alone: two viewers of one picture may have
|
|
55
|
+
* chosen different languages, and one browser may decode a track another
|
|
56
|
+
* cannot.
|
|
57
|
+
* @type {{ trackIndex: number, transcode: boolean }}
|
|
58
|
+
*/
|
|
59
|
+
this.audio = { trackIndex: 0, transcode: false };
|
|
60
|
+
/** The quality step on their screen. Null means the base session. @type {string | null} */
|
|
61
|
+
this.activeVariantId = null;
|
|
62
|
+
/** A step being prepared for a switch they have not made yet. @type {string | null} */
|
|
63
|
+
this.warmingVariantId = null;
|
|
64
|
+
/** A soundtrack being prepared for the same reason. @type {string | null} */
|
|
65
|
+
this.warmingAudioId = null;
|
|
66
|
+
/**
|
|
67
|
+
* Where they are.
|
|
68
|
+
*
|
|
69
|
+
* Set when they arrive, from the position their own request named, and
|
|
70
|
+
* moved by the segments they ask for and by the seeks they report. Never
|
|
71
|
+
* null for a viewer this process has met: "we do not know where they are"
|
|
72
|
+
* is not a state a viewer can be in, because a viewer arrives by asking for
|
|
73
|
+
* a position.
|
|
74
|
+
*
|
|
75
|
+
* `seeked` holds a position they STATED, for as long as they stay there,
|
|
76
|
+
* which is the distinction a cold open's soundtrack placement turns on: a
|
|
77
|
+
* request is evidence about where their player is reading, a seek is the
|
|
78
|
+
* viewer saying where they are.
|
|
79
|
+
*
|
|
80
|
+
* @type {{ segment: number, seconds: number, at: number, seeked?: number | null } | null}
|
|
81
|
+
*/
|
|
82
|
+
this.position = null;
|
|
83
|
+
/**
|
|
84
|
+
* When this viewer was last known to be there.
|
|
85
|
+
*
|
|
86
|
+
* Every piece of evidence refreshes it: a request of any kind, a link
|
|
87
|
+
* report, an echo of a delivery probe. It is NOT the position's timestamp —
|
|
88
|
+
* a viewer with a full buffer legitimately asks for nothing for a minute
|
|
89
|
+
* and is no less present for it.
|
|
90
|
+
*
|
|
91
|
+
* @type {number}
|
|
92
|
+
*/
|
|
93
|
+
this.lastSeenAt = now;
|
|
94
|
+
/**
|
|
95
|
+
* Set when something has SAID this viewer is gone — the browser released
|
|
96
|
+
* the session, or their connection closed. Silence never sets it: a
|
|
97
|
+
* paused viewer, a viewer whose tab is hidden and whose timers the browser
|
|
98
|
+
* has throttled, and a viewer holding two minutes of buffer are all silent
|
|
99
|
+
* and all still watching.
|
|
100
|
+
*
|
|
101
|
+
* @type {boolean}
|
|
102
|
+
*/
|
|
103
|
+
this.gone = false;
|
|
104
|
+
/** What their link was last measured to carry. @type {object | null} */
|
|
105
|
+
this.netReport = null;
|
|
106
|
+
/**
|
|
107
|
+
* Every output this viewer is watching, by session id: the picture, the
|
|
108
|
+
* quality step on their screen, the soundtrack they chose.
|
|
109
|
+
*
|
|
110
|
+
* WHY THERE ARE TWO SETS AND NOT ONE. There is one relation — this person
|
|
111
|
+
* watches this output — and it is asked from both ends. An output asks "has
|
|
112
|
+
* anybody left?", to decide whether to go on producing. A viewer who leaves
|
|
113
|
+
* asks "what was I watching?", so that each of those outputs can be told.
|
|
114
|
+
* Neither question can be answered from the other side without walking every
|
|
115
|
+
* session in the process, so the relation is indexed both ways. It is
|
|
116
|
+
* written in exactly one place — `Viewers.of` and `Viewers.leaves` write
|
|
117
|
+
* both directions together — which is what keeps two indexes of one relation
|
|
118
|
+
* from becoming two different answers.
|
|
119
|
+
*
|
|
120
|
+
* This is what replaced a film object. There is no "film" anywhere in this
|
|
121
|
+
* proxy — its parts are born at different times, die at different times and
|
|
122
|
+
* are addressed separately — and the three link fields that stood in for one
|
|
123
|
+
* could not say how many people were listening to a soundtrack.
|
|
124
|
+
*
|
|
125
|
+
* @type {Set<string>}
|
|
126
|
+
*/
|
|
127
|
+
this.outputs = new Set();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether this viewer is still watching.
|
|
132
|
+
*
|
|
133
|
+
* Two ways to stop being present, and neither of them is "asked for nothing
|
|
134
|
+
* recently". Something must have SAID they are gone, or nothing at all must
|
|
135
|
+
* have been heard from them for longer than any silence a watching viewer can
|
|
136
|
+
* produce.
|
|
137
|
+
*
|
|
138
|
+
* The second half exists only because the connection cannot always say: an
|
|
139
|
+
* `onClosed` on a data channel does not always come, and a transport that is
|
|
140
|
+
* not a data channel at all may have nothing to say. It is a backstop for a
|
|
141
|
+
* missing statement, not the ordinary way a viewer leaves.
|
|
142
|
+
*
|
|
143
|
+
* @param {number} now
|
|
144
|
+
* @param {number} staleAfterMs - Longer than any silence a watching viewer
|
|
145
|
+
* can produce. Derived from the cushion, never chosen here.
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
isPresent(now, staleAfterMs) {
|
|
149
|
+
if (this.gone) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return now - this.lastSeenAt <= staleAfterMs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Note that this viewer has been heard from.
|
|
157
|
+
*
|
|
158
|
+
* @param {number} [now]
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
seen(now = Date.now()) {
|
|
162
|
+
this.lastSeenAt = now;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Where this viewer is, in seconds, or null when they have somehow never been
|
|
167
|
+
* placed — which for a viewer created through `Viewers.of` cannot happen.
|
|
168
|
+
*
|
|
169
|
+
* @returns {number | null}
|
|
170
|
+
*/
|
|
171
|
+
positionSeconds() {
|
|
172
|
+
if (this.position === null) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return Number.isFinite(this.position.seeked) ? this.position.seeked : this.position.seconds;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The viewers of one session, made on first use.
|
|
181
|
+
*
|
|
182
|
+
* @param {object} session
|
|
183
|
+
* @returns {Map<string, Viewer>}
|
|
184
|
+
*/
|
|
185
|
+
export function viewersOf(session) {
|
|
186
|
+
if (!(session.viewers instanceof Map)) {
|
|
187
|
+
session.viewers = new Map();
|
|
188
|
+
}
|
|
189
|
+
return session.viewers;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The viewer with this id, made if this session has not met them before.
|
|
194
|
+
*
|
|
195
|
+
* Use `Viewers.of` instead wherever a registry is at hand: this makes ONE
|
|
196
|
+
* viewer per session, so the same person watching a picture, a quality step and
|
|
197
|
+
* a soundtrack is three objects, and `outputs` — a fact about the person — is
|
|
198
|
+
* then three sets that nothing keeps in step. It is kept for a session assembled
|
|
199
|
+
* by hand in a test that has no registry.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} session
|
|
202
|
+
* @param {string} consumerId
|
|
203
|
+
* @returns {Viewer}
|
|
204
|
+
*/
|
|
205
|
+
export function viewerOf(session, consumerId) {
|
|
206
|
+
const viewers = viewersOf(session);
|
|
207
|
+
let viewer = viewers.get(consumerId);
|
|
208
|
+
if (!viewer) {
|
|
209
|
+
viewer = new Viewer(consumerId);
|
|
210
|
+
viewers.set(consumerId, viewer);
|
|
211
|
+
}
|
|
212
|
+
viewer.outputs.add(session.id);
|
|
213
|
+
return viewer;
|
|
214
|
+
}
|