@torrent-tv/proxy 2.80.9 → 2.80.11

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +1685 -1666
  2. package/package.json +1 -1
  3. package/services/data-channel-handler.js +2 -4
  4. package/services/encode/EncodePlan.js +0 -3
  5. package/services/encode/EncodeRun.js +14 -0
  6. package/services/encode/run-command.js +729 -707
  7. package/services/hls-session-manager.js +10168 -10494
  8. package/services/hwaccel.js +2 -2
  9. package/services/orchestrators/EncodeOrchestrator.js +4 -4
  10. package/services/output/LiveOutputs.js +49 -0
  11. package/services/priority/PriorityOrchestrator.js +278 -174
  12. package/services/quality/EncodeCost.js +555 -555
  13. package/test/auto-quality-step.test.js +514 -507
  14. package/test/contention.test.js +1 -1
  15. package/test/decode-cost-fit.test.js +57 -0
  16. package/test/decode-cost.test.js +13 -36
  17. package/test/encode-orchestrator.test.js +1 -2
  18. package/test/encode-plan-viewers.test.js +18 -19
  19. package/test/encode-plan.test.js +539 -539
  20. package/test/held-request-width.test.js +155 -154
  21. package/test/helpers/encode-run.js +136 -128
  22. package/test/keyframe-table-budget.test.js +7 -1
  23. package/test/one-authority.test.js +220 -105
  24. package/test/orchestrator-wired.test.js +16 -11
  25. package/test/piece-store-slow-disk.test.js +32 -2
  26. package/test/priority-map-per-output.test.js +211 -0
  27. package/test/produced-copy-choice.test.js +358 -327
  28. package/test/quality-variants.test.js +1222 -1209
  29. package/test/segment-serve-wiring.test.js +76 -20
  30. package/test/stale-request-after-seek.test.js +51 -77
  31. package/test/tracks-begin-together.test.js +195 -153
  32. package/test/tunnel-renewal.test.js +34 -5
  33. package/test/two-viewers-one-picture.test.js +374 -347
  34. package/test/viewer-outputs.test.js +18 -14
  35. package/test/behind-head-repair.test.js +0 -261
  36. package/test/decode-measurement.test.js +0 -83
  37. /package/services/{contention.js → encode/contention.js} +0 -0
@@ -82,7 +82,6 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
82
82
  applyTonemap: false
83
83
  }),
84
84
  encodeRunGeneration: 0,
85
- lastRestartAt: 0,
86
85
  failedStartAt: -1,
87
86
  failedStartCount: 0,
88
87
  seekSettleTimer: null,
@@ -128,14 +127,7 @@ async function pictureWithStepAndSoundtrack() {
128
127
  manager.sessionsById.set(session.id, session);
129
128
  }
130
129
  file.stepHeights.set(540, 540);
131
- /** @type {string[]} */
132
- const released = [];
133
- const realRelease = manager.encodeOrchestrator.release.bind(manager.encodeOrchestrator);
134
- manager.encodeOrchestrator.release = (claimant) => {
135
- released.push(claimant);
136
- return realRelease(claimant);
137
- };
138
- return { manager, base, step, audio, dirPath, released };
130
+ return { manager, base, step, audio, dirPath };
139
131
  }
140
132
 
141
133
  test("a viewer who steps down, back to the picture's own height and down again keeps their record", async (t) => {
@@ -224,8 +216,8 @@ test("an output somebody else is still watching is kept when one viewer leaves",
224
216
  assert.equal(step.viewers.size, 0, "the step they had is watched by nobody");
225
217
  });
226
218
 
227
- test("leaving an output releases what the watching claimed of production", async (t) => {
228
- const { manager, base, step, audio, dirPath, released } = await pictureWithStepAndSoundtrack();
219
+ test("nothing is left wanting production once the last viewer has left", async (t) => {
220
+ const { manager, base, step, audio, dirPath } = await pictureWithStepAndSoundtrack();
229
221
  t.after(() => rm(dirPath, { recursive: true, force: true }));
230
222
  manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
231
223
  manager.viewers.of(step, VIEWER);
@@ -233,10 +225,22 @@ test("leaving an output releases what the watching claimed of production", async
233
225
 
234
226
  await manager.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
235
227
 
228
+ // NOTHING HAS TO BE RELEASED, and that is the point of the shape. A claim per
229
+ // viewer per output used to be held in the encoding layer and taken back one
230
+ // by one when they left — the viewer's own name as the key of a claim, which
231
+ // is the coupling the layer rule forbids. What the plan is given now is a map
232
+ // with nobody's name in it, rebuilt from whoever is watching; a viewer who has
233
+ // left is simply not in the next one.
236
234
  for (const id of [BASE_ID, STEP_ID, AUDIO_ID]) {
237
- assert.ok(
238
- released.includes(`${id}:${VIEWER}`),
239
- `the claim on ${id.slice(0, 8)} is released — the plan's own pass cannot do it once the viewer is out of the map`
235
+ const session = manager.sessionsById.get(id);
236
+ assert.equal(session, undefined, `${id.slice(0, 8)} is let go with its last viewer`);
237
+ }
238
+ manager.planEncodersNow();
239
+ for (const address of [base.outputKey, step.outputKey, audio.outputKey]) {
240
+ assert.deepEqual(
241
+ manager.encodeOrchestrator.demand.mapOn(address),
242
+ [],
243
+ "and no output is left asking for an encoder"
240
244
  );
241
245
  }
242
246
  });
@@ -1,261 +0,0 @@
1
- /**
2
- * @file A segment request below the running encode must not be held for ever.
3
- *
4
- * The encoder only moves forward from where its run began, so a request BELOW
5
- * that point cannot be answered by anything the run does. Every other far
6
- * request is a claim the running encode may yet reach; this one is a hole.
7
- *
8
- * Measured 2026-08-11: a quality switch placed a run at segment #770 while the
9
- * player needed #757, and the request was held for two minutes forty-one while
10
- * the encoder produced 409 s of video nobody had asked for at 2.48x. The
11
- * placement that caused it is fixed; this is the guard that stops the SHAPE
12
- * from hanging a session again, whatever puts it there.
13
- */
14
-
15
- import test from "node:test";
16
- import assert from "node:assert/strict";
17
- import { SourceFile } from "../services/source/SourceFile.js";
18
- import { Timeline } from "../services/output/Timeline.js";
19
- import { mkdtemp, rm } from "node:fs/promises";
20
- import os from "node:os";
21
- import path from "node:path";
22
- import { HlsSessionManager } from "../services/hls-session-manager.js";
23
- import { startRunOn } from "./helpers/encode-run.js";
24
- import { fmp4Format } from "../services/segment-formats/fmp4.js";
25
-
26
- const SESSION_ID = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff";
27
- const SEGMENT_SECONDS = 4;
28
- const RUN_STARTS_AT = 770;
29
- const WANTED = 757;
30
-
31
- /**
32
- * A session whose run began well past the segment being asked for.
33
- *
34
- * @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string, restarts: number[] }>}
35
- */
36
- async function managerWithRunAhead() {
37
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "behind-head-"));
38
- const manager = new HlsSessionManager({
39
- enabled: true,
40
- ffmpegBin: "ffmpeg",
41
- localBindHost: "127.0.0.1",
42
- localPort: 9090
43
- });
44
- const session = {
45
- id: SESSION_ID,
46
- dirPath,
47
- // Where this file is cut, held by the file. A fixture that stated it
48
- // on the session was describing what production no longer does.
49
- timeline: new Timeline({
50
- boundaries: Array.from({ length: 1937 }, (_, index) => index * SEGMENT_SECONDS),
51
- cutGrid: "uniform"
52
- }),
53
- state: "ready",
54
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.avi" }),
55
- // An ordinary session reads its own file, and its sound is inside it. The
56
- // three differ only for a soundtrack shipped as a file of its own.
57
- get inputFile() { return this.file; },
58
- get audioFile() { return this.file; },
59
- startedAt: Date.now(),
60
- createEntryMs: Date.now(),
61
- lastAccessedAt: Date.now(),
62
- lastError: "",
63
- consumers: new Set(),
64
- segmentFormat: fmp4Format,
65
- transcodeVideo: true,
66
- useSyntheticPlaylist: true,
67
- playlistText: "#EXTM3U\n",
68
- lastRestartAt: 0,
69
- failedStartAt: -1,
70
- failedStartCount: 0,
71
- seekSettleTimer: null,
72
- seekTarget: null,
73
- waitEpoch: 0,
74
- firstWantedAt: new Map(),
75
- runs: new Set(),
76
- progress: { state: "running", processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS + 400, startPositionSeconds: RUN_STARTS_AT * SEGMENT_SECONDS }
77
- };
78
- manager.sessionsById.set(SESSION_ID, session);
79
- startRunOn(session, { from: RUN_STARTS_AT });
80
- const restarts = [];
81
- return { manager, session, dirPath, restarts };
82
- }
83
-
84
- test("a request behind the run is repaired once the player asks again", async (t) => {
85
- const { manager, session, dirPath } = await managerWithRunAhead();
86
- t.after(async () => {
87
- await manager.disposeAll();
88
- await rm(dirPath, { recursive: true, force: true });
89
- });
90
- // Asked for a moment ago and still unanswerable. What decides is that the
91
- // player comes BACK for the same segment: it is the only one it wants, and no
92
- // amount of waiting could say that as clearly.
93
- session.firstWantedAt.set(WANTED, Date.now() - 1000);
94
- const name = fmp4Format.segmentFileName(WANTED);
95
-
96
- await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
97
-
98
- assert.equal(session.seekTarget, null, "one poll is not yet evidence");
99
-
100
- await manager.getFileStream(SESSION_ID, name, { requestSeq: 2 });
101
-
102
- assert.equal(
103
- session.seekTarget,
104
- WANTED - 1,
105
- "the encoder must be moved back to it — one segment early, for the preceding keyframe"
106
- );
107
- });
108
-
109
- test("a burst of different segments behind the run is a scan, and moves nothing", async (t) => {
110
- const { manager, session, dirPath } = await managerWithRunAhead();
111
- t.after(async () => {
112
- await manager.disposeAll();
113
- await rm(dirPath, { recursive: true, force: true });
114
- });
115
- // What hls.js does on a seek: dozens of indices within half a second, each
116
- // abandoned. Field log 2026-08-02: #178, #681, #725, #807, #74, #245, #387.
117
- const scanned = [WANTED, WANTED - 40, WANTED - 120, WANTED - 200, WANTED - 300];
118
- for (const index of scanned) {
119
- session.firstWantedAt.set(index, Date.now() - 1000);
120
- }
121
- // Asked ONCE each, which is what a scan is: the player opens them together
122
- // and abandons them together. Field log 2026-08-02 — #178, #681, #725, #807,
123
- // #74, #245, #387 within half a second, none of them repeated.
124
- for (const index of scanned) {
125
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(index), { requestSeq: 1 });
126
- }
127
-
128
- assert.equal(
129
- session.seekTarget,
130
- null,
131
- "moving the encoder to one of these would be moving it to a number the player picked at random"
132
- );
133
- });
134
-
135
- test("a request behind the run is left alone at first", async (t) => {
136
- const { manager, session, dirPath } = await managerWithRunAhead();
137
- t.after(async () => {
138
- await manager.disposeAll();
139
- await rm(dirPath, { recursive: true, force: true });
140
- });
141
-
142
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
143
-
144
- assert.equal(
145
- session.seekTarget,
146
- null,
147
- "a burst around a reported seek settles by itself, and the seek is what should move the encoder"
148
- );
149
- });
150
-
151
- test("a seek already settling is not overridden", async (t) => {
152
- const { manager, session, dirPath } = await managerWithRunAhead();
153
- t.after(async () => {
154
- await manager.disposeAll();
155
- await rm(dirPath, { recursive: true, force: true });
156
- });
157
- session.firstWantedAt.set(WANTED, Date.now() - 4000);
158
- // The viewer has stated where they are and it is about to be acted on.
159
- session.seekTarget = 1200;
160
- session.seekSettleTimer = setTimeout(() => {}, 60_000);
161
- t.after(() => clearTimeout(session.seekSettleTimer));
162
-
163
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
164
-
165
- assert.equal(
166
- session.seekTarget,
167
- 1200,
168
- "a statement from the viewer outranks anything inferred from what the player is fetching"
169
- );
170
- });
171
-
172
- test("a playlist scan far below the run is left where it belongs", async (t) => {
173
- const { manager, session, dirPath } = await managerWithRunAhead();
174
- t.after(async () => {
175
- await manager.disposeAll();
176
- await rm(dirPath, { recursive: true, force: true });
177
- });
178
- // A player that cannot get what it wants scans the playlist: field log
179
- // 2026-08-02, probes at #178, #681, #725, #807, #74, #245, #387 within half a
180
- // second. Steering on the lowest of those put the encoder at the start of the
181
- // film and left the viewer's own requests unreachable ahead of it — the exact
182
- // reason request-steering was removed from this proxy.
183
- const probe = 74;
184
- session.firstWantedAt.set(probe, Date.now() - 30_000);
185
-
186
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(probe), { requestSeq: 1 });
187
-
188
- assert.equal(
189
- session.seekTarget,
190
- null,
191
- "a misplaced run is out by a buffer; a scan probe is out by anything, and the two must not be confused"
192
- );
193
- });
194
-
195
- test("a rung whose encoder was stopped is not brought back by a held request", async (t) => {
196
- const { manager, session, dirPath } = await managerWithRunAhead();
197
- t.after(async () => {
198
- await manager.disposeAll();
199
- await rm(dirPath, { recursive: true, force: true });
200
- });
201
- // What a quality switch leaves behind: the rung nobody is watching, parked.
202
- session.runs = new Set();
203
- session.firstWantedAt.set(WANTED, Date.now() - 4000);
204
-
205
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
206
-
207
- assert.equal(
208
- session.seekTarget,
209
- null,
210
- "restarting it would put a second encoder on a host sized for one, for a rung nobody is watching"
211
- );
212
- });
213
-
214
- test("a request ahead of the run is not touched", async (t) => {
215
- const { manager, session, dirPath } = await managerWithRunAhead();
216
- t.after(async () => {
217
- await manager.disposeAll();
218
- await rm(dirPath, { recursive: true, force: true });
219
- });
220
- const ahead = RUN_STARTS_AT + 400;
221
- session.firstWantedAt.set(ahead, Date.now() - 30_000);
222
-
223
- await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(ahead), { requestSeq: 1 });
224
-
225
- assert.equal(
226
- session.seekTarget,
227
- null,
228
- "the running encode may yet reach it; restarting on a far request is what produced nine restarts in a minute"
229
- );
230
- });
231
-
232
- test("a request far beyond the repair's reach is answered at once, not held", async (t) => {
233
- const { manager, session, dirPath } = await managerWithRunAhead();
234
- t.after(async () => {
235
- await manager.disposeAll();
236
- await rm(dirPath, { recursive: true, force: true });
237
- });
238
- // What a track change does: hls.js asks the new stream for segment #0 while
239
- // the run is hundreds of segments in. The repair cannot reach it, no seek is
240
- // coming, and the run only moves forward — so it can never be produced.
241
- // Measured 2026-08-15: held for the full minute, and the viewer watched a
242
- // spinner for 63 s after a track that had been made ready in 7.
243
- const answer = await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(0), { requestSeq: 1 });
244
-
245
- assert.equal(answer.kind, "not-found", "answered, so the player can move on to what it can have");
246
- assert.equal(session.seekTarget, null, "and the encoder was not sent to the start of the film for it");
247
- });
248
-
249
- test("a request just behind the run is still held, because the repair will fetch it", async (t) => {
250
- const { manager, session, dirPath } = await managerWithRunAhead();
251
- t.after(async () => {
252
- await manager.disposeAll();
253
- await rm(dirPath, { recursive: true, force: true });
254
- });
255
- const name = fmp4Format.segmentFileName(WANTED);
256
- session.firstWantedAt.set(WANTED, Date.now() - 1000);
257
-
258
- const answer = await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
259
-
260
- assert.equal(answer.kind, "warming-up", "within reach: the encoder is about to be moved there");
261
- });
@@ -1,83 +0,0 @@
1
- /**
2
- * @file What the decode measurement must never get wrong.
3
- *
4
- * The quality menu is predicted from these readings, so a reading that is
5
- * merely PRECISE is worth nothing — it has to be ordered the way decoding is.
6
- * Under `-stream_loop -1` it was not: the loop restart costs 0.03 s on a 480p
7
- * clip and 0.12 s on a 1080p one (the decoder tearing down and re-allocating
8
- * its frame buffers), a five-second clip at 55x restarts eleven times a second,
9
- * and the resulting bias depends on the clip's own resolution and on how fast
10
- * the host is. On a fast desktop, 2026-08-20, that made 1080p read cheaper than
11
- * 720p — which is not a thing a decoder does, and the fit refused to solve.
12
- *
13
- * These tests state the orderings instead of the numbers: the numbers are
14
- * properties of whatever machine runs them.
15
- */
16
-
17
- import test from "node:test";
18
- import assert from "node:assert/strict";
19
- import path from "node:path";
20
- import { fileURLToPath } from "node:url";
21
- import ffmpegStatic from "ffmpeg-static";
22
- import { measureDecodeSlope } from "../services/hwaccel.js";
23
-
24
- const CLIPS = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "calibration");
25
- const clip = (name) => path.join(CLIPS, name);
26
-
27
- test("a bigger picture costs more than a smaller one of the same bitrate", async () => {
28
- const big = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-1080-hi.mp4"));
29
- const small = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-hi.mp4"));
30
-
31
- assert.ok(big, "the 1080p clip was measured");
32
- assert.ok(small, "the 480p clip was measured");
33
- assert.ok(
34
- small.speed > big.speed,
35
- `480p decoded at ${small.speed.toFixed(1)}x and 1080p at ${big.speed.toFixed(1)}x — ` +
36
- `an ordering no decoder produces, which is what the loop used to invert`
37
- );
38
- });
39
-
40
- test("a thicker stream costs more than a thin one of the same size", async () => {
41
- const thick = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-hi.mp4"));
42
- const thin = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-lo.mp4"));
43
-
44
- assert.ok(thick && thin);
45
- assert.ok(
46
- thin.speed > thick.speed,
47
- `the same picture at ${thin.megabitsPerSecond.toFixed(2)} Mbit/s decoded at ${thin.speed.toFixed(1)}x ` +
48
- `and at ${thick.megabitsPerSecond.toFixed(2)} Mbit/s at ${thick.speed.toFixed(1)}x`
49
- );
50
- });
51
-
52
- test("HEVC costs more than H.264 for the same picture on the same machine", async () => {
53
- const h264 = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-lo.mp4"), "h264");
54
- const hevc = await measureDecodeSlope(ffmpegStatic, clip("cal-hevc-480-lo.mp4"), "hevc");
55
-
56
- assert.ok(h264 && hevc, "both families are lifted out of their containers correctly");
57
- assert.ok(
58
- h264.speed > hevc.speed,
59
- `H.264 ${h264.speed.toFixed(1)}x against HEVC ${hevc.speed.toFixed(1)}x — the reason the model ` +
60
- `is fitted per codec family at all`
61
- );
62
- });
63
-
64
- test("the clip's own characteristics come back with the reading", async () => {
65
- const measured = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-1080-hi.mp4"));
66
-
67
- assert.ok(measured);
68
- // Read from the container rather than declared anywhere, so replacing a clip
69
- // cannot silently invalidate the fit that rests on it.
70
- assert.ok(measured.megapixelsPerSecond > 40 && measured.megapixelsPerSecond < 60);
71
- assert.ok(measured.megabitsPerSecond > 5 && measured.megabitsPerSecond < 15);
72
- assert.ok(measured.windowSec >= 0.5, "the slope is taken over a window wide enough to divide by");
73
- });
74
-
75
- test("a reading carries what the pipe was actually fed, for item 4(d2)'s sanity check", async () => {
76
- const measured = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-lo.mp4"));
77
-
78
- assert.ok(measured);
79
- assert.ok(
80
- measured.pipeThroughputMBps > 0,
81
- "the write path pushed a nonzero amount of the clip through, or nothing was measured at all"
82
- );
83
- });