@torrent-tv/proxy 2.12.2 → 2.14.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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * @file What a re-encode costs on this host, decoding included.
3
+ *
4
+ * The budget used to price the encoder alone. Measured on the addon host
5
+ * 2026-08-14, that made it offer a 240p rung it then ran at 0.388-0.947x while
6
+ * claiming the host cleared the bar 2.5 times over — the error on that rung was
7
+ * 209 %. The decode term brings a controlled measurement to within 5 %.
8
+ *
9
+ * The first test runs the real benchmark against the shipped clips with the
10
+ * real ffmpeg, because a fit that only ever runs against invented numbers can
11
+ * be wrong in every way that matters (2.9.124: a module tested only through its
12
+ * own exports missed the caller that never called it).
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { createRequire } from "node:module";
18
+ import { spawn } from "node:child_process";
19
+ import { mkdtemp, rm } from "node:fs/promises";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ import {
23
+ benchmarkDecodeCost,
24
+ canSustainOutput,
25
+ decodeSpeedFor,
26
+ predictedRealtimeSpeed,
27
+ REALTIME_SPEED_MARGIN
28
+ } from "../services/hwaccel.js";
29
+ import { HlsSessionManager, sourceDecodeCharacteristics } from "../services/hls-session-manager.js";
30
+ import { parseFfmpegBitrateKbps, parseFfmpegVideoDimensions, parseFfmpegVideoFps } from "../services/ffmpeg-banner.js";
31
+ import { fmp4Format } from "../services/segment-formats/fmp4.js";
32
+
33
+ const require = createRequire(import.meta.url);
34
+ const ffmpegBin = require("ffmpeg-static");
35
+ const CALIBRATION_DIR = path.join(import.meta.dirname, "..", "assets", "calibration");
36
+
37
+ // The constants measured on the addon host (CM4) on 2026-08-14. Used here as
38
+ // a fixed host, so the arithmetic can be checked against figures that were
39
+ // measured rather than against figures this test invented.
40
+ const ADDON_HOST_MODEL = { pixelTerm: 0.005555, bitrateTerm: 0.00990, constantTerm: 0.0572 };
41
+ // The film measured that day: 1920x1080 at 24 fps, about 8 Mbit/s.
42
+ const MEASURED_FILM = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
43
+
44
+ test("the fit comes out of the real clips, and predicts one of them back", async () => {
45
+ const model = await benchmarkDecodeCost({ ffmpegBin });
46
+
47
+ assert.ok(model, "the clips ship with the package and this host has ffmpeg");
48
+ assert.ok(model.pixelTerm > 0, "more pixels cannot decode faster");
49
+ assert.ok(Number.isFinite(model.bitrateTerm) && Number.isFinite(model.constantTerm));
50
+
51
+ // What the model must get right is how cost SCALES from one source to
52
+ // another — that is the whole of its job, since it is asked about rungs
53
+ // nobody has decoded. So: a bigger, richer source is never cheaper.
54
+ //
55
+ // Deliberately not a numeric bound. This suite runs its files in parallel and
56
+ // the benchmark is a live measurement, so the fit it produces depends on what
57
+ // else the machine was doing: on this desktop the same clips have solved to
58
+ // pixels+bitrate+constant, to pixels alone, and — under load — to a
59
+ // constant-dominated shape whose 720p/1080p ratio was 1.32 rather than the
60
+ // ~2.4 of a quiet run. That instability is real and is recorded against
61
+ // roadmap item 1; pinning a number here would only pin how busy the machine
62
+ // happened to be. What the FIGURES are worth is checked where it is quiet:
63
+ // against the addon host's recorded constants below, and against the real
64
+ // film in the field.
65
+ const clip720 = { megapixelsPerSecond: (1280 * 720 * 24) / 1e6, megabitsPerSecond: 2.248 };
66
+ const clip1080 = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 11.375 };
67
+ const ratio = decodeSpeedFor(model, clip720) / decodeSpeedFor(model, clip1080);
68
+
69
+ assert.ok(
70
+ ratio >= 1,
71
+ `720p at a fifth of the bitrate cannot decode slower than 1080p; the fit says ${ratio.toFixed(2)}x`
72
+ );
73
+ });
74
+
75
+ test("decode cost prices the film it was checked against", () => {
76
+ const speed = decodeSpeedFor(ADDON_HOST_MODEL, MEASURED_FILM);
77
+
78
+ // 0.005555 x 49.77 + 0.00990 x 8 + 0.0572 = 0.4129 s per second of video.
79
+ assert.ok(Math.abs(1 / speed - 0.4129) < 0.001, `cost was ${(1 / speed).toFixed(4)} s/s`);
80
+ // Measured that day: 0.434 s/s. The claim is 5 %, not exactness.
81
+ assert.ok(Math.abs(1 / speed - 0.434) / 0.434 < 0.05);
82
+ });
83
+
84
+ test("decoding and encoding share the machine, so their speeds combine", () => {
85
+ // The rung that broke playback: 240p at 24 fps, encoded at 5.99x by the
86
+ // measurement, decoded at 2.31x. Measured combined speed was 1.48x.
87
+ const outputPixelsPerSec = 426 * 240 * 24;
88
+ const speed = predictedRealtimeSpeed({
89
+ decodeModel: { pixelTerm: 0, bitrateTerm: 0, constantTerm: 1 / 2.31 },
90
+ encodePixelsPerSec: outputPixelsPerSec * 5.99,
91
+ outputPixelsPerSec,
92
+ source: MEASURED_FILM
93
+ });
94
+
95
+ assert.ok(Math.abs(speed - 1.67) < 0.01, `predicted ${speed.toFixed(2)}x`);
96
+ // 1.67 against the 1.48x that rung measured under a controlled encode: 12.8 %
97
+ // out. The figure is pinned so a change to the combination has to state what
98
+ // it does to it.
99
+ assert.ok(Math.abs(speed - 1.48) / 1.48 < 0.13);
100
+ });
101
+
102
+ test("with no decode fit the prediction is the encoder alone — what it was before", () => {
103
+ const outputPixelsPerSec = 426 * 240 * 24;
104
+ const speed = predictedRealtimeSpeed({
105
+ decodeModel: null,
106
+ encodePixelsPerSec: outputPixelsPerSec * 5.99,
107
+ outputPixelsPerSec,
108
+ source: MEASURED_FILM
109
+ });
110
+
111
+ assert.equal(speed, 5.99, "the old figure, four times the truth on that rung");
112
+ });
113
+
114
+ test("a rung is refused when the combined speed is under the margin", () => {
115
+ // The addon host's fastest preset, read from its own log: 11.2 Mpx/s.
116
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
117
+ const source = MEASURED_FILM;
118
+
119
+ const heavy = canSustainOutput({
120
+ benchmark,
121
+ decodeModel: ADDON_HOST_MODEL,
122
+ source,
123
+ outputPixelsPerSec: 1280 * 720 * 24
124
+ });
125
+ assert.equal(heavy.sustainable, false, "720p needs 22 Mpx/s from an 11.2 Mpx/s host");
126
+ assert.ok(heavy.speed < REALTIME_SPEED_MARGIN);
127
+
128
+ // And this is the gap the model does NOT close: the 240p rung predicts 1.58x
129
+ // and clears a margin of 1.5, while the field measured that same rung at
130
+ // 0.388-0.947x under real load — a host simultaneously copying 1080p,
131
+ // downloading the torrent and pushing segments. The prediction is honest for
132
+ // an idle machine; the margin is what has to carry the load, and 1.5 does not
133
+ // carry it. Pinned here so the arithmetic is not rediscovered from a log.
134
+ const light = canSustainOutput({
135
+ benchmark,
136
+ decodeModel: ADDON_HOST_MODEL,
137
+ source,
138
+ outputPixelsPerSec: 426 * 240 * 24
139
+ });
140
+ assert.ok(Math.abs(light.speed - 1.58) < 0.01, `predicted ${light.speed.toFixed(2)}x`);
141
+ assert.equal(light.sustainable, true);
142
+ });
143
+
144
+ test("a reading from the running encoder outranks the model of the clips", () => {
145
+ // The field case, 2026-08-14: the 240p rung of that film ran at 0.95x at its
146
+ // best on a host whose fastest preset benchmarked at 11.2 Mpx/s. Subtracting
147
+ // the encode half of that reading leaves what the SOURCE costs to decode
148
+ // there — 0.83 s per second of video, i.e. 1.20x — which is four times what
149
+ // the H.264 clips predicted for it, and is the truth about this file.
150
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
151
+ const observedDecodeCostSec = 1 / 0.95 - (426 * 240 * 24) / 11.2e6;
152
+ assert.ok(Math.abs(observedDecodeCostSec - 0.8335) < 0.001);
153
+
154
+ const withObservation = canSustainOutput({
155
+ benchmark,
156
+ decodeModel: ADDON_HOST_MODEL,
157
+ source: MEASURED_FILM,
158
+ outputPixelsPerSec: 426 * 240 * 24,
159
+ observedDecodeCostSec
160
+ });
161
+
162
+ // The same rung the clip model called 1.58x and admitted is now priced at
163
+ // 0.95x — the speed it was actually seen to run at — and refused.
164
+ assert.ok(Math.abs(withObservation.speed - 0.95) < 0.01, `priced ${withObservation.speed.toFixed(2)}x`);
165
+ assert.equal(withObservation.sustainable, false);
166
+ });
167
+
168
+ test("an observation prices a host whose clips were never fitted", () => {
169
+ // No model at all — clips missing, or a fit rejected. Before an observation
170
+ // nothing can be refused; after one, the same rung is priced and refused.
171
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
172
+ const outputPixelsPerSec = 426 * 240 * 24;
173
+
174
+ const unpriced = canSustainOutput({
175
+ benchmark,
176
+ decodeModel: null,
177
+ source: MEASURED_FILM,
178
+ outputPixelsPerSec
179
+ });
180
+ assert.deepEqual(unpriced, { speed: null, sustainable: true });
181
+
182
+ const priced = canSustainOutput({
183
+ benchmark,
184
+ decodeModel: null,
185
+ source: MEASURED_FILM,
186
+ outputPixelsPerSec,
187
+ observedDecodeCostSec: 0.8335
188
+ });
189
+ assert.equal(priced.sustainable, false);
190
+ assert.ok(priced.speed < 1, `priced ${priced.speed?.toFixed(2)}x`);
191
+ });
192
+
193
+ test("nothing measured means nothing refused", () => {
194
+ const verdict = canSustainOutput({
195
+ benchmark: [],
196
+ decodeModel: null,
197
+ source: null,
198
+ outputPixelsPerSec: 1920 * 1080 * 24
199
+ });
200
+
201
+ assert.deepEqual(verdict, { speed: null, sustainable: true });
202
+ });
203
+
204
+ test("a real ffmpeg banner reads into the figures the budget prices", async () => {
205
+ // The chain a session actually runs: ffmpeg prints its banner, the four
206
+ // readers take the four facts out of it, and `sourceDecodeCharacteristics`
207
+ // turns them into the two the fit uses. Run against a real file, because the
208
+ // bitrate reader is new and a banner is the one input nobody can invent
209
+ // faithfully.
210
+ const stderr = await new Promise((resolve) => {
211
+ let text = "";
212
+ const child = spawn(
213
+ ffmpegBin,
214
+ ["-hide_banner", "-loglevel", "info", "-i", path.join(CALIBRATION_DIR, "cal-720.mp4"), "-t", "0.1", "-f", "null", "-"],
215
+ { stdio: ["ignore", "ignore", "pipe"], windowsHide: true }
216
+ );
217
+ child.stderr.on("data", (chunk) => {
218
+ text += String(chunk);
219
+ });
220
+ child.on("close", () => resolve(text));
221
+ });
222
+
223
+ // The VIDEO stream's rate (2248), not the container's (2252). On a clip that
224
+ // carries nothing else the two differ only by container overhead; on a film
225
+ // with two AC-3 tracks they differ by the whole of the audio, and the fit
226
+ // these figures feed was made from clips decoded with `-an`.
227
+ assert.equal(parseFfmpegBitrateKbps(stderr), 2248, "the video stream's own bitrate, in kb/s");
228
+ const dimensions = parseFfmpegVideoDimensions(stderr);
229
+ const figures = sourceDecodeCharacteristics({
230
+ width: dimensions.width,
231
+ height: dimensions.height,
232
+ fps: parseFfmpegVideoFps(stderr),
233
+ bitrateKbps: parseFfmpegBitrateKbps(stderr)
234
+ });
235
+ assert.ok(Math.abs(figures.megapixelsPerSecond - (1280 * 720 * 24) / 1e6) < 0.01);
236
+ assert.ok(Math.abs(figures.megabitsPerSecond - 2.248) < 0.001);
237
+ });
238
+
239
+ test("the video stream's bitrate is preferred, and the output's is never read", () => {
240
+ // A real banner states three rates: the container's, the input video
241
+ // stream's, and — below "Stream mapping:" — the one ffmpeg is about to
242
+ // produce. Only the middle one describes the source.
243
+ const banner = [
244
+ " Duration: 01:31:19.00, start: 0.000000, bitrate: 9500 kb/s",
245
+ " Stream #0:0: Video: h264 (High), yuv420p, 1920x1080, 7800 kb/s, 23.98 fps",
246
+ " Stream #0:1: Audio: ac3, 48000 Hz, 5.1, fltp, 640 kb/s",
247
+ "Stream mapping:",
248
+ " Stream #0:0(und): Video: wrapped_avframe, yuv420p, 1920x1080, q=2-31, 200 kb/s, 24 fps"
249
+ ].join("\n");
250
+ assert.equal(parseFfmpegBitrateKbps(banner), 7800);
251
+
252
+ // No per-stream rate: the container's stands in, as it always did.
253
+ const noStreamRate = [
254
+ " Duration: 00:10:00.00, bitrate: 4200 kb/s",
255
+ " Stream #0:0: Video: hevc (Main 10), yuv420p10le, 1920x1080, 23.98 fps"
256
+ ].join("\n");
257
+ assert.equal(parseFfmpegBitrateKbps(noStreamRate), 4200);
258
+ });
259
+
260
+ test("a banner with no bitrate reads as no bitrate, not as zero", () => {
261
+ assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000000\n"), null);
262
+ assert.equal(parseFfmpegBitrateKbps(""), null);
263
+ assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000, bitrate: 8000 kb/s"), 8000);
264
+ });
265
+
266
+ test("the source's decode figures come off the probe, or not at all", () => {
267
+ assert.deepEqual(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 }), {
268
+ megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
269
+ megabitsPerSecond: 8
270
+ });
271
+ assert.equal(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: null }), null);
272
+ assert.equal(sourceDecodeCharacteristics(null), null);
273
+ });
274
+
275
+ test("the master playlist drops the rungs the host cannot hold", async (t) => {
276
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "decode-cost-"));
277
+ const manager = new HlsSessionManager({
278
+ enabled: true,
279
+ ffmpegBin: "ffmpeg",
280
+ localBindHost: "127.0.0.1",
281
+ localPort: 9090,
282
+ // A host that can encode a little: 3 Mpx/s, so only the smallest rungs of a
283
+ // 1080p source can be produced faster than they are watched.
284
+ softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 3e6 }],
285
+ decodeCostModel: ADDON_HOST_MODEL
286
+ });
287
+ t.after(async () => {
288
+ await manager.disposeAll();
289
+ await rm(dirPath, { recursive: true, force: true });
290
+ });
291
+
292
+ const session = {
293
+ id: "cccccccc-dddd-eeee-ffff-000000000000",
294
+ dirPath,
295
+ state: "ready",
296
+ fileName: "video.mkv",
297
+ startedAt: Date.now(),
298
+ lastAccessedAt: Date.now(),
299
+ ffmpeg: null,
300
+ lastError: "",
301
+ consumers: new Set(),
302
+ segmentFormat: fmp4Format,
303
+ transcodeVideo: false,
304
+ transcodeAudio: true,
305
+ audioTrackIndex: 0,
306
+ sourceKey: "source-1",
307
+ fileIndex: 0,
308
+ sourceWidth: 1920,
309
+ sourceHeight: 1080,
310
+ sourceDecode: MEASURED_FILM,
311
+ outputFps: 24,
312
+ // A copy can only be cut where the source already has a keyframe, and a
313
+ // master is offered only when that grid is real.
314
+ cutGrid: "keyframe",
315
+ encodeWidth: 0,
316
+ encodeHeight: 0,
317
+ usesExplicitCuts: true,
318
+ useSyntheticPlaylist: true,
319
+ playlistText: "#EXTM3U\n",
320
+ segmentBoundaries: Array.from({ length: 101 }, (_, index) => index * 4),
321
+ segmentCount: 100,
322
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
323
+ };
324
+ manager.sessionsById.set(session.id, session);
325
+
326
+ assert.equal(
327
+ manager.buildMasterPlaylist(session.id),
328
+ null,
329
+ "every rung under the copy runs below realtime here, so there is nothing to switch to"
330
+ );
331
+ assert.deepEqual(
332
+ manager.offeredHeights(session),
333
+ [1080],
334
+ "and the list the browser is given says the same, since both come from one answer"
335
+ );
336
+
337
+ // A host with a little more encoder keeps the rungs it can actually hold. A
338
+ // second session, because the answer is settled once per session.
339
+ manager.softwarePresetBenchmark = [{ preset: "ultrafast", pixelsPerSec: 12e6 }];
340
+ const stronger = { ...session, id: "dddddddd-eeee-ffff-0000-111111111111", offeredHeightsCache: undefined };
341
+ manager.sessionsById.set(stronger.id, stronger);
342
+ const master = manager.buildMasterPlaylist(stronger.id);
343
+ assert.ok(master, "1080p copied plus the one rung this host can produce");
344
+ const heights = [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1]));
345
+ assert.deepEqual(heights, [1080, 240]);
346
+ assert.deepEqual(manager.offeredHeights(stronger), heights);
347
+ });
@@ -133,6 +133,57 @@ test("the master offers every rung, the session's own height among them", async
133
133
  );
134
134
  });
135
135
 
136
+ test("audio is published once for the file, and every rung points at it", async (t) => {
137
+ const { manager, base, dirPath } = await managerWithBase();
138
+ t.after(async () => {
139
+ await manager.disposeAll();
140
+ await rm(dirPath, { recursive: true, force: true });
141
+ });
142
+ // The inventory the plan already probed — the same list the browser's audio
143
+ // menu is built from.
144
+ manager.getCachedAudioTracks = () => [
145
+ { index: 0, language: "rus", title: "Дубляж", isDefault: true },
146
+ { index: 1, language: "eng", title: "", isDefault: false }
147
+ ];
148
+ // Settled at creation in production; set here directly, since this test
149
+ // builds its session by hand.
150
+ base.audioSeparate = true;
151
+ base.audioTrackIndex = 1;
152
+
153
+ const master = manager.buildMasterPlaylist(BASE_ID);
154
+
155
+ assert.match(
156
+ master,
157
+ /#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="Дубляж",LANGUAGE="ru",AUTOSELECT=YES,DEFAULT=NO,URI="a\/0\/index\.m3u8"/,
158
+ "a track with a title is named by it"
159
+ );
160
+ assert.match(
161
+ master,
162
+ /#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="eng",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,URI="a\/1\/index\.m3u8"/,
163
+ "the track the session was created with is the default one, and an untitled track is named by its language"
164
+ );
165
+ const streams = [...master.matchAll(/^#EXT-X-STREAM-INF:.*$/gm)].map((match) => match[0]);
166
+ assert.equal(streams.length, 7, "every rung");
167
+ assert.ok(
168
+ streams.every((line) => line.includes('AUDIO="aud"')),
169
+ "each rung plays with the shared audio rather than carrying its own"
170
+ );
171
+ });
172
+
173
+ test("a session that did not ask for renditions gets audio in its stream, as before", async (t) => {
174
+ const { manager, dirPath } = await managerWithBase();
175
+ t.after(async () => {
176
+ await manager.disposeAll();
177
+ await rm(dirPath, { recursive: true, force: true });
178
+ });
179
+ manager.getCachedAudioTracks = () => [{ index: 0, language: "rus", title: "", isDefault: true }];
180
+
181
+ const master = manager.buildMasterPlaylist(BASE_ID);
182
+
183
+ assert.ok(!master.includes("EXT-X-MEDIA"), "a browser that does not know about renditions is not sent any");
184
+ assert.ok(!master.includes("AUDIO="), "and its rungs still carry their own audio");
185
+ });
186
+
136
187
  test("a copied video is offered variants when its cut grid is real", async (t) => {
137
188
  const { manager, base, dirPath } = await managerWithBase();
138
189
  t.after(async () => {
@@ -310,6 +361,78 @@ test("warming a rung prepares it without taking the encoder from the one on scre
310
361
  assert.deepEqual(encoder.signals, [], "stopping it here is what would put the spinner back");
311
362
  });
312
363
 
364
+ test("a rung warmed at the playhead survives the switch that lands just ahead of it", async (t) => {
365
+ const { manager, base, dirPath } = await managerWithBase();
366
+ t.after(async () => {
367
+ await manager.disposeAll();
368
+ await rm(dirPath, { recursive: true, force: true });
369
+ });
370
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
371
+ variant.variantHeight = 540;
372
+ variant.variantBases = new Set([BASE_ID]);
373
+ manager.sessionsById.set(VARIANT_ID, variant);
374
+ base.variants = new Map([[540, VARIANT_ID]]);
375
+ base.ffmpeg = fakeEncoder();
376
+
377
+ // Warmed AT THE PLAYHEAD (240 s = segment #60), which is what the browser
378
+ // sends from server 0.10.0 onwards, and the run is alive and has produced a
379
+ // few segments past it.
380
+ await manager.prepareVariant(BASE_ID, 540, 240);
381
+ variant.encodeStartIndex = 59;
382
+ variant.ffmpeg = fakeEncoder();
383
+ variant.progress = { ...variant.progress, processedSeconds: 268 };
384
+ variant.seekTarget = null;
385
+ variant.seekSettleTimer = null;
386
+
387
+ // hls.js flushes from the fragment after the one holding
388
+ // `currentTime + fetchdelay`, so its first request for the new rung is the
389
+ // playhead plus up to one fragment — here #61 against a run that began at
390
+ // #59. Warming at the END OF THE BUFFER instead put the run tens of seconds
391
+ // AHEAD of this request, which the proxy then read as a seek backwards:
392
+ // measured 2026-08-14, that killed a run holding 21.8 s of encoded output.
393
+ await manager.resolveVariantFile(BASE_ID, 540, "segment-00061.mp4");
394
+
395
+ assert.equal(base.activeVariantId, VARIANT_ID, "the viewer has moved to this rung");
396
+ assert.equal(
397
+ variant.seekTarget,
398
+ null,
399
+ "the request is inside the warmed run, so nothing is repositioned and the warm-up is kept"
400
+ );
401
+ assert.equal(variant.encodeStartIndex, 59, "the run still begins where it was warmed");
402
+ });
403
+
404
+ test("a rung warmed PAST the switch is repositioned, which is what warming late costs", async (t) => {
405
+ const { manager, base, dirPath } = await managerWithBase();
406
+ t.after(async () => {
407
+ await manager.disposeAll();
408
+ await rm(dirPath, { recursive: true, force: true });
409
+ });
410
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
411
+ variant.variantHeight = 540;
412
+ variant.variantBases = new Set([BASE_ID]);
413
+ manager.sessionsById.set(VARIANT_ID, variant);
414
+ base.variants = new Map([[540, VARIANT_ID]]);
415
+ base.ffmpeg = fakeEncoder();
416
+
417
+ // The same session, warmed where the BUFFER ended rather than where the
418
+ // picture was — 60 s further on, which is an ordinary cushion. This is what
419
+ // server 0.9.3 sent and 0.11.0 stopped sending.
420
+ await manager.prepareVariant(BASE_ID, 540, 300);
421
+ variant.encodeStartIndex = 74;
422
+ variant.ffmpeg = fakeEncoder();
423
+ variant.progress = { ...variant.progress, processedSeconds: 310 };
424
+ variant.seekTarget = null;
425
+ variant.seekSettleTimer = null;
426
+
427
+ // hls.js still lands near the playhead, so the request is far BEHIND the
428
+ // warmed run: the proxy reads it as a seek backwards and starts again, and
429
+ // everything the warm-up produced is thrown away. Measured in the field
430
+ // 2026-08-14 as 21.8 s of encoded output destroyed by the act of using it.
431
+ await manager.resolveVariantFile(BASE_ID, 540, "segment-00061.mp4");
432
+
433
+ assert.equal(variant.seekTarget, 60, "the run is moved back to where the player actually asked");
434
+ });
435
+
313
436
  test("the rung on screen fetching its own segments does not cancel a warm-up", async (t) => {
314
437
  const { manager, base, dirPath } = await managerWithBase();
315
438
  t.after(async () => {
@@ -471,3 +594,46 @@ test("the cut grid follows the grid asked for, not who produces the frames", ()
471
594
  "no keyframes means no keyframe grid, however the caller asks"
472
595
  );
473
596
  });
597
+
598
+ test("a rung served by copy stays offered while a re-encoded rung is on screen", async (t) => {
599
+ const { manager, base, dirPath } = await managerWithBase();
600
+ t.after(async () => {
601
+ await manager.disposeAll();
602
+ await rm(dirPath, { recursive: true, force: true });
603
+ });
604
+ // The field case of 2026-08-15: a 1080p source served by COPY, the viewer on
605
+ // 240p, and a host too weak to re-encode anything above it.
606
+ base.transcodeVideo = false;
607
+ base.encodeHeight = 1080;
608
+ base.variantHeight = 1080;
609
+ // Enough to re-encode 240p (1.67x combined) and nowhere near enough for
610
+ // 720p (0.45x) — the field's own shape, where the rung the viewer picked was
611
+ // offered and everything between it and the copy was not.
612
+ manager.softwarePresetBenchmark = [{ preset: "ultrafast", pixelsPerSec: 12_000_000 }];
613
+ manager.decodeCostModel = { pixelTerm: 0.00793, bitrateTerm: 0, constantTerm: 0 };
614
+ base.sourceDecode = { megapixelsPerSecond: 49.766, megabitsPerSecond: 8 };
615
+
616
+ const watching = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
617
+ watching.variantHeight = 240;
618
+ watching.transcodeVideo = true;
619
+ // The rung knows the source as well as the base does. Without this it prices
620
+ // nothing at all — every height comes back "sustainable" for want of a
621
+ // measurement — and the assertion below would hold for the wrong reason.
622
+ watching.sourceDecode = base.sourceDecode;
623
+ watching.variantBases = new Set([BASE_ID]);
624
+ manager.sessionsById.set(VARIANT_ID, watching);
625
+ base.variants = new Map([[240, VARIANT_ID]]);
626
+ base.activeVariantId = VARIANT_ID;
627
+
628
+ const offered = manager.offeredHeights(watching);
629
+
630
+ assert.ok(
631
+ offered.includes(1080),
632
+ "the height the source is COPIED at costs no encoder, so no measurement of this host can withdraw it"
633
+ );
634
+ assert.deepEqual(
635
+ offered,
636
+ manager.offeredHeights(base),
637
+ "one answer for the family: a rung asked while watching another must not disagree with the base"
638
+ );
639
+ });