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