@torrent-tv/proxy 2.75.0 → 2.76.1
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 +1509 -1461
- package/CLAUDE.md +11 -1
- package/biome.json +182 -1
- package/docs/container-architecture.md +2 -1
- package/docs/encode-run-state.md +1 -1
- package/knip.json +14 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +1 -1
- package/routes/api/transcode-sessions/post.js +185 -185
- package/routes/api/transcode-sessions/progress/get.js +5 -1
- package/routes/transcode/audio-file/get.js +11 -1
- package/routes/transcode/audio-warm/get.js +11 -1
- package/routes/transcode/session-file/get.js +1 -1
- package/routes/transcode/variant-file/get.js +10 -1
- package/scripts/render-run-graph.js +2 -2
- package/server.js +25 -0
- package/services/audio-inventory.js +206 -201
- package/services/container/AviContainer.js +1 -1
- package/services/container/Container.js +33 -1
- package/services/container/MatroskaContainer.js +1 -1
- package/services/container/Mp4Container.js +1 -1
- package/services/container/SubtitleFileContainer.js +0 -1
- package/services/controllers/SubtitleController.js +128 -128
- package/services/demand/index.js +7 -10
- package/services/download/registry.js +0 -14
- package/services/encode/CoverageMap.js +281 -0
- package/services/encode/EncodePlan.js +255 -0
- package/services/encode/EncodeRun.js +587 -0
- package/services/encode/Encoder.js +84 -0
- package/services/encode/NvencEncoder.js +45 -0
- package/services/encode/QsvEncoder.js +47 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +529 -0
- package/services/encode/SoftwareEncoder.js +111 -0
- package/services/encode/V4l2m2mEncoder.js +53 -0
- package/services/encode/VaapiEncoder.js +53 -0
- package/services/encode/args.js +200 -0
- package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
- package/services/encode/index.js +9 -0
- package/services/encode/run-command.js +647 -0
- package/services/hls-session-manager.js +11100 -10711
- package/services/hwaccel.js +1688 -1992
- package/services/orchestrators/EncodeOrchestrator.js +359 -0
- package/services/output/LiveOutputs.js +213 -0
- package/services/output/Output.js +94 -0
- package/services/output/OutputSpec.js +195 -0
- package/services/output/Timeline.js +220 -0
- package/services/output/index.js +1 -0
- package/services/output/ladder.js +26 -0
- package/services/playback-planner.js +806 -775
- package/services/produced-index.js +222 -300
- package/services/source/SourceFile.js +346 -0
- package/services/{sidecar-files.js → torrent/files.js} +107 -11
- package/services/torrent/naming.js +619 -0
- package/services/torrent-worker/client.js +10 -0
- package/services/torrent-worker/container-tracks.js +71 -43
- package/services/torrent-worker/pool-adapter.js +18 -0
- package/services/torrent-worker/protocol.js +7 -0
- package/services/torrent-worker/subtitle-cues.js +549 -549
- package/services/torrent-worker/worker.js +18 -0
- package/services/tracks/TextSubtitleTrack.js +287 -287
- package/services/tracks/index.js +15 -14
- package/services/viewer/Viewer.js +145 -0
- package/services/viewer/Viewers.js +124 -0
- package/test/auto-quality-step.test.js +508 -506
- package/test/behind-head-repair.test.js +17 -7
- package/test/coverage-map.test.js +153 -0
- package/test/cut-times-timeline.test.js +6 -5
- package/test/cuts-follow-published-grid.test.js +4 -4
- package/test/decode-cost.test.js +31 -12
- package/test/encode-exit.test.js +1 -1
- package/test/encode-orchestrator.test.js +196 -0
- package/test/encode-plan.test.js +245 -0
- package/test/encode-run-state.test.js +2 -2
- package/test/encode-run.test.js +168 -0
- package/test/encoder-kinds.test.js +122 -0
- package/test/held-request-width.test.js +9 -3
- package/test/helpers/encode-run.js +128 -0
- package/test/keyframe-index-accuracy.test.js +19 -12
- package/test/keyframes-belong-to-the-file.test.js +132 -0
- package/test/orchestrator-wired.test.js +164 -0
- package/test/output-shape.test.js +68 -0
- package/test/output-spec.test.js +157 -0
- package/test/produced-copy-choice.test.js +58 -92
- package/test/produced-index.test.js +142 -188
- package/test/quality-variants.test.js +1079 -1075
- package/test/run-graph-drift.test.js +1 -1
- package/test/run-intervals.test.js +329 -0
- package/test/run-position-follows-published-grid.test.js +4 -4
- package/test/seek-landing.test.js +8 -8
- package/test/seek-target-not-superseded.test.js +21 -9
- package/test/segment-demand.test.js +82 -0
- package/test/segment-serve-wiring.test.js +47 -52
- package/test/segment-store.test.js +187 -0
- package/test/segments-are-shared.test.js +175 -0
- package/test/sidecar-naming.test.js +142 -0
- package/test/source-file.test.js +133 -0
- package/test/stale-request-after-seek.test.js +18 -12
- package/test/subtitle-language.test.js +252 -252
- package/test/timeline.test.js +95 -0
- package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
- package/test/torrent-naming.test.js +255 -0
- package/test/tracks-begin-together.test.js +44 -32
- package/test/two-viewers-one-picture.test.js +347 -0
- package/test/viewer-outputs.test.js +273 -0
- package/test/viewer.test.js +91 -0
- package/utils/perf.js +1 -63
- package/services/container/index.js +0 -6
- package/services/controllers/index.js +0 -2
- package/services/download/index.js +0 -8
- package/services/orchestrators/index.js +0 -2
- /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
- /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
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
|
+
/**
|
|
42
|
+
* One encoder that is running now.
|
|
43
|
+
*
|
|
44
|
+
* @typedef {object} LiveRun
|
|
45
|
+
* @property {string} id
|
|
46
|
+
* @property {number} from - The first number it was given.
|
|
47
|
+
* @property {number} to - The last number it was given, inclusive.
|
|
48
|
+
* @property {number} head - The next number it will produce. Its position.
|
|
49
|
+
* @property {number} speedX - Measured encode speed against realtime, from
|
|
50
|
+
* ffmpeg's own progress. Zero or less means nothing has measured it yet, and
|
|
51
|
+
* then no comparison involving its speed can be made.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What a viewer is waiting for. Which viewer is deliberately absent.
|
|
56
|
+
*
|
|
57
|
+
* @typedef {object} WantedSpan
|
|
58
|
+
* @property {number} from
|
|
59
|
+
* @property {number} to
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {{ type: "start", from: number, to: number, because: string }
|
|
64
|
+
* | { type: "move", run: object, from: number, to: number, because: string }
|
|
65
|
+
* | { type: "stop", run: object, because: string }
|
|
66
|
+
* | { type: "keep", run: object, from: number, to: number }} PlanAction
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether a stretch and a window touch at all.
|
|
71
|
+
*
|
|
72
|
+
* @param {number} fromA
|
|
73
|
+
* @param {number} toA
|
|
74
|
+
* @param {number} fromB
|
|
75
|
+
* @param {number} toB
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function overlaps(fromA, toA, fromB, toB) {
|
|
79
|
+
return fromA <= toB && fromB <= toA;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Decide what to do with the encoders on one output.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} params
|
|
86
|
+
* @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
|
|
87
|
+
* been made and what is being made.
|
|
88
|
+
* @param {WantedSpan[]} params.windows - What viewers are waiting for, one
|
|
89
|
+
* window each. Empty means nobody is watching this output.
|
|
90
|
+
* @param {LiveRun[]} params.runs - The encoders running on it now.
|
|
91
|
+
* @param {number} params.maxRuns - How many encoders this machine can afford on
|
|
92
|
+
* this output. Comes from the same arithmetic that decides the quality offer;
|
|
93
|
+
* it is measured per host and never chosen here.
|
|
94
|
+
* @param {number} params.segmentSeconds - How much film one segment holds.
|
|
95
|
+
* @param {number} params.restartCostSec - What it costs to stop an encoder and
|
|
96
|
+
* start it somewhere else: process start plus opening the input. Measured —
|
|
97
|
+
* 0.12 s on the addon host, 0.5-0.6 s on a desktop.
|
|
98
|
+
* @returns {PlanAction[]} Stops first, then moves, then starts, so that a plan
|
|
99
|
+
* carried out in order never holds two encoders where it means to hold one.
|
|
100
|
+
*/
|
|
101
|
+
export function planEncoders({
|
|
102
|
+
coverage,
|
|
103
|
+
windows,
|
|
104
|
+
runs,
|
|
105
|
+
maxRuns,
|
|
106
|
+
segmentSeconds,
|
|
107
|
+
restartCostSec
|
|
108
|
+
}) {
|
|
109
|
+
/** @type {PlanAction[]} */
|
|
110
|
+
const stops = [];
|
|
111
|
+
/** @type {PlanAction[]} */
|
|
112
|
+
const moves = [];
|
|
113
|
+
/** @type {PlanAction[]} */
|
|
114
|
+
const starts = [];
|
|
115
|
+
/** @type {PlanAction[]} */
|
|
116
|
+
const keeps = [];
|
|
117
|
+
|
|
118
|
+
const wanted = Array.isArray(windows) ? windows : [];
|
|
119
|
+
const live = Array.isArray(runs) ? runs : [];
|
|
120
|
+
|
|
121
|
+
// Nobody is watching this output: every encoder on it is making segments for
|
|
122
|
+
// no one. This is the case a look-ahead cannot answer, because look-ahead
|
|
123
|
+
// asks how far AHEAD of a viewer a run is and there is no viewer.
|
|
124
|
+
if (wanted.length === 0) {
|
|
125
|
+
for (const run of live) {
|
|
126
|
+
stops.push({ type: "stop", run, because: "nobody is watching this output" });
|
|
127
|
+
}
|
|
128
|
+
return stops;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// How far a search for a gap needs to look: past the furthest thing anybody
|
|
132
|
+
// is waiting for there is nothing to decide about.
|
|
133
|
+
const demandTo = Math.max(...wanted.map((span) => span.to));
|
|
134
|
+
|
|
135
|
+
/** Runs that survive this pass. @type {Set<object>} */
|
|
136
|
+
const surviving = new Set();
|
|
137
|
+
|
|
138
|
+
for (const run of live) {
|
|
139
|
+
// 1. Is anybody waiting for what this run was given? A run whose stretch
|
|
140
|
+
// touches no window is making material nobody has asked for.
|
|
141
|
+
const stillWanted = wanted.some((span) => overlaps(run.from, run.to, span.from, span.to));
|
|
142
|
+
if (!stillWanted) {
|
|
143
|
+
stops.push({ type: "stop", run, because: "nothing it was given is wanted" });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Has it arrived at material that already exists, or that another run is
|
|
148
|
+
// making? Its own claim does not count against it.
|
|
149
|
+
const coveredAhead = coverage.coveredRunFrom(run.head, run);
|
|
150
|
+
if (coveredAhead === 0) {
|
|
151
|
+
surviving.add(run);
|
|
152
|
+
keeps.push({ type: "keep", run, from: run.head, to: run.to });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Where it would go instead: the first thing nobody has and nobody is
|
|
157
|
+
// making, at or after where it stands.
|
|
158
|
+
const gap = coverage.firstGapFrom(run.head, demandTo, run);
|
|
159
|
+
if (gap === null) {
|
|
160
|
+
stops.push({
|
|
161
|
+
type: "stop",
|
|
162
|
+
run,
|
|
163
|
+
because: "everything wanted ahead of it is already made or being made"
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Driving through costs its own encode time for material that exists.
|
|
169
|
+
// Moving costs one restart. Both are measured; neither is chosen here.
|
|
170
|
+
//
|
|
171
|
+
// A run whose speed nothing has measured yet cannot be compared, and then
|
|
172
|
+
// moving is the answer rather than a default: driving through is work that
|
|
173
|
+
// is certainly wasted, while the restart is a known and small cost.
|
|
174
|
+
const driveSec = run.speedX > 0 ? (coveredAhead * segmentSeconds) / run.speedX : null;
|
|
175
|
+
if (driveSec !== null && driveSec <= restartCostSec) {
|
|
176
|
+
surviving.add(run);
|
|
177
|
+
keeps.push({ type: "keep", run, from: run.head, to: run.to });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const free = coverage.freeRunFrom(gap, run);
|
|
182
|
+
surviving.add(run);
|
|
183
|
+
moves.push({
|
|
184
|
+
type: "move",
|
|
185
|
+
run,
|
|
186
|
+
from: gap,
|
|
187
|
+
to: gap + Math.max(1, free) - 1,
|
|
188
|
+
because: driveSec === null
|
|
189
|
+
? `${coveredAhead} segment(s) ahead are already covered and its speed is not measured`
|
|
190
|
+
: `driving through ${coveredAhead} covered segment(s) costs ${driveSec.toFixed(2)}s ` +
|
|
191
|
+
`against ${restartCostSec.toFixed(2)}s to move`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 3. Gaps somebody is waiting for that nobody is making. Taken in the order
|
|
196
|
+
// the viewers meet them — the lowest first — because that is the one a
|
|
197
|
+
// viewer is stopped at, and the budget may not stretch to all of them.
|
|
198
|
+
const gapsWanted = [];
|
|
199
|
+
for (const span of wanted) {
|
|
200
|
+
const gap = coverage.firstGapFrom(span.from, span.to);
|
|
201
|
+
if (gap !== null) {
|
|
202
|
+
gapsWanted.push(gap);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const alreadyPlanned = new Set(moves.map((action) => /** @type {{from:number}} */ (action).from));
|
|
206
|
+
const budget = Math.max(0, maxRuns - surviving.size);
|
|
207
|
+
for (const gap of [...new Set(gapsWanted)].sort((left, right) => left - right)) {
|
|
208
|
+
if (starts.length >= budget) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
if (alreadyPlanned.has(gap)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const free = coverage.freeRunFrom(gap);
|
|
215
|
+
if (free === 0) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
alreadyPlanned.add(gap);
|
|
219
|
+
starts.push({
|
|
220
|
+
type: "start",
|
|
221
|
+
from: gap,
|
|
222
|
+
to: gap + free - 1,
|
|
223
|
+
because: `#${gap} is wanted and nobody is making it`
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return [...stops, ...moves, ...starts, ...keeps];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The lowest number a viewer is waiting for that is not ready — what the plan
|
|
232
|
+
* is judged by.
|
|
233
|
+
*
|
|
234
|
+
* Not used to decide anything: it is the figure a log line carries, so that a
|
|
235
|
+
* plan that keeps producing while a viewer waits is visible rather than
|
|
236
|
+
* inferred.
|
|
237
|
+
*
|
|
238
|
+
* @param {import("./CoverageMap.js").CoverageMap} coverage
|
|
239
|
+
* @param {WantedSpan[]} windows
|
|
240
|
+
* @returns {number | null}
|
|
241
|
+
*/
|
|
242
|
+
export function firstUnmetWant(coverage, windows) {
|
|
243
|
+
let lowest = null;
|
|
244
|
+
for (const span of windows ?? []) {
|
|
245
|
+
for (let at = span.from; at <= span.to; at += 1) {
|
|
246
|
+
if (!coverage.isReady(at)) {
|
|
247
|
+
if (lowest === null || at < lowest) {
|
|
248
|
+
lowest = at;
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return lowest;
|
|
255
|
+
}
|