@torrent-tv/proxy 2.31.0 → 2.32.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 2.32.0
2
+
3
+ - **New**: The decode cost is fitted from a clip set that can be checked, and a term the measurements do not determine is refused instead of published as a zero. The set that shipped until now was three clips for three unknowns — an EXACT system, with two of the clips at the same pixel rate — and such a system cannot fail visibly: it returns whatever satisfies its equations. On 2026-08-17 it returned `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s`, so a film's own bitrate never entered its price, and the prediction built on it was 1.8-2.2x optimistic against the same file measured while playing. The new set is six clips — three sizes × two bitrates, the axes varied INDEPENDENTLY — cut from the same Netflix Open Content "Meridian" footage (CC BY 4.0, `assets/calibration/NOTICE.md`), 7.7 MB against 8.8 MB before. Three spare measurements give the fit a residual, and with it two questions it could not ask before: whether a term's whole effect across the measured range exceeds the scatter, and whether the coefficient exceeds its own standard error. A term that fails either is dropped, the rest are fitted again, and the log names it — a zero now means "not measured" only when it says so. A NEGATIVE coefficient is dropped too rather than clamped to zero: more pixels cannot cost less work, so a negative fit is noise beating an effect, not a discovery about the host. Measured on the developer's machine, the new set determines all three terms (`0.000520 × Mpx/s + 0.002086 × Mbit/s + 0.0033 s/s`, typical disagreement 0.0012 s/s), and the bitrate term it recovers matches the difference between the two 1080p clips to 15 %. The arithmetic is a pure module with the degenerate case as a test (`services/decode-cost-fit.js`).
4
+ - **Chore**: What the calibration costs at startup, measured rather than assumed: six clips take 14.8 s on the developer's Windows box, of which the decoding is about 50 ms per clip — an empty ffmpeg spawn there costs 774 ms, and opening each file most of the rest. Shortening the clips would therefore save nothing; the cost is spawning ffmpeg once per clip, and it is paid before any viewer exists.
5
+
1
6
  ## 2.31.0
2
7
 
3
8
  - **Chore**: The encoder run's two status strings are gone; both are now outputs of the state table shipped in 2.23.0. `session.progress.state` was maintained by hand at seven sites and `session.state` at nine, and neither could answer on its own — the warm-up test had to read both under an `||`, because one said "starting" from the first spawn until something overwrote it while the other said it again on its own schedule. What the browser is told is computed where it is sent (`wireState(runState)`), and `session.state` is reduced to the session's own lifetime: it exists, or it has been disposed. That deletes the line in the spawn path that read `state === "disposed" ? "disposed" : "starting"` — two lifetimes in one variable, which is what it was there to paper over. Verified before the change that nothing in the browser reads the wire string, so the value set is unchanged and unobserved either way; the four values it can take are the same four as before.
@@ -1,30 +1,63 @@
1
1
  # Calibration clips — attribution
2
2
 
3
- The three clips in this folder are excerpts from **"Meridian"**, part of
3
+ The clips in this folder are excerpts from **"Meridian"**, part of
4
4
  [Netflix Open Content](https://opencontent.netflix.com/), licensed under the
5
5
  **Creative Commons Attribution 4.0 International (CC BY 4.0)** licence:
6
6
  <https://creativecommons.org/licenses/by/4.0/>.
7
7
 
8
8
  They were cut from `Meridian/Meridian_UHD4k5994_HDR_P3PQ.mp4` and re-encoded to
9
- H.264 High, 24 fps, 5.04 s (121 frames), no audio:
9
+ H.264 High, 24 fps, 2 s each, no audio:
10
10
 
11
11
  | file | resolution | bitrate |
12
12
  |---|---|---|
13
- | `cal-1080-hi.mp4` | 1920×1080 | 11.38 Mbit/s |
14
- | `cal-1080-lo.mp4` | 1920×1080 | 0.97 Mbit/s |
15
- | `cal-720.mp4` | 1280×720 | 2.25 Mbit/s |
13
+ | `cal-h264-1080-hi.mp4` | 1920×1080 | 9.36 Mbit/s |
14
+ | `cal-h264-1080-lo.mp4` | 1920×1080 | 1.00 Mbit/s |
15
+ | `cal-h264-720-hi.mp4` | 1280×720 | 9.95 Mbit/s |
16
+ | `cal-h264-720-lo.mp4` | 1280×720 | 1.04 Mbit/s |
17
+ | `cal-h264-480-hi.mp4` | 854×480 | 9.48 Mbit/s |
18
+ | `cal-h264-480-lo.mp4` | 854×480 | 1.25 Mbit/s |
16
19
 
17
- ## Why these three, and why real footage
20
+ ## Why six, and why this grid
18
21
 
19
- `services/hwaccel.js` decodes them at startup and solves the host's decode cost,
20
- `a × Mpixel/s + b × Mbit/s + c`, from the three measurements. The first two
21
- clips share a pixel count and differ 11.7× in bitrate; the third changes the
22
- pixel count. Three points, three unknowns.
22
+ `services/hwaccel.js` decodes them at startup and fits the host's decode cost,
23
+ `a × Mpixel/s + b × Mbit/s + c`, from the measurements
24
+ (`services/decode-cost-fit.js`).
23
25
 
24
- Real, grainy live action rather than a generated pattern: measured 2026-08-14 on
25
- the addon host, this material decodes 11 % away from the film being served,
26
- where a generated `testsrc2` clip is 158 % away.
26
+ The set before this one was three clips for three unknowns — an EXACT system,
27
+ with two of the clips sharing a pixel count. Such a system cannot fail visibly:
28
+ it returns whatever satisfies its three equations, and on 2026-08-17 it returned
29
+ `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s` — the bitrate term and the
30
+ constant exactly zero, so a film's own bitrate never entered its price, and the
31
+ prediction built on it was 1.8-2.2x optimistic against the same file measured
32
+ while playing.
27
33
 
28
- Replacing a clip is allowed the benchmark reads each clip's dimensions, frame
29
- rate and bitrate from ffmpeg's own output rather than from this table — but the
30
- three must keep spanning the two axes, or the fit has nothing to separate.
34
+ Six clips are three sizes × two bitrates, with the two axes varied
35
+ **independently**. That gives three spare measurements, so the fit has a
36
+ residual something to notice a degeneracy WITH and lets a term be refused
37
+ when the data does not determine it, instead of being published as a zero that
38
+ looks measured.
39
+
40
+ A 480p clip at 9.5 Mbit/s is not content anyone ships; that is the point. The
41
+ bitrate has to vary at every size, or it cannot be told apart from the size.
42
+
43
+ ## Why real footage
44
+
45
+ Grainy live action rather than a generated pattern: measured 2026-08-14 on the
46
+ addon host, this material decodes 11 % away from the film being served, where a
47
+ generated `testsrc2` clip is 158 % away. It also matches most of what the
48
+ product actually plays — a survey of 133 releases (2026-07-10) found ~67 %
49
+ H.264 live action, the rest HEVC, XviD and animation.
50
+
51
+ ## Replacing or extending the set
52
+
53
+ Allowed: the benchmark reads each clip's dimensions, frame rate and bitrate from
54
+ ffmpeg's own output rather than from this table. Two rules hold, though:
55
+
56
+ - **more measurements than terms**, or there is no residual and no way to see a
57
+ degenerate fit;
58
+ - **the axes stay independent** — every size at both bitrates. Dropping one cell
59
+ reintroduces the collinearity that produced the zeros above.
60
+
61
+ Codecs other than H.264 (HEVC, AV1, 10-bit) decode dearer per pixel, and the
62
+ model fitted here does not describe them. Covering them means a set per codec
63
+ family and constants chosen by the source's own codec — roadmap item 3(b).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.31.0",
3
+ "version": "2.32.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -0,0 +1,331 @@
1
+ /**
2
+ * @file Fit the cost of decoding from measured clips — and say which terms the
3
+ * measurements actually determine.
4
+ *
5
+ * The model is `cost = pixelTerm × Mpixel/s + bitrateTerm × Mbit/s +
6
+ * constantTerm`, in seconds of work per second of content.
7
+ *
8
+ * What went wrong with the fit this replaces, measured 2026-08-17: three clips
9
+ * for three unknowns is an EXACT system, and two of those clips shared a pixel
10
+ * rate and differed only in bitrate. A system that near-singular does not fail
11
+ * loudly — it returns whatever satisfies the three equations, and on that boot
12
+ * it returned `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s`. The bitrate
13
+ * term and the constant were zero, so a film's own bitrate never entered its
14
+ * price, and the whole prediction was 1.8-2.2x optimistic against what the same
15
+ * file measured while playing.
16
+ *
17
+ * Two things are different here, and both are about honesty rather than
18
+ * accuracy:
19
+ *
20
+ * - the system is OVER-determined (six clips, three unknowns) and solved by
21
+ * least squares, so no single noisy reading decides a term;
22
+ * - a term is published only if the measurements determine it. The test is
23
+ * the coefficient against its OWN standard error: a term smaller than the
24
+ * uncertainty in it has not been measured, and stating it as a number —
25
+ * zero or otherwise — claims knowledge that is not there. It is dropped,
26
+ * the remaining terms are fitted again, and the caller is told which shape
27
+ * survived.
28
+ *
29
+ * There is no threshold to tune in that rule and no coefficient anywhere in
30
+ * this file. "Smaller than its own uncertainty" is a statement about the data,
31
+ * not a setting.
32
+ */
33
+
34
+ /**
35
+ * One measured clip.
36
+ *
37
+ * @typedef {object} DecodeSample
38
+ * @property {number} megapixelsPerSecond - Width × height × frame rate ÷ 1e6.
39
+ * @property {number} megabitsPerSecond - The clip's own measured bitrate.
40
+ * @property {number} costSecondsPerSecond - Seconds of work per second of
41
+ * content, from ffmpeg's own progress.
42
+ */
43
+
44
+ /**
45
+ * The fitted model, with the shape the data supported.
46
+ *
47
+ * @typedef {object} DecodeCostModel
48
+ * @property {number} pixelTerm
49
+ * @property {number} bitrateTerm
50
+ * @property {number} constantTerm
51
+ * @property {string} shape - Which terms were determined.
52
+ * @property {string[]} dropped - Terms the measurements could not determine.
53
+ * @property {number} samples
54
+ * @property {number} residualRms - Typical disagreement between the fit and a
55
+ * measurement, in the same units as the cost.
56
+ */
57
+
58
+ /** Every term the model can carry, in column order. */
59
+ const TERMS = ["pixels", "bitrate", "constant"];
60
+
61
+ /**
62
+ * Fit the model, dropping any term the measurements do not determine.
63
+ *
64
+ * @param {DecodeSample[]} samples
65
+ * @returns {DecodeCostModel | null} Null when nothing at all can be said: fewer
66
+ * measurements than terms, or no measurable dependence on the source.
67
+ */
68
+ export function fitDecodeCost(samples) {
69
+ const usable = (Array.isArray(samples) ? samples : []).filter(
70
+ (sample) =>
71
+ Number.isFinite(sample?.megapixelsPerSecond) &&
72
+ sample.megapixelsPerSecond > 0 &&
73
+ Number.isFinite(sample?.megabitsPerSecond) &&
74
+ sample.megabitsPerSecond >= 0 &&
75
+ Number.isFinite(sample?.costSecondsPerSecond) &&
76
+ sample.costSecondsPerSecond > 0
77
+ );
78
+ // Fitting three terms to three points is what produced the degenerate answer
79
+ // this file exists to prevent: it has no residual and therefore no way to
80
+ // notice that two of the points said the same thing.
81
+ if (usable.length < TERMS.length + 1) {
82
+ return null;
83
+ }
84
+
85
+ let columns = [...TERMS];
86
+ let dropped = [];
87
+ for (let attempt = 0; attempt < TERMS.length; attempt += 1) {
88
+ const fit = leastSquares(usable, columns);
89
+ if (!fit) {
90
+ return null;
91
+ }
92
+ // Which terms the data did not determine: a coefficient no larger than the
93
+ // uncertainty in it. Only one is dropped per pass — removing a column
94
+ // changes the uncertainty in the others, so the question has to be asked
95
+ // again of the smaller model.
96
+ const undetermined = columns
97
+ .map((name, index) => ({
98
+ name,
99
+ coefficient: fit.coefficients[index],
100
+ ratio: determination(usable, columns, name, fit.coefficients[index], fit.standardErrors[index], fit.residualRms)
101
+ }))
102
+ // The pixel term is the one relationship every measurement agrees on, and
103
+ // a model without it says nothing at all. If IT is undetermined the
104
+ // answer is no model, which the caller handles by pricing the encoder
105
+ // alone and refusing nothing.
106
+ // A NEGATIVE coefficient is undetermined whatever its ratio says. Every
107
+ // term here is physically non-negative — more pixels cannot cost less
108
+ // work, and neither can more bits — so a negative fit is noise winning
109
+ // over an effect, not a discovery about the host. Clamping it to zero
110
+ // instead, which the first version of this file did, publishes a zero
111
+ // that looks measured: exactly the dishonesty the module exists to
112
+ // remove.
113
+ .filter((term) => term.ratio < 1 || term.coefficient < 0)
114
+ .sort((left, right) => {
115
+ const negative = (term) => (term.coefficient < 0 ? 0 : 1);
116
+ return negative(left) - negative(right) || left.ratio - right.ratio;
117
+ });
118
+
119
+ if (undetermined.length === 0) {
120
+ return published(columns, fit, dropped, usable.length);
121
+ }
122
+ const weakest = undetermined[0].name;
123
+ if (weakest === "pixels") {
124
+ return null;
125
+ }
126
+ columns = columns.filter((name) => name !== weakest);
127
+ dropped = [...dropped, weakest];
128
+ if (columns.length === 0) {
129
+ return null;
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+
135
+
136
+ /**
137
+ * How well one term is determined, as a ratio against 1.
138
+ *
139
+ * Two ways a term can fail to be measured, and both have to be asked, because
140
+ * each is blind where the other sees:
141
+ *
142
+ * - **against the noise.** The term's effect across the range the clips
143
+ * actually cover — its coefficient times the spread of its own column — has
144
+ * to be larger than the typical disagreement between the fit and the
145
+ * measurements. A term whose whole contribution is smaller than the scatter
146
+ * was not measured; it was fitted to the scatter.
147
+ * - **against the arithmetic.** When a fit happens to be exact the scatter is
148
+ * zero, every standard error vanishes, and a coefficient of 7e-18 — which
149
+ * is how double-precision writes "nothing" — passes any test framed as a
150
+ * ratio. So the effect must also be larger than what the arithmetic itself
151
+ * can represent at this scale. That floor is a property of the numbers, not
152
+ * a setting: it moves with the size of the measurements.
153
+ *
154
+ * @param {DecodeSample[]} samples
155
+ * @param {string[]} columns
156
+ * @param {string} name
157
+ * @param {number} coefficient
158
+ * @param {number} standardError
159
+ * @param {number} residualRms
160
+ * @returns {number} Below 1 means the measurements do not determine this term.
161
+ */
162
+ function determination(samples, columns, name, coefficient, standardError, residualRms) {
163
+ const values = samples.map((sample) => {
164
+ if (name === "pixels") {
165
+ return sample.megapixelsPerSecond;
166
+ }
167
+ if (name === "bitrate") {
168
+ return sample.megabitsPerSecond;
169
+ }
170
+ return 1;
171
+ });
172
+ const spread = name === "constant" ? 1 : Math.max(...values) - Math.min(...values);
173
+ const effect = Math.abs(coefficient) * spread;
174
+ const meanCost = samples.reduce((sum, sample) => sum + sample.costSecondsPerSecond, 0) / samples.length;
175
+ const arithmeticFloor = Math.abs(meanCost) * Number.EPSILON * samples.length * columns.length;
176
+ const noiseFloor = Math.max(residualRms, arithmeticFloor);
177
+ const againstNoise = noiseFloor > 0 ? effect / noiseFloor : Number.POSITIVE_INFINITY;
178
+ const againstError = standardError > 0 ? Math.abs(coefficient) / standardError : Number.POSITIVE_INFINITY;
179
+ return Math.min(againstNoise, againstError);
180
+ }
181
+
182
+ /**
183
+ * Assemble the answer, with absent terms as zero — and `dropped` saying that
184
+ * the zero means "not measured" rather than "measured to be nothing".
185
+ *
186
+ * @param {string[]} columns
187
+ * @param {{ coefficients: number[], residualRms: number }} fit
188
+ * @param {string[]} dropped
189
+ * @param {number} samples
190
+ * @returns {DecodeCostModel | null}
191
+ */
192
+ function published(columns, fit, dropped, samples) {
193
+ const value = (name) => {
194
+ const index = columns.indexOf(name);
195
+ return index < 0 ? 0 : fit.coefficients[index];
196
+ };
197
+ const pixelTerm = value("pixels");
198
+ // A negative price for pixels is not a host being unusual — it is noise
199
+ // larger than the effect, and carrying it would price a bigger picture as
200
+ // cheaper than a smaller one.
201
+ if (!(pixelTerm > 0)) {
202
+ return null;
203
+ }
204
+ const constantTerm = value("constant");
205
+ return {
206
+ pixelTerm,
207
+ bitrateTerm: Math.max(0, value("bitrate")),
208
+ // A negative constant would make a small enough source free, which no
209
+ // measurement here supports: it is the fitted line crossing below zero
210
+ // outside the range that was measured.
211
+ constantTerm: Math.max(0, constantTerm),
212
+ shape: columns.join("+"),
213
+ dropped,
214
+ samples,
215
+ residualRms: fit.residualRms
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Least squares over the named columns, with the standard error of each
221
+ * coefficient.
222
+ *
223
+ * Solved through the normal equations. The matrix is at most 3×3 and the
224
+ * columns are of similar magnitude once a clip set varies them deliberately,
225
+ * which is exactly what the set is designed for.
226
+ *
227
+ * @param {DecodeSample[]} samples
228
+ * @param {string[]} columns
229
+ * @returns {{ coefficients: number[], standardErrors: number[], residualRms: number } | null}
230
+ */
231
+ function leastSquares(samples, columns) {
232
+ const rowOf = (sample) =>
233
+ columns.map((name) => {
234
+ if (name === "pixels") {
235
+ return sample.megapixelsPerSecond;
236
+ }
237
+ if (name === "bitrate") {
238
+ return sample.megabitsPerSecond;
239
+ }
240
+ return 1;
241
+ });
242
+ const width = columns.length;
243
+ const normal = Array.from({ length: width }, () => new Array(width).fill(0));
244
+ const right = new Array(width).fill(0);
245
+ for (const sample of samples) {
246
+ const row = rowOf(sample);
247
+ for (let i = 0; i < width; i += 1) {
248
+ right[i] += row[i] * sample.costSecondsPerSecond;
249
+ for (let j = 0; j < width; j += 1) {
250
+ normal[i][j] += row[i] * row[j];
251
+ }
252
+ }
253
+ }
254
+ const inverse = invert(normal);
255
+ if (!inverse) {
256
+ return null;
257
+ }
258
+ const coefficients = inverse.map((row) => row.reduce((sum, value, index) => sum + value * right[index], 0));
259
+
260
+ let residualSquares = 0;
261
+ for (const sample of samples) {
262
+ const row = rowOf(sample);
263
+ const predicted = row.reduce((sum, value, index) => sum + value * coefficients[index], 0);
264
+ residualSquares += (sample.costSecondsPerSecond - predicted) ** 2;
265
+ }
266
+ const degreesOfFreedom = samples.length - width;
267
+ const variance = degreesOfFreedom > 0 ? residualSquares / degreesOfFreedom : 0;
268
+ const standardErrors = inverse.map((row, index) => Math.sqrt(Math.max(0, variance * row[index])));
269
+ return {
270
+ coefficients,
271
+ standardErrors,
272
+ residualRms: Math.sqrt(residualSquares / samples.length)
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Invert a small symmetric matrix by Gauss-Jordan, or null when it is singular.
278
+ *
279
+ * @param {number[][]} matrix
280
+ * @returns {number[][] | null}
281
+ */
282
+ function invert(matrix) {
283
+ const size = matrix.length;
284
+ const work = matrix.map((row, index) => [
285
+ ...row,
286
+ ...Array.from({ length: size }, (_unused, column) => (column === index ? 1 : 0))
287
+ ]);
288
+ for (let column = 0; column < size; column += 1) {
289
+ let pivot = column;
290
+ for (let row = column + 1; row < size; row += 1) {
291
+ if (Math.abs(work[row][column]) > Math.abs(work[pivot][column])) {
292
+ pivot = row;
293
+ }
294
+ }
295
+ // Singular to the precision of the arithmetic: the columns are not
296
+ // independent, which is a statement about the clip set, not about the host.
297
+ if (Math.abs(work[pivot][column]) < 1e-12) {
298
+ return null;
299
+ }
300
+ [work[column], work[pivot]] = [work[pivot], work[column]];
301
+ const scale = work[column][column];
302
+ for (let k = 0; k < 2 * size; k += 1) {
303
+ work[column][k] /= scale;
304
+ }
305
+ for (let row = 0; row < size; row += 1) {
306
+ if (row === column) {
307
+ continue;
308
+ }
309
+ const factor = work[row][column];
310
+ for (let k = 0; k < 2 * size; k += 1) {
311
+ work[row][k] -= factor * work[column][k];
312
+ }
313
+ }
314
+ }
315
+ return work.map((row) => row.slice(size));
316
+ }
317
+
318
+ /**
319
+ * What a model prices one source at.
320
+ *
321
+ * @param {DecodeCostModel} model
322
+ * @param {{ megapixelsPerSecond: number, megabitsPerSecond: number }} source
323
+ * @returns {number} Seconds of work per second of content.
324
+ */
325
+ export function decodeCostOf(model, source) {
326
+ return (
327
+ model.pixelTerm * source.megapixelsPerSecond +
328
+ model.bitrateTerm * source.megabitsPerSecond +
329
+ model.constantTerm
330
+ );
331
+ }
@@ -24,6 +24,7 @@ import { spawn } from "node:child_process";
24
24
  import { mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
25
25
  import os from "node:os";
26
26
  import path from "node:path";
27
+ import { fitDecodeCost } from "./decode-cost-fit.js";
27
28
  import { fileURLToPath } from "node:url";
28
29
  import {
29
30
  parseFfmpegBitrateKbps,
@@ -670,45 +671,28 @@ export async function detectTonemapSupport({ ffmpegBin, logger }) {
670
671
  return supported;
671
672
  }
672
673
 
673
- /**
674
- * Solve a 3×3 linear system by Gaussian elimination with partial pivoting.
675
- *
676
- * @param {number[][]} rows - Three rows of [c0, c1, c2, rhs].
677
- * @returns {number[] | null} The three unknowns, or null when singular.
678
- */
679
- function solveLinear3(rows) {
680
- const m = rows.map((row) => [...row]);
681
- for (let col = 0; col < 3; col += 1) {
682
- let pivot = col;
683
- for (let row = col + 1; row < 3; row += 1) {
684
- if (Math.abs(m[row][col]) > Math.abs(m[pivot][col])) {
685
- pivot = row;
686
- }
687
- }
688
- if (Math.abs(m[pivot][col]) < 1e-12) {
689
- return null;
690
- }
691
- [m[col], m[pivot]] = [m[pivot], m[col]];
692
- for (let row = 0; row < 3; row += 1) {
693
- if (row === col) {
694
- continue;
695
- }
696
- const factor = m[row][col] / m[col][col];
697
- for (let k = col; k < 4; k += 1) {
698
- m[row][k] -= factor * m[col][k];
699
- }
700
- }
701
- }
702
- return [m[0][3] / m[0][0], m[1][3] / m[1][1], m[2][3] / m[2][2]];
703
- }
704
674
 
705
- // The clips the decode cost is solved from. They ship with the package
675
+ // The clips the decode cost is fitted from. They ship with the package
706
676
  // (`assets/calibration/`), cut from Netflix Open Content "Meridian" (CC-BY 4.0)
707
677
  // — real, grainy live action, because a generated `testsrc2` clip decodes 158 %
708
- // away from a real film where these are 11 % away (measured 2026-08-14). Two
709
- // share a pixel count and differ 11.7× in bitrate, the third has the same
710
- // bitrate class at fewer pixels: three points, three unknowns.
711
- const CALIBRATION_CLIPS = ["cal-1080-hi.mp4", "cal-1080-lo.mp4", "cal-720.mp4"];
678
+ // away from a real film where these are 11 % away (measured 2026-08-14).
679
+ //
680
+ // Three sizes at two bitrates each, with the axes varied INDEPENDENTLY. The set
681
+ // this replaced was three clips for three unknowns, two of them at the same
682
+ // size: an exact system, which cannot fail visibly. On 2026-08-17 it returned
683
+ // `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s` — the bitrate term and
684
+ // the constant exactly zero — and the prediction on top of it was 1.8-2.2x
685
+ // optimistic. Six points leave three spare, so the fit has a residual, and a
686
+ // term the data does not determine can be refused instead of published as a
687
+ // zero that looks measured. See `assets/calibration/NOTICE.md`.
688
+ const CALIBRATION_CLIPS = [
689
+ "cal-h264-1080-hi.mp4",
690
+ "cal-h264-1080-lo.mp4",
691
+ "cal-h264-720-hi.mp4",
692
+ "cal-h264-720-lo.mp4",
693
+ "cal-h264-480-hi.mp4",
694
+ "cal-h264-480-lo.mp4"
695
+ ];
712
696
  const CALIBRATION_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "calibration");
713
697
  // How wide the measured window must be before the slope is trusted, and how
714
698
  // long to wait for it at most. A second of decoding is thousands of frames on a
@@ -770,8 +754,8 @@ function parseClipCharacteristics(stderr) {
770
754
  export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBRATION_DIR }) {
771
755
  const log = logger ?? { info: () => {}, warn: () => {} };
772
756
  const startedAllAt = Date.now();
773
- /** @type {number[][]} */
774
- const equations = [];
757
+ /** @type {Array<{ megapixelsPerSecond: number, megabitsPerSecond: number, costSecondsPerSecond: number }>} */
758
+ const samples = [];
775
759
  for (const clip of CALIBRATION_CLIPS) {
776
760
  const measured = await measureDecodeSlope(ffmpegBin, path.join(clipsDir, clip));
777
761
  if (!measured) {
@@ -779,21 +763,31 @@ export async function benchmarkDecodeCost({ ffmpegBin, logger, clipsDir = CALIBR
779
763
  return null;
780
764
  }
781
765
  const cost = 1 / measured.speed;
782
- equations.push([measured.megapixelsPerSecond, measured.megabitsPerSecond, 1, cost]);
766
+ samples.push({
767
+ megapixelsPerSecond: measured.megapixelsPerSecond,
768
+ megabitsPerSecond: measured.megabitsPerSecond,
769
+ costSecondsPerSecond: cost
770
+ });
783
771
  log.info(
784
772
  `hwaccel: decode "${clip}" ${measured.megapixelsPerSecond.toFixed(1)} Mpx/s ` +
785
773
  `${measured.megabitsPerSecond.toFixed(2)} Mbit/s -> ${measured.speed.toFixed(1)}x ` +
786
774
  `(cost ${cost.toFixed(4)} s/s, over ${measured.windowSec.toFixed(1)}s of decoding)`
787
775
  );
788
776
  }
789
- const fitted = fitDecodeCost(equations);
777
+ const fitted = fitDecodeCost(samples);
790
778
  if (!fitted) {
791
779
  log.warn("hwaccel: decode cost could not be fitted to these measurements; decode cost unknown");
792
780
  return null;
793
781
  }
794
782
  log.info(
795
783
  `hwaccel: decode cost = ${fitted.pixelTerm.toFixed(6)} × Mpx/s + ${fitted.bitrateTerm.toFixed(6)} × Mbit/s ` +
796
- `+ ${fitted.constantTerm.toFixed(4)} s/s (${fitted.shape}, measured in ${((Date.now() - startedAllAt) / 1000).toFixed(1)}s)`
784
+ `+ ${fitted.constantTerm.toFixed(4)} s/s (${fitted.shape} from ${fitted.samples} clips, ` +
785
+ `typical disagreement ${fitted.residualRms.toFixed(4)} s/s` +
786
+ // Named rather than implied: a zero in the line above means "not
787
+ // measured" for a dropped term and "measured to be nothing" otherwise,
788
+ // and those are different claims.
789
+ (fitted.dropped.length > 0 ? `, ${fitted.dropped.join(" and ")} not determined by these clips` : "") +
790
+ `, measured in ${((Date.now() - startedAllAt) / 1000).toFixed(1)}s)`
797
791
  );
798
792
  return { pixelTerm: fitted.pixelTerm, bitrateTerm: fitted.bitrateTerm, constantTerm: fitted.constantTerm };
799
793
  }
@@ -911,65 +905,6 @@ function measureDecodeSlope(ffmpegBin, clipPath) {
911
905
  });
912
906
  }
913
907
 
914
- /**
915
- * Fit the three measurements, and say which shape the data supported.
916
- *
917
- * The three-term fit is exact — three points, three unknowns — and is used
918
- * whenever every term comes out non-negative. A negative term is not a host
919
- * being odd; it says the difference it was solved from is smaller than the
920
- * noise between runs, which is what a fast machine produces: measured on a
921
- * desktop, the 720p clip took LONGER per second than the low-bitrate 1080p one,
922
- * because process startup is a large share of a decode that takes a second.
923
- *
924
- * When that happens the bitrate term — the weak one, and the one solved from a
925
- * single difference — is dropped and the remaining two are fitted by least
926
- * squares over all three points. If even the pixel slope comes out non-positive
927
- * there is no measurable dependence on the source at all, and inventing one is
928
- * worse than having none: the caller then prices the encoder alone and refuses
929
- * nothing.
930
- *
931
- * @param {number[][]} equations - Rows of [Mpixel/s, Mbit/s, 1, cost].
932
- * @returns {{ pixelTerm: number, bitrateTerm: number, constantTerm: number, shape: string } | null}
933
- */
934
- function fitDecodeCost(equations) {
935
- const exact = solveLinear3(equations);
936
- if (exact && exact[0] > 0 && exact[1] >= 0 && exact[2] >= 0) {
937
- return { pixelTerm: exact[0], bitrateTerm: exact[1], constantTerm: exact[2], shape: "pixels+bitrate+constant" };
938
- }
939
- const count = equations.length;
940
- const meanPixels = equations.reduce((sum, row) => sum + row[0], 0) / count;
941
- const meanCost = equations.reduce((sum, row) => sum + row[3], 0) / count;
942
- let covariance = 0;
943
- let variance = 0;
944
- for (const row of equations) {
945
- covariance += (row[0] - meanPixels) * (row[3] - meanCost);
946
- variance += (row[0] - meanPixels) ** 2;
947
- }
948
- if (!(variance > 0)) {
949
- return null;
950
- }
951
- const pixelTerm = covariance / variance;
952
- const constantTerm = meanCost - pixelTerm * meanPixels;
953
- if (pixelTerm > 0 && constantTerm >= 0) {
954
- return { pixelTerm, bitrateTerm: 0, constantTerm, shape: "pixels+constant" };
955
- }
956
- // A negative constant is the line crossing below zero where no clip was
957
- // measured — every clip is 22 Mpixel/s or more, and nothing here says what a
958
- // tiny picture costs. Rather than carry a term that would price a small
959
- // source as free work, fit through the origin: cost proportional to pixels,
960
- // which is the relationship the measurements do support.
961
- let weighted = 0;
962
- let squares = 0;
963
- for (const row of equations) {
964
- weighted += row[0] * row[3];
965
- squares += row[0] ** 2;
966
- }
967
- const throughOrigin = squares > 0 ? weighted / squares : 0;
968
- if (!(throughOrigin > 0)) {
969
- return null;
970
- }
971
- return { pixelTerm: throughOrigin, bitrateTerm: 0, constantTerm: 0, shape: "pixels only" };
972
- }
973
908
 
974
909
  /**
975
910
  * How many times realtime this host can DECODE a source of these