@torrent-tv/proxy 2.77.0 → 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.
@@ -1,272 +1,283 @@
1
- /**
2
- * @file How many encoders there should be on one output, and where each of them
3
- * belongs — decided from numbers alone.
4
- *
5
- * The decision is separated from carrying it out on purpose. Every rule below
6
- * was previously a condition somewhere inside an eleven-thousand-line file,
7
- * reachable only by starting a real ffmpeg, and each of them was written for
8
- * one viewer:
9
- *
10
- * - a run was placed at the position of whoever asked, and never at the first
11
- * thing missing, so a viewer moving into a stretch already on disk restarted
12
- * an encoder to make it a second time;
13
- * - a run had no end at all — neither `-to` nor `-t` appeared anywhere — so it
14
- * ran until something killed it, and two runs on one output could not exist
15
- * without writing over each other;
16
- * - nothing stopped a run that had caught up with material somebody else had
17
- * already made.
18
- *
19
- * The rule this file exists to express, stated by the user 2026-09-04:
20
- *
21
- * > Viewers are always independent and always reuse what can be reused. The
22
- * > number of encoders is however many are needed; how many are needed follows
23
- * > from which sets of output parameters are wanted and where the viewers stand
24
- * > inside each. Segments produced by ANY encoder are available to ANY viewer,
25
- * > and which viewer asked never enters the question.
26
- *
27
- * So no name of a viewer reaches this file. It is given what is wanted, what
28
- * exists, what is being made, and what the machine can afford.
29
- *
30
- * **What a viewer wants decides WHERE a run starts and WHETHER it is needed,
31
- * never where it stops.** A run's end comes from the coverage: it runs until it
32
- * meets material somebody else has made or is making, or until the end of the
33
- * film. Bounding it by the window instead was tried and is wrong, because a
34
- * window travels forward as the viewer plays: a second viewer whose cushion
35
- * reached two segments past the first run's end was given an encoder of their
36
- * own to make those two, while the run already there had nowhere left to go.
37
- * How far ahead of the viewers a run may get is a different question with its
38
- * own answer — the look-ahead, which suspends a run rather than bounding it.
39
- */
40
-
41
- import { endOfRun } from "./EncodeRun.js";
42
-
43
- /**
44
- * One encoder that is running now.
45
- *
46
- * @typedef {object} LiveRun
47
- * @property {string} id
48
- * @property {number} from - The first number it was given.
49
- * @property {number} to - The last number it was given, inclusive.
50
- * @property {number} head - The next number it will produce. Its position.
51
- * @property {number} speedX - Measured encode speed against realtime, from
52
- * ffmpeg's own progress. Zero or less means nothing has measured it yet, and
53
- * then no comparison involving its speed can be made.
54
- */
55
-
56
- /**
57
- * What a viewer is waiting for. Which viewer is deliberately absent.
58
- *
59
- * @typedef {object} WantedSpan
60
- * @property {number} from
61
- * @property {number} to
62
- */
63
-
64
- /**
65
- * @typedef {{ type: "start", from: number, to: number, because: string }
66
- * | { type: "move", run: object, from: number, to: number, because: string }
67
- * | { type: "stop", run: object, because: string }
68
- * | { type: "keep", run: object, from: number, to: number }} PlanAction
69
- */
70
-
71
- /**
72
- * Whether a stretch and a window touch at all.
73
- *
74
- * @param {number} fromA
75
- * @param {number} toA
76
- * @param {number} fromB
77
- * @param {number} toB
78
- * @returns {boolean}
79
- */
80
- function overlaps(fromA, toA, fromB, toB) {
81
- return fromA <= toB && fromB <= toA;
82
- }
83
-
84
- /**
85
- * Decide what to do with the encoders on one output.
86
- *
87
- * @param {object} params
88
- * @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
89
- * been made and what is being made.
90
- * @param {WantedSpan[]} params.windows - What viewers are waiting for, one
91
- * window each. Empty means nobody is watching this output.
92
- * @param {LiveRun[]} params.runs - The encoders running on it now.
93
- * @param {number} params.maxRuns - How many encoders this machine can afford on
94
- * this output. Comes from the same arithmetic that decides the quality offer;
95
- * it is measured per host and never chosen here.
96
- * @param {number} params.segmentSeconds - How much film one segment holds.
97
- * @param {number} params.restartCostSec - What it costs to stop an encoder and
98
- * start it somewhere else: process start plus opening the input. Measured —
99
- * 0.12 s on the addon host, 0.5-0.6 s on a desktop.
100
- * @returns {PlanAction[]} Stops first, then moves, then starts, so that a plan
101
- * carried out in order never holds two encoders where it means to hold one.
102
- */
103
- export function planEncoders({
104
- coverage,
105
- windows,
106
- runs,
107
- maxRuns,
108
- segmentSeconds,
109
- restartCostSec
110
- }) {
111
- /** @type {PlanAction[]} */
112
- const stops = [];
113
- /** @type {PlanAction[]} */
114
- const moves = [];
115
- /** @type {PlanAction[]} */
116
- const starts = [];
117
- /** @type {PlanAction[]} */
118
- const keeps = [];
119
-
120
- const wanted = Array.isArray(windows) ? windows : [];
121
- const live = Array.isArray(runs) ? runs : [];
122
-
123
- // Nobody is watching this output: every encoder on it is making segments for
124
- // no one. This is the case a look-ahead cannot answer, because look-ahead
125
- // asks how far AHEAD of a viewer a run is and there is no viewer.
126
- if (wanted.length === 0) {
127
- for (const run of live) {
128
- stops.push({ type: "stop", run, because: "nobody is watching this output" });
129
- }
130
- return stops;
131
- }
132
-
133
- // How far a search for a gap needs to look: past the furthest thing anybody
134
- // is waiting for there is nothing to decide about.
135
- const demandTo = Math.max(...wanted.map((span) => span.to));
136
-
137
- /** Runs that survive this pass. @type {Set<object>} */
138
- const surviving = new Set();
139
-
140
- for (const run of live) {
141
- // 1. Is anybody waiting for what this run was given? A run whose stretch
142
- // touches no window is making material nobody has asked for.
143
- const stillWanted = wanted.some((span) => overlaps(run.from, endOfRun(run), span.from, span.to));
144
- if (!stillWanted) {
145
- stops.push({ type: "stop", run, because: "nothing it was given is wanted" });
146
- continue;
147
- }
148
-
149
- // 2. Has it arrived at material that already exists, or that another run is
150
- // making? Its own claim does not count against it.
151
- const coveredAhead = coverage.coveredRunFrom(run.head, run);
152
- if (coveredAhead === 0) {
153
- surviving.add(run);
154
- keeps.push({ type: "keep", run, from: run.head, to: run.to });
155
- continue;
156
- }
157
-
158
- // Where it would go instead: the first thing nobody has and nobody is
159
- // making, at or after where it stands.
160
- const gap = coverage.firstGapFrom(run.head, demandTo, run);
161
- if (gap === null) {
162
- stops.push({
163
- type: "stop",
164
- run,
165
- because: "everything wanted ahead of it is already made or being made"
166
- });
167
- continue;
168
- }
169
-
170
- // Driving through costs its own encode time for material that exists.
171
- // Moving costs one restart. Both are measured; neither is chosen here.
172
- //
173
- // A run whose speed nothing has measured yet cannot be compared, and then
174
- // moving is the answer rather than a default: driving through is work that
175
- // is certainly wasted, while the restart is a known and small cost.
176
- const driveSec = run.speedX > 0 ? (coveredAhead * segmentSeconds) / run.speedX : null;
177
- if (driveSec !== null && driveSec <= restartCostSec) {
178
- surviving.add(run);
179
- keeps.push({ type: "keep", run, from: run.head, to: run.to });
180
- continue;
181
- }
182
-
183
- const free = coverage.freeRunFrom(gap, run);
184
- surviving.add(run);
185
- moves.push({
186
- type: "move",
187
- run,
188
- from: gap,
189
- to: endOfStretch(gap, free),
190
- because: driveSec === null
191
- ? `${coveredAhead} segment(s) ahead are already covered and its speed is not measured`
192
- : `driving through ${coveredAhead} covered segment(s) costs ${driveSec.toFixed(2)}s ` +
193
- `against ${restartCostSec.toFixed(2)}s to move`
194
- });
195
- }
196
-
197
- // 3. Gaps somebody is waiting for that nobody is making. Taken in the order
198
- // the viewers meet them the lowest first because that is the one a
199
- // viewer is stopped at, and the budget may not stretch to all of them.
200
- const gapsWanted = [];
201
- for (const span of wanted) {
202
- const gap = coverage.firstGapFrom(span.from, span.to);
203
- if (gap !== null) {
204
- gapsWanted.push(gap);
205
- }
206
- }
207
- const alreadyPlanned = new Set(moves.map((action) => /** @type {{from:number}} */ (action).from));
208
- const budget = Math.max(0, maxRuns - surviving.size);
209
- for (const gap of [...new Set(gapsWanted)].sort((left, right) => left - right)) {
210
- if (starts.length >= budget) {
211
- break;
212
- }
213
- if (alreadyPlanned.has(gap)) {
214
- continue;
215
- }
216
- const free = coverage.freeRunFrom(gap);
217
- if (free === 0) {
218
- continue;
219
- }
220
- alreadyPlanned.add(gap);
221
- starts.push({
222
- type: "start",
223
- from: gap,
224
- to: endOfStretch(gap, free),
225
- because: `#${gap} is wanted and nobody is making it`
226
- });
227
- }
228
-
229
- return [...stops, ...moves, ...starts, ...keeps];
230
- }
231
-
232
- /**
233
- * The last number of a stretch that begins at `from` and is `length` long.
234
- *
235
- * `-1` when the length is not finite, which is this layer's word for a run with
236
- * no end: the film's length is not known, so there is nothing to stop it at, and
237
- * a number invented here would be an end nobody measured.
238
- *
239
- * @param {number} from
240
- * @param {number} length
241
- * @returns {number}
242
- */
243
- function endOfStretch(from, length) {
244
- return Number.isFinite(length) ? from + Math.max(1, length) - 1 : -1;
245
- }
246
-
247
- /**
248
- * The lowest number a viewer is waiting for that is not ready — what the plan
249
- * is judged by.
250
- *
251
- * Not used to decide anything: it is the figure a log line carries, so that a
252
- * plan that keeps producing while a viewer waits is visible rather than
253
- * inferred.
254
- *
255
- * @param {import("./CoverageMap.js").CoverageMap} coverage
256
- * @param {WantedSpan[]} windows
257
- * @returns {number | null}
258
- */
259
- export function firstUnmetWant(coverage, windows) {
260
- let lowest = null;
261
- for (const span of windows ?? []) {
262
- for (let at = span.from; at <= span.to; at += 1) {
263
- if (!coverage.isReady(at)) {
264
- if (lowest === null || at < lowest) {
265
- lowest = at;
266
- }
267
- break;
268
- }
269
- }
270
- }
271
- return lowest;
272
- }
1
+ /**
2
+ * @file How many encoders there should be on one output, and where each of them
3
+ * belongs — decided from numbers alone.
4
+ *
5
+ * The decision is separated from carrying it out on purpose. Every rule below
6
+ * was previously a condition somewhere inside an eleven-thousand-line file,
7
+ * reachable only by starting a real ffmpeg, and each of them was written for
8
+ * one viewer:
9
+ *
10
+ * - a run was placed at the position of whoever asked, and never at the first
11
+ * thing missing, so a viewer moving into a stretch already on disk restarted
12
+ * an encoder to make it a second time;
13
+ * - a run had no end at all — neither `-to` nor `-t` appeared anywhere — so it
14
+ * ran until something killed it, and two runs on one output could not exist
15
+ * without writing over each other;
16
+ * - nothing stopped a run that had caught up with material somebody else had
17
+ * already made.
18
+ *
19
+ * The rule this file exists to express, stated by the user 2026-09-04:
20
+ *
21
+ * > Viewers are always independent and always reuse what can be reused. The
22
+ * > number of encoders is however many are needed; how many are needed follows
23
+ * > from which sets of output parameters are wanted and where the viewers stand
24
+ * > inside each. Segments produced by ANY encoder are available to ANY viewer,
25
+ * > and which viewer asked never enters the question.
26
+ *
27
+ * So no name of a viewer reaches this file. It is given what is wanted, what
28
+ * exists, what is being made, and what the machine can afford.
29
+ *
30
+ * **A viewer decides the ORDER the map is walked in and, through the budget, how
31
+ * many processes walk it. Nothing else.** Stated by the user 2026-09-05, and it
32
+ * is the rule the rest of this file now follows: while a file is being encoded
33
+ * it is encoded WHOLE, in the order the map dictates. Who wants which segment
34
+ * decides which gap is closed first, never whether a run may go on living.
35
+ *
36
+ * **A run is therefore never stopped for standing outside a viewer's window.**
37
+ * It used to be, and the two decisions that produced that were in direct
38
+ * contradictionmeasured in the field 2026-09-05 on a viewer watching an
39
+ * episode:
40
+ *
41
+ * 1. this file commanded a start inside the window, at #46;
42
+ * 2. `planRunInterval` in the session manager moved the start to #78, because
43
+ * it counted a suspended run's claim as reaching `head + look-ahead`;
44
+ * 3. this file then saw a run at #78 against a window of [27, 57], found no
45
+ * overlap, and killed it as "nothing it was given is wanted";
46
+ * 4. neither coverage nor demand had changed, so the same start was commanded
47
+ * again 350-700ms per cycle, dozens of times, no segment ever produced,
48
+ * the viewer's picture stopped for 125 seconds.
49
+ *
50
+ * Both of those other authorities are gone (roadmap item 76, step 5). What is
51
+ * left is this file, and the only reasons it stops a run are: nobody is
52
+ * watching the output at all; the machine affords fewer processes; or there is
53
+ * nothing left unmade anywhere in the track.
54
+ *
55
+ * **A run's end comes from the coverage**, never from a window: it runs until
56
+ * it meets material somebody else has made or is making, or until the end of
57
+ * the film.
58
+ */
59
+
60
+ /**
61
+ * One encoder that is running now.
62
+ *
63
+ * @typedef {object} LiveRun
64
+ * @property {string} id
65
+ * @property {number} from - The first number it was given.
66
+ * @property {number} to - The last number it was given, inclusive.
67
+ * @property {number} head - The next number it will produce. Its position.
68
+ * @property {number} speedX - Measured encode speed against realtime, from
69
+ * ffmpeg's own progress. Zero or less means nothing has measured it yet, and
70
+ * then no comparison involving its speed can be made.
71
+ */
72
+
73
+ /**
74
+ * What a viewer is waiting for. Which viewer is deliberately absent.
75
+ *
76
+ * @typedef {object} WantedSpan
77
+ * @property {number} from
78
+ * @property {number} to
79
+ */
80
+
81
+ /**
82
+ * @typedef {{ type: "start", from: number, to: number, because: string }
83
+ * | { type: "move", run: object, from: number, to: number, because: string }
84
+ * | { type: "stop", run: object, because: string }
85
+ * | { type: "keep", run: object, from: number, to: number }} PlanAction
86
+ */
87
+
88
+ /**
89
+ * Decide what to do with the encoders on one output.
90
+ *
91
+ * @param {object} params
92
+ * @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
93
+ * been made and what is being made.
94
+ * @param {WantedSpan[]} params.windows - What viewers are waiting for, one
95
+ * window each. Empty means nobody is watching this output.
96
+ * @param {LiveRun[]} params.runs - The encoders running on it now.
97
+ * @param {number} params.maxRuns - How many encoders this machine can afford on
98
+ * this output. Comes from the same arithmetic that decides the quality offer;
99
+ * it is measured per host and never chosen here.
100
+ * @param {number} params.segmentSeconds - How much film one segment holds.
101
+ * @param {number} params.restartCostSec - What it costs to stop an encoder and
102
+ * start it somewhere else: process start plus opening the input. Measured —
103
+ * 0.12 s on the addon host, 0.5-0.6 s on a desktop.
104
+ * @returns {PlanAction[]} Stops first, then moves, then starts, so that a plan
105
+ * carried out in order never holds two encoders where it means to hold one.
106
+ */
107
+ export function planEncoders({
108
+ coverage,
109
+ windows,
110
+ runs,
111
+ maxRuns,
112
+ segmentSeconds,
113
+ restartCostSec
114
+ }) {
115
+ /** @type {PlanAction[]} */
116
+ const stops = [];
117
+ /** @type {PlanAction[]} */
118
+ const moves = [];
119
+ /** @type {PlanAction[]} */
120
+ const starts = [];
121
+ /** @type {PlanAction[]} */
122
+ const keeps = [];
123
+
124
+ const wanted = Array.isArray(windows) ? windows : [];
125
+ const live = Array.isArray(runs) ? runs : [];
126
+
127
+ // Nobody is watching this output: every encoder on it is making segments for
128
+ // no one. This is the case a look-ahead cannot answer, because look-ahead
129
+ // asks how far AHEAD of a viewer a run is and there is no viewer.
130
+ if (wanted.length === 0) {
131
+ for (const run of live) {
132
+ stops.push({ type: "stop", run, because: "nobody is watching this output" });
133
+ }
134
+ return stops;
135
+ }
136
+
137
+ // How far a search for a gap needs to look: past the furthest thing anybody
138
+ // is waiting for there is nothing to decide about.
139
+ const demandTo = Math.max(...wanted.map((span) => span.to));
140
+
141
+ /** Runs that survive this pass. @type {Set<object>} */
142
+ const surviving = new Set();
143
+
144
+ for (const run of live) {
145
+ // 1. A RUN IS NEVER STOPPED FOR STANDING OUTSIDE A WINDOW. While a file is
146
+ // being encoded it is encoded whole; a viewer decides the ORDER the work
147
+ // is taken in and, through the budget, how many processes take it.
148
+ //
149
+ // This used to stop a run whose stretch touched no window, and that
150
+ // decision contradicted the one below it: the run was placed by a search
151
+ // the retention test did not accept, so it was killed on the pass after
152
+ // it started and started again in the same place — 350-700ms per cycle in
153
+ // the field on 2026-09-05, no segment ever produced, the viewer's picture
154
+ // stopped for 125 seconds.
155
+ //
156
+ // 2. Has it arrived at material that already exists, or that another run is
157
+ // making? Its own claim does not count against it.
158
+ const coveredAhead = coverage.coveredRunFrom(run.head, run);
159
+ if (coveredAhead === 0) {
160
+ surviving.add(run);
161
+ keeps.push({ type: "keep", run, from: run.head, to: run.to });
162
+ continue;
163
+ }
164
+
165
+ // Where it would go instead: the first thing nobody has and nobody is
166
+ // making, at or after where it stands.
167
+ const gap = coverage.firstGapFrom(run.head, demandTo, run);
168
+ if (gap === null) {
169
+ stops.push({
170
+ type: "stop",
171
+ run,
172
+ because: "everything wanted ahead of it is already made or being made"
173
+ });
174
+ continue;
175
+ }
176
+
177
+ // Driving through costs its own encode time for material that exists.
178
+ // Moving costs one restart. Both are measured; neither is chosen here.
179
+ //
180
+ // A run whose speed nothing has measured yet cannot be compared, and then
181
+ // moving is the answer rather than a default: driving through is work that
182
+ // is certainly wasted, while the restart is a known and small cost.
183
+ const driveSec = run.speedX > 0 ? (coveredAhead * segmentSeconds) / run.speedX : null;
184
+ if (driveSec !== null && driveSec <= restartCostSec) {
185
+ surviving.add(run);
186
+ keeps.push({ type: "keep", run, from: run.head, to: run.to });
187
+ continue;
188
+ }
189
+
190
+ const free = coverage.freeRunFrom(gap, run);
191
+ surviving.add(run);
192
+ moves.push({
193
+ type: "move",
194
+ run,
195
+ from: gap,
196
+ to: endOfStretch(gap, free),
197
+ because: driveSec === null
198
+ ? `${coveredAhead} segment(s) ahead are already covered and its speed is not measured`
199
+ : `driving through ${coveredAhead} covered segment(s) costs ${driveSec.toFixed(2)}s ` +
200
+ `against ${restartCostSec.toFixed(2)}s to move`
201
+ });
202
+ }
203
+
204
+ // 3. Gaps somebody is waiting for that nobody is making, IN THE ORDER THE
205
+ // DEMAND MAP PUTS THEM: most urgent zone first, and within one zone the
206
+ // lowest number, because that is where a viewer is stopped. The budget
207
+ // rarely stretches to every gap, so which one is taken first is the whole
208
+ // of what a viewer's presence decides.
209
+ const gapsWanted = [];
210
+ for (const span of [...wanted].sort(
211
+ (left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.from - right.from
212
+ )) {
213
+ const gap = coverage.firstGapFrom(span.from, span.to);
214
+ if (gap !== null) {
215
+ gapsWanted.push(gap);
216
+ }
217
+ }
218
+ const alreadyPlanned = new Set(moves.map((action) => /** @type {{from:number}} */ (action).from));
219
+ const budget = Math.max(0, maxRuns - surviving.size);
220
+ for (const gap of [...new Set(gapsWanted)]) {
221
+ if (starts.length >= budget) {
222
+ break;
223
+ }
224
+ if (alreadyPlanned.has(gap)) {
225
+ continue;
226
+ }
227
+ const free = coverage.freeRunFrom(gap);
228
+ if (free === 0) {
229
+ continue;
230
+ }
231
+ alreadyPlanned.add(gap);
232
+ starts.push({
233
+ type: "start",
234
+ from: gap,
235
+ to: endOfStretch(gap, free),
236
+ because: `#${gap} is wanted and nobody is making it`
237
+ });
238
+ }
239
+
240
+ return [...stops, ...moves, ...starts, ...keeps];
241
+ }
242
+
243
+ /**
244
+ * The last number of a stretch that begins at `from` and is `length` long.
245
+ *
246
+ * `-1` when the length is not finite, which is this layer's word for a run with
247
+ * no end: the film's length is not known, so there is nothing to stop it at, and
248
+ * a number invented here would be an end nobody measured.
249
+ *
250
+ * @param {number} from
251
+ * @param {number} length
252
+ * @returns {number}
253
+ */
254
+ function endOfStretch(from, length) {
255
+ return Number.isFinite(length) ? from + Math.max(1, length) - 1 : -1;
256
+ }
257
+
258
+ /**
259
+ * The lowest number a viewer is waiting for that is not ready — what the plan
260
+ * is judged by.
261
+ *
262
+ * Not used to decide anything: it is the figure a log line carries, so that a
263
+ * plan that keeps producing while a viewer waits is visible rather than
264
+ * inferred.
265
+ *
266
+ * @param {import("./CoverageMap.js").CoverageMap} coverage
267
+ * @param {WantedSpan[]} windows
268
+ * @returns {number | null}
269
+ */
270
+ export function firstUnmetWant(coverage, windows) {
271
+ let lowest = null;
272
+ for (const span of windows ?? []) {
273
+ for (let at = span.from; at <= span.to; at += 1) {
274
+ if (!coverage.isReady(at)) {
275
+ if (lowest === null || at < lowest) {
276
+ lowest = at;
277
+ }
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ return lowest;
283
+ }
Binary file