@torrent-tv/proxy 2.80.4 → 2.80.6
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 +36 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +42 -22
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +55 -3
- package/services/encode/open-piece.js +47 -24
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +123 -94
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +152 -0
- package/test/output-speed.test.js +86 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
- package/test/flushed-piece.test.js +0 -108
|
@@ -1,213 +1,233 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which outputs of one file exist right now, and what each of them is.
|
|
3
|
-
*
|
|
4
|
-
* Every question here is answered by walking the live sessions and looking at
|
|
5
|
-
* what they ARE — the same file, a step, a soundtrack, this height — and not by
|
|
6
|
-
* following a list of ids anybody keeps. A list of ids is a link between
|
|
7
|
-
* sessions: it ties their lifetimes together, it goes stale when one of them is
|
|
8
|
-
* disposed, and it has to be cleaned from the other side. What an output is is
|
|
9
|
-
* enough to find it, which is the rule `OutputSpec` exists for.
|
|
10
|
-
*
|
|
11
|
-
* Nothing here writes anything except the two answers a session memoizes about
|
|
12
|
-
* itself, and nothing here knows about encoders, viewers, the torrent or the
|
|
13
|
-
* disk. It is the layer the quality budget and the serving path both stand on,
|
|
14
|
-
* and it is separated first for that reason.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { variantHeightsFor } from "./ladder.js";
|
|
18
|
-
|
|
19
|
-
export class LiveOutputs {
|
|
20
|
-
/**
|
|
21
|
-
* @param {object} params
|
|
22
|
-
* @param {Map<string, object>} params.sessionsById - The live sessions. Read,
|
|
23
|
-
* never written.
|
|
24
|
-
*/
|
|
25
|
-
constructor({ sessionsById }) {
|
|
26
|
-
this.sessionsById = sessionsById;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Every live session
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* one
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @param {object}
|
|
94
|
-
* @returns {object}
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Which outputs of one file exist right now, and what each of them is.
|
|
3
|
+
*
|
|
4
|
+
* Every question here is answered by walking the live sessions and looking at
|
|
5
|
+
* what they ARE — the same file, a step, a soundtrack, this height — and not by
|
|
6
|
+
* following a list of ids anybody keeps. A list of ids is a link between
|
|
7
|
+
* sessions: it ties their lifetimes together, it goes stale when one of them is
|
|
8
|
+
* disposed, and it has to be cleaned from the other side. What an output is is
|
|
9
|
+
* enough to find it, which is the rule `OutputSpec` exists for.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here writes anything except the two answers a session memoizes about
|
|
12
|
+
* itself, and nothing here knows about encoders, viewers, the torrent or the
|
|
13
|
+
* disk. It is the layer the quality budget and the serving path both stand on,
|
|
14
|
+
* and it is separated first for that reason.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { variantHeightsFor } from "./ladder.js";
|
|
18
|
+
|
|
19
|
+
export class LiveOutputs {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} params
|
|
22
|
+
* @param {Map<string, object>} params.sessionsById - The live sessions. Read,
|
|
23
|
+
* never written.
|
|
24
|
+
*/
|
|
25
|
+
constructor({ sessionsById }) {
|
|
26
|
+
this.sessionsById = sessionsById;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every live session producing ONE output.
|
|
31
|
+
*
|
|
32
|
+
* The output is the address the encoding layer works in: two sessions whose
|
|
33
|
+
* output parameters agree ARE the same output, so what one of them measured
|
|
34
|
+
* about the machine is true of the other.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} address
|
|
37
|
+
* @returns {object[]}
|
|
38
|
+
*/
|
|
39
|
+
sessionsOn(address) {
|
|
40
|
+
const found = [];
|
|
41
|
+
for (const session of this.sessionsById.values()) {
|
|
42
|
+
if (session.outputKey === address && session.state !== "disposed") {
|
|
43
|
+
found.push(session);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every live session of one file: the picture, its quality steps, and the
|
|
51
|
+
* soundtracks published separately.
|
|
52
|
+
*
|
|
53
|
+
* The file is what they share, so the file is what this asks about. Where the
|
|
54
|
+
* sums this feeds are concerned that is also the right question: two pictures
|
|
55
|
+
* of one file are two encoders on one machine whether or not anybody thinks
|
|
56
|
+
* of them as one film.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} session
|
|
59
|
+
* @returns {object[]}
|
|
60
|
+
*/
|
|
61
|
+
familyOf(session) {
|
|
62
|
+
const family = [session];
|
|
63
|
+
const key = session?.file?.key;
|
|
64
|
+
for (const other of this.sessionsById.values()) {
|
|
65
|
+
if (other === session || other.state === "disposed" || other.file?.key !== key) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
family.push(other);
|
|
69
|
+
}
|
|
70
|
+
return family;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The soundtracks published separately for this picture, live ones only.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} base
|
|
77
|
+
* @returns {object[]}
|
|
78
|
+
*/
|
|
79
|
+
renditionsOf(base) {
|
|
80
|
+
return this.familyOf(base).filter((session) => session !== base && session.audioOnly === true);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The quality steps of this picture, live ones only.
|
|
85
|
+
*
|
|
86
|
+
* A step is a session made as one — `isStep`, written where it is created —
|
|
87
|
+
* and not merely "another session of this file that carries a picture". The
|
|
88
|
+
* difference is the second picture: one file can hold two — a browser that
|
|
89
|
+
* understands rendition groups and one that needs the sound muxed in produce
|
|
90
|
+
* two — and calling one of them a step of the other would let a switch away
|
|
91
|
+
* from a step stop the encoder of somebody else's picture.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} base
|
|
94
|
+
* @returns {object[]}
|
|
95
|
+
*/
|
|
96
|
+
stepsOf(base) {
|
|
97
|
+
return this.familyOf(base).filter((session) => session !== base && session.isStep === true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The picture a step belongs to, or the session itself when it is not a step.
|
|
102
|
+
*
|
|
103
|
+
* How a session came to be is a fact about it, not a link to another session
|
|
104
|
+
* — so a step whose picture has gone answers for itself rather than following
|
|
105
|
+
* a dead reference, and nothing has to be cleaned from the other side when
|
|
106
|
+
* one of them ends.
|
|
107
|
+
*
|
|
108
|
+
* One file can carry two pictures at once, and then this returns whichever
|
|
109
|
+
* was made first. Everything asked of the answer is a fact of the FILE and of
|
|
110
|
+
* this host: what heights can be offered, what a step costs, when the budget
|
|
111
|
+
* last acted. Two pictures of one file answer all of those alike.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} session
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
pictureOf(session) {
|
|
117
|
+
if (!session || session.isStep !== true) {
|
|
118
|
+
return session;
|
|
119
|
+
}
|
|
120
|
+
for (const other of this.familyOf(session)) {
|
|
121
|
+
if (other.isStep !== true && other.audioOnly !== true) {
|
|
122
|
+
return other;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return session;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Which variant a session IS, as a height. Zero encode height means "keep the
|
|
130
|
+
* source", so the source's own height is the answer.
|
|
131
|
+
*
|
|
132
|
+
* Settled once and then kept, because it is a NAME — the player addresses the
|
|
133
|
+
* variant by it for the whole session, having fetched the master exactly
|
|
134
|
+
* once. The height a session encodes at is not stable: the realtime budget
|
|
135
|
+
* steps it down when the host cannot keep up. Deriving the name afresh each
|
|
136
|
+
* time would mean a downshift silently renames the variant the viewer is
|
|
137
|
+
* watching, and the next segment request under the old name would build a
|
|
138
|
+
* SECOND session at the height the host had just proved it could not manage.
|
|
139
|
+
* A downshift changes the picture inside the variant instead, which is what
|
|
140
|
+
* it has always done.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} session
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
variantHeightOf(session) {
|
|
146
|
+
if (Number.isInteger(session.variantHeight) && session.variantHeight > 0) {
|
|
147
|
+
return session.variantHeight;
|
|
148
|
+
}
|
|
149
|
+
const encodeHeight = Number(session.output.encodeHeight) || 0;
|
|
150
|
+
session.variantHeight = encodeHeight > 0
|
|
151
|
+
? encodeHeight
|
|
152
|
+
: Math.round(Number(session.file.height) || 0);
|
|
153
|
+
return session.variantHeight;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The height a session's encoder is actually producing, or 0 when it produces
|
|
158
|
+
* no encoded picture of its own (a copy, or a soundtrack).
|
|
159
|
+
*
|
|
160
|
+
* A COPY must never be adopted: it costs no encoder at all, so handing it to
|
|
161
|
+
* a request for a re-encoded rung would give away the one thing this host can
|
|
162
|
+
* always serve.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} session
|
|
165
|
+
* @returns {number}
|
|
166
|
+
*/
|
|
167
|
+
producedHeightOf(session) {
|
|
168
|
+
if (!session || session.transcodeVideo !== true || session.audioOnly === true) {
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
return Math.round(Number(session.output.encodeHeight) || 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The heights this file's variants CAN be spliced at — a fact about the
|
|
176
|
+
* source and the cut grid, settled once and never moved.
|
|
177
|
+
*
|
|
178
|
+
* Separate from which of them are worth OFFERING to the viewer right now, on
|
|
179
|
+
* a machine whose load moves every five seconds. Both were the same list
|
|
180
|
+
* until 2026-08-18, and that is what broke playback outright: the browser is
|
|
181
|
+
* told at session creation that a master playlist exists, and 192 ms later —
|
|
182
|
+
* after the session's own encoder had started and the first supply reading
|
|
183
|
+
* had arrived — the live list had fallen from five rungs to one,
|
|
184
|
+
* `buildMasterPlaylist` returned null for having fewer than two, and the
|
|
185
|
+
* master answered 404 to the very session that had just published it. hls.js
|
|
186
|
+
* treats that as fatal and unrecoverable, so nothing played at all (session
|
|
187
|
+
* `4ef731d8`, "Moana (2016).mkv", 17:43:01).
|
|
188
|
+
*
|
|
189
|
+
* A live figure may decide what to offer. It may not decide whether a
|
|
190
|
+
* published document exists.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} session
|
|
193
|
+
* @returns {number[]} Largest first.
|
|
194
|
+
*/
|
|
195
|
+
splicableHeights(session) {
|
|
196
|
+
const owner = this.pictureOf(session);
|
|
197
|
+
if (Array.isArray(owner.splicableHeights)) {
|
|
198
|
+
return owner.splicableHeights;
|
|
199
|
+
}
|
|
200
|
+
const heights = new Set(variantHeightsFor(Number(owner.file.height) || 0));
|
|
201
|
+
const own = this.variantHeightOf(owner);
|
|
202
|
+
if (own > 0) {
|
|
203
|
+
heights.add(own);
|
|
204
|
+
}
|
|
205
|
+
owner.splicableHeights = [...heights].sort((left, right) => right - left);
|
|
206
|
+
return owner.splicableHeights;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether this stream publishes a master playlist at all — that is, whether
|
|
211
|
+
* there is anything for a player to move BETWEEN.
|
|
212
|
+
*
|
|
213
|
+
* Asked in one place because two callers depend on the same answer and used
|
|
214
|
+
* to compute it differently: the builder refused a copied stream whose cut
|
|
215
|
+
* grid is a fiction, while the budget looked only at how many heights could
|
|
216
|
+
* in principle be spliced. A copy with no readable keyframe index therefore
|
|
217
|
+
* had requests recorded against it — asking a player with no variants to
|
|
218
|
+
* change variant, once every window, for the whole film.
|
|
219
|
+
*
|
|
220
|
+
* @param {object} session
|
|
221
|
+
* @returns {boolean}
|
|
222
|
+
*/
|
|
223
|
+
publishesVariants(session) {
|
|
224
|
+
const owner = this.pictureOf(session);
|
|
225
|
+
// A copy can only be cut where the source already has a keyframe, so a rung
|
|
226
|
+
// meant to splice into it has to be cut at exactly those times. A copy that
|
|
227
|
+
// fell back to an even grid ffmpeg does not cut on has nothing to align to.
|
|
228
|
+
if (!owner.transcodeVideo && owner.timeline.cutGrid !== "keyframe") {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return this.splicableHeights(owner).length >= 2;
|
|
232
|
+
}
|
|
233
|
+
}
|