@torrent-tv/proxy 2.80.8 → 2.80.10
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 +1675 -1658
- package/bin/cli.js +606 -596
- package/package.json +51 -50
- package/services/data-channel-handler.js +2 -4
- package/services/encode/CoverageMap.js +428 -401
- package/services/encode/EncodePlan.js +0 -3
- package/services/encode/EncodeRun.js +14 -0
- package/services/encode/SegmentStore.js +718 -669
- package/services/hls-session-manager.js +44 -162
- package/services/hwaccel.js +2 -2
- package/services/memory-report.js +596 -592
- package/services/orchestrators/EncodeOrchestrator.js +143 -11
- package/services/quality/EncodeCost.js +555 -555
- package/test/auto-quality-step.test.js +514 -507
- package/test/contention.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +187 -0
- package/test/coverage-map.test.js +195 -178
- package/test/encode-orchestrator.test.js +1 -2
- package/test/encode-plan-viewers.test.js +18 -19
- package/test/encode-plan.test.js +3 -4
- package/test/held-request-width.test.js +155 -154
- package/test/helpers/encode-run.js +136 -128
- package/test/orchestrator-wired.test.js +16 -11
- package/test/produced-copy-choice.test.js +358 -327
- package/test/run-intervals.test.js +100 -100
- package/test/segment-serve-wiring.test.js +76 -20
- package/test/segment-store.test.js +216 -187
- package/test/stale-request-after-seek.test.js +51 -77
- package/test/tracks-begin-together.test.js +176 -153
- package/test/viewer-outputs.test.js +278 -273
- package/test/behind-head-repair.test.js +0 -261
- /package/services/{contention.js → encode/contention.js} +0 -0
- /package/{test/decode-cost.test.js → test-measured/decode-cost.measured.js} +0 -0
- /package/{test/decode-measurement.test.js → test-measured/decode-measurement.measured.js} +0 -0
|
@@ -29,7 +29,7 @@ import { endOfRun } from "../encode/EncodeRun.js";
|
|
|
29
29
|
import { ENCODE_EXIT } from "../encode/encode-exit.js";
|
|
30
30
|
import { affordableRuns } from "../encode/run-budget.js";
|
|
31
31
|
import { RunCosts } from "../encode/run-costs.js";
|
|
32
|
-
import { contentionPenalty } from "../contention.js";
|
|
32
|
+
import { contentionPenalty } from "../encode/contention.js";
|
|
33
33
|
import { SegmentDemand } from "../encode/SegmentDemand.js";
|
|
34
34
|
|
|
35
35
|
export class EncodeOrchestrator {
|
|
@@ -55,6 +55,9 @@ export class EncodeOrchestrator {
|
|
|
55
55
|
/** The last reason a budget was cut, so the same one is not said twice. */
|
|
56
56
|
#lastBudgetReason = new Map();
|
|
57
57
|
|
|
58
|
+
/** The last unmet want said out loud, so a stuck one is said once. */
|
|
59
|
+
#lastUnmet = new Map();
|
|
60
|
+
|
|
58
61
|
/**
|
|
59
62
|
* @param {object} params
|
|
60
63
|
* @param {(address: string) => number} params.maxRunsFor - How many encoders
|
|
@@ -65,7 +68,7 @@ export class EncodeOrchestrator {
|
|
|
65
68
|
* for a stretch. What to read, what to map and how to cut belong to whoever
|
|
66
69
|
* knows the source.
|
|
67
70
|
* @param {number} params.segmentSeconds
|
|
68
|
-
* @param {import("../contention.js").ContentionPenalties | null}
|
|
71
|
+
* @param {import("../encode/contention.js").ContentionPenalties | null}
|
|
69
72
|
* [params.contentionPenalties] - How much slower one encoder runs beside
|
|
70
73
|
* others, MEASURED on this host at startup and keyed by how many others
|
|
71
74
|
* there are. Null until something has measured it, and then the penalty is
|
|
@@ -126,6 +129,73 @@ export class EncodeOrchestrator {
|
|
|
126
129
|
return map;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Put the map's picture of what is ready back in step with the disk.
|
|
134
|
+
*
|
|
135
|
+
* ONE AUTHORITY ON WHAT EXISTS, AND IT IS THE STORE. The map holds no memory
|
|
136
|
+
* of readiness between calls: it is handed the whole answer, replacing
|
|
137
|
+
* whatever it had, immediately before anything is decided from it. So a
|
|
138
|
+
* segment whose file was discarded with the run that had it open, dropped to
|
|
139
|
+
* make room, or reopened by a run restarting on it stops being ready in the
|
|
140
|
+
* same breath — without anything having to notice and say so.
|
|
141
|
+
*
|
|
142
|
+
* The map used to be filled from outside, by the session manager, with a
|
|
143
|
+
* method that only ever added. Nothing anywhere took a number back. Field
|
|
144
|
+
* 2026-09-07: the map claimed all 482 segments of a film while the directory
|
|
145
|
+
* held nothing, so every arrangement scored perfect, the only encoder was
|
|
146
|
+
* stopped as unnecessary and none was placed again — two sessions in a row
|
|
147
|
+
* with no picture at all.
|
|
148
|
+
*
|
|
149
|
+
* A store is optional here only in the sense that an authority which was not
|
|
150
|
+
* supplied cannot be consulted: without one the map keeps what it was told
|
|
151
|
+
* directly, which is how this class is exercised with plain numbers.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} address
|
|
154
|
+
* @returns {CoverageMap}
|
|
155
|
+
*/
|
|
156
|
+
#upToDateCoverage(address) {
|
|
157
|
+
const coverage = this.coverageOf(address);
|
|
158
|
+
if (this.segmentStore) {
|
|
159
|
+
coverage.setReady(this.segmentStore.provenNumbers(address));
|
|
160
|
+
}
|
|
161
|
+
return coverage;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Where a run started here must stop: the free stretch in front of it.
|
|
166
|
+
*
|
|
167
|
+
* Asked of the one map, brought up to date first. It used to be worked out by
|
|
168
|
+
* the session manager, which reached into this layer for the map and into the
|
|
169
|
+
* store for what was on the disk and put the two together itself — one fact
|
|
170
|
+
* with two owners and a third party carrying it between them, which is how the
|
|
171
|
+
* two came to disagree.
|
|
172
|
+
*
|
|
173
|
+
* @param {object} params
|
|
174
|
+
* @param {string} params.address
|
|
175
|
+
* @param {number} params.from - Where the run will start.
|
|
176
|
+
* @param {object | null} [params.exceptRun] - The run being replaced, whose
|
|
177
|
+
* own claim is not somebody else's material.
|
|
178
|
+
* @param {number} [params.segmentCount] - The output's length, when known.
|
|
179
|
+
* @returns {number} The last number to work through, or `-1` for the end of
|
|
180
|
+
* the film.
|
|
181
|
+
*/
|
|
182
|
+
freeStretchEnd({ address, from, exceptRun = null, segmentCount = 0 }) {
|
|
183
|
+
if (!address) {
|
|
184
|
+
return -1;
|
|
185
|
+
}
|
|
186
|
+
if (segmentCount > 0) {
|
|
187
|
+
this.coverageOf(address).setSegmentCount(segmentCount);
|
|
188
|
+
}
|
|
189
|
+
const coverage = this.#upToDateCoverage(address);
|
|
190
|
+
const start = Math.max(0, from);
|
|
191
|
+
const free = coverage.freeRunFrom(start, exceptRun);
|
|
192
|
+
if (!Number.isFinite(free)) {
|
|
193
|
+
return -1;
|
|
194
|
+
}
|
|
195
|
+
const end = start + Math.max(1, free) - 1;
|
|
196
|
+
return segmentCount > 0 && end >= segmentCount - 1 ? -1 : end;
|
|
197
|
+
}
|
|
198
|
+
|
|
129
199
|
/**
|
|
130
200
|
* @param {string} address
|
|
131
201
|
* @returns {import("../encode/EncodeRun.js").EncodeRun[]}
|
|
@@ -153,7 +223,9 @@ export class EncodeOrchestrator {
|
|
|
153
223
|
* @param {Iterable<number>} indexes
|
|
154
224
|
*/
|
|
155
225
|
noteAlreadyMade(address, indexes) {
|
|
156
|
-
|
|
226
|
+
for (const index of indexes) {
|
|
227
|
+
this.noteProduced(address, index);
|
|
228
|
+
}
|
|
157
229
|
}
|
|
158
230
|
|
|
159
231
|
/**
|
|
@@ -200,6 +272,11 @@ export class EncodeOrchestrator {
|
|
|
200
272
|
* @param {number} index
|
|
201
273
|
*/
|
|
202
274
|
noteProduced(address, index) {
|
|
275
|
+
// TOLD TO THE AUTHORITY, not only to the map. A piece being closed is a fact
|
|
276
|
+
// about the disk, and the store is what holds those; told to the map alone
|
|
277
|
+
// it would survive exactly until the next time the map is brought back into
|
|
278
|
+
// step, and then be gone with no file to show for it.
|
|
279
|
+
this.segmentStore?.markClosed(address, index);
|
|
203
280
|
this.coverageOf(address).markReady(index);
|
|
204
281
|
for (const run of this.runsOn(address)) {
|
|
205
282
|
run.noteProduced(index);
|
|
@@ -258,7 +335,10 @@ export class EncodeOrchestrator {
|
|
|
258
335
|
* @param {string} address
|
|
259
336
|
*/
|
|
260
337
|
#reconcileOne(address) {
|
|
261
|
-
|
|
338
|
+
// WHAT EXISTS IS ASKED OF THE DISK, HERE, EVERY TIME. The plan is arithmetic
|
|
339
|
+
// over what is made, what is being made and what is wanted, and the first of
|
|
340
|
+
// those is not this layer's to remember.
|
|
341
|
+
const coverage = this.#upToDateCoverage(address);
|
|
262
342
|
// A run that has ended and said nothing. One built here reports its own
|
|
263
343
|
// ending and is released by `noteEnded`; one ADOPTED from elsewhere — a
|
|
264
344
|
// session whose encoder stopped — has no such promise, and its claim would
|
|
@@ -290,6 +370,10 @@ export class EncodeOrchestrator {
|
|
|
290
370
|
// lives where it belongs to nobody.
|
|
291
371
|
const windows = this.demand.mapOn(address);
|
|
292
372
|
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
373
|
+
// Asked ONCE. It is arithmetic over measurements, but it also says out loud
|
|
374
|
+
// when the reason it cuts the budget changes, so asking it three times in
|
|
375
|
+
// one pass is three chances to say a thing that happened once.
|
|
376
|
+
const maxRuns = this.#affordableOn(address, live);
|
|
293
377
|
const actions = planEncoders({
|
|
294
378
|
coverage,
|
|
295
379
|
windows,
|
|
@@ -297,7 +381,7 @@ export class EncodeOrchestrator {
|
|
|
297
381
|
// each; what it hands back names the run by BEING it, so nothing has to
|
|
298
382
|
// invent a token to refer to one by.
|
|
299
383
|
runs: live,
|
|
300
|
-
maxRuns
|
|
384
|
+
maxRuns,
|
|
301
385
|
segmentSeconds: this.segmentSeconds,
|
|
302
386
|
// What a start and a kill cost, measured from this host's own runs rather
|
|
303
387
|
// than written into the code from one machine's reading. Zero until
|
|
@@ -335,7 +419,7 @@ export class EncodeOrchestrator {
|
|
|
335
419
|
if (actions.some((action) => action.type === "move")) {
|
|
336
420
|
this.logger.info(
|
|
337
421
|
`encode-plan move on ${address}: windows=${JSON.stringify(windows)} ` +
|
|
338
|
-
`maxRuns=${
|
|
422
|
+
`maxRuns=${maxRuns} ` +
|
|
339
423
|
`live=${JSON.stringify(live.map((run) => ({ from: run.from, to: run.to, head: run.head, speedX: run.speedX })))}`
|
|
340
424
|
);
|
|
341
425
|
}
|
|
@@ -376,6 +460,35 @@ export class EncodeOrchestrator {
|
|
|
376
460
|
// `-to` and simply stops when its head meets somebody else's claim.
|
|
377
461
|
coverage.claim(action.run, action.from, endOfRun({ from: action.from, to: action.to }));
|
|
378
462
|
}
|
|
463
|
+
|
|
464
|
+
// NOBODY IS MAKING WHAT SOMEBODY IS WAITING FOR. Said here, with the numbers
|
|
465
|
+
// the decision was taken from, because it is the one state in which a viewer
|
|
466
|
+
// waits for ever and every line above it reads as a healthy proxy.
|
|
467
|
+
//
|
|
468
|
+
// Field 2026-09-07, twice in one evening: the last word about an output was
|
|
469
|
+
// "the film is no worse off without it", and after it nothing — no run, no
|
|
470
|
+
// refusal, no answer to the browser's request for the header. The wait ended
|
|
471
|
+
// at the browser's own timeout with a message naming no cause, and the
|
|
472
|
+
// proxy's log named none either.
|
|
473
|
+
//
|
|
474
|
+
// SAID ONCE PER STATE, not once per pass. A stuck output is reconciled on
|
|
475
|
+
// every event that touches it, and a line repeated for as long as the state
|
|
476
|
+
// lasts is what buried the last one: 49 295 copies of `send queue stuck` in
|
|
477
|
+
// a 159 090-line file, 31 % of the log, all of one wedge.
|
|
478
|
+
const wanting = firstUnmetWant(coverage, windows);
|
|
479
|
+
const stillRunning = this.runsOn(address).filter((run) => run.isAlive);
|
|
480
|
+
if (wanting === null || stillRunning.length > 0) {
|
|
481
|
+
this.#lastUnmet.delete(address);
|
|
482
|
+
} else if (this.#lastUnmet.get(address) !== wanting) {
|
|
483
|
+
this.#lastUnmet.set(address, wanting);
|
|
484
|
+
const held = this.segmentStore ? this.segmentStore.filesHeld(address) : -1;
|
|
485
|
+
this.logger.warn(
|
|
486
|
+
`encode: #${wanting} of ${address} is wanted and NO ENCODER IS MAKING IT — ` +
|
|
487
|
+
`ready=${coverage.stats().ready} of ${coverage.segmentCount} ` +
|
|
488
|
+
`files=${held < 0 ? "?" : held} maxRuns=${maxRuns} ` +
|
|
489
|
+
`windows=${JSON.stringify(windows)}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
379
492
|
}
|
|
380
493
|
|
|
381
494
|
/**
|
|
@@ -417,11 +530,17 @@ export class EncodeOrchestrator {
|
|
|
417
530
|
const onThisOutput = this.#runs.get(address) ?? [];
|
|
418
531
|
onThisOutput.push(run);
|
|
419
532
|
this.#runs.set(address, onThisOutput);
|
|
420
|
-
// This run rewrites
|
|
421
|
-
//
|
|
533
|
+
// This run rewrites the stretch it was given, so what was closed inside that
|
|
534
|
+
// stretch is no longer closed. Without this a number closed by an earlier run
|
|
422
535
|
// stays servable while a later one is halfway through writing it again.
|
|
423
|
-
|
|
424
|
-
|
|
536
|
+
//
|
|
537
|
+
// Bounded by the run's own end, which is the same number the claim below
|
|
538
|
+
// carries. Unbounded it unproved the whole film beyond the start of any run,
|
|
539
|
+
// and readiness is now a projection of what is proven — so a one-segment run
|
|
540
|
+
// at the beginning would have declared the rest of the output unmade.
|
|
541
|
+
const runsTo = endOfRun({ from, to });
|
|
542
|
+
this.segmentStore?.forgetClosed(address, from, runsTo);
|
|
543
|
+
this.coverageOf(address).claim(run, from, runsTo);
|
|
425
544
|
run.start(because);
|
|
426
545
|
}
|
|
427
546
|
|
|
@@ -579,8 +698,21 @@ export class EncodeOrchestrator {
|
|
|
579
698
|
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.from - right.from)
|
|
580
699
|
.map((w) => `p${w.priority ?? 0}:#${w.from}..#${w.to}`)
|
|
581
700
|
.join(" ");
|
|
701
|
+
// THE WHOLE ADDRESS. Cut to sixty characters, every output of one film
|
|
702
|
+
// printed the same string — the picture, its quality steps and each
|
|
703
|
+
// soundtrack are told apart only by the tail — so three lines of this
|
|
704
|
+
// could not be matched to the three things they describe. Read on
|
|
705
|
+
// 2026-09-07 while accounting for a session that produced nothing, and
|
|
706
|
+
// the accounting had to be done by which line carried a run.
|
|
707
|
+
//
|
|
708
|
+
// And WHAT THE DISK HOLDS beside what is proven closed. They are two
|
|
709
|
+
// different statements: files with nothing proving them closed reads as a
|
|
710
|
+
// reporting fault, no files at all reads as an output yet to be made, and
|
|
711
|
+
// the difference decides where to look.
|
|
712
|
+
const held = this.segmentStore ? this.segmentStore.filesHeld(address) : -1;
|
|
582
713
|
parts.push(
|
|
583
|
-
`${address
|
|
714
|
+
`${address} ready=${coverage.stats().ready}` +
|
|
715
|
+
`${held < 0 ? "" : ` of ${held} file(s) on disk`} ` +
|
|
584
716
|
`zones=[${zones}] runs=[${runs}] ` +
|
|
585
717
|
`waiting=${waiting === null ? "nobody" : `#${waiting}`}`
|
|
586
718
|
);
|