@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
@@ -1,153 +1,195 @@
1
- /**
2
- * @file Picture and sound must begin their runs at the same real instant.
3
- *
4
- * The two branches are asked for the same time and land in different places: a
5
- * copied picture may only begin at a real keyframe and may not begin before the
6
- * time asked for, so it moves FORWARD to the next one; a soundtrack has no
7
- * keyframes and begins exactly where asked. Measured 2026-08-17, the difference
8
- * was 0.58-2.96 s on one file, and the viewer got sound with no new picture for
9
- * as long as it lasted.
10
- *
11
- * The picture's true start is measured from the piece it produced. This pins
12
- * that the measurement is carried to the other members of the family.
13
- */
14
-
15
- import assert from "node:assert/strict";
16
- import { SourceFile } from "../services/source/SourceFile.js";
17
- import { startRunOn } from "./helpers/encode-run.js";
18
- import { Timeline } from "../services/output/Timeline.js";
19
- import test from "node:test";
20
-
21
- import { HlsSessionManager } from "../services/hls-session-manager.js";
22
- import { ENCODE_RUN_STATE, INITIAL_RUN_STATE } from "../services/encode/encode-run-state.js";
23
-
24
- const BOUNDARIES = [0, 4, 8, 12, 16, 20];
25
-
26
- /**
27
- * A film's family: the picture, and a soundtrack rendition of it. Both runs
28
- * begin at boundary #2, which the container's table puts at 8 s.
29
- *
30
- * @returns {{ manager: HlsSessionManager, picture: object, sound: object }}
31
- */
32
- function familyAtBoundaryTwo() {
33
- // ONE table for the film. Where it is cut is a fact about the FILE, so the
34
- // picture and its soundtrack hold the same array rather than a copy each.
35
- const boundaries = [...BOUNDARIES];
36
- const manager = new HlsSessionManager({
37
- enabled: true,
38
- ffmpegBin: "ffmpeg",
39
- localBindHost: "127.0.0.1",
40
- localPort: 9090
41
- });
42
- const picture = {
43
- id: "picture",
44
- state: "ready",
45
- timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
46
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
47
- runs: new Set(),
48
- pendingRun: null,
49
- indexCheck: null
50
- };
51
- // The soundtrack of that picture: the same file, published on its own. Found
52
- // by what it is, since no list of ids names it any more.
53
- const sound = {
54
- id: "sound",
55
- state: "ready",
56
- audioOnly: true,
57
- baseSessionId: "picture",
58
- timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
59
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
60
- runs: new Set(),
61
- pendingRun: null,
62
- indexCheck: null
63
- };
64
- startRunOn(picture, { from: 2 });
65
- startRunOn(sound, { from: 2 });
66
- manager.sessionsById.set("picture", picture);
67
- manager.sessionsById.set("sound", sound);
68
-
69
- return { manager, picture, sound };
70
- }
71
-
72
- test("a soundtrack follows the picture to the instant the picture really began", () => {
73
- const { manager, picture, sound } = familyAtBoundaryTwo();
74
- const runBefore = [...sound.runs][0];
75
-
76
- manager.correctBoundaryFromSegment(picture, 2, 10.5);
77
-
78
- assert.deepEqual(
79
- picture.timeline.boundaries,
80
- [0, 4, 10.5, 12, 16, 20],
81
- "the family's table must hold what the file itself said"
82
- );
83
- assert.equal(
84
- sound.timeline.boundaries[2],
85
- 10.5,
86
- "and every member's table with it — one film, one table, nothing to keep in step"
87
- );
88
- // A NEW run, not a seek. A seek decides by index, finds the soundtrack
89
- // already begins at #2 and answers "already within the running encode" —
90
- // true about the index, false about the instant. The first version of this
91
- // fix did exactly that and moved nothing.
92
- //
93
- // The attempt is the evidence because it is what a run start claims before
94
- // it awaits anything: the assertion then holds without the test needing a
95
- // filesystem, a process, or a guess about how many ticks to wait for one.
96
- assert.ok(
97
- sound.pendingRun,
98
- "the soundtrack's run must be started again, at the corrected time"
99
- );
100
- assert.equal(sound.pendingRun.startIndex, 2);
101
- assert.equal([...sound.runs][0], runBefore, "and the run in force is only replaced once one is built");
102
- });
103
-
104
- test("a correction the table already holds moves nobody", () => {
105
- const { manager, picture, sound } = familyAtBoundaryTwo();
106
- const before = [...sound.timeline.boundaries];
107
- // Within the tolerance: the reading agrees with the table, so there is
108
- // nothing to correct and nothing to move. This is what makes the repositioning
109
- // converge instead of repeating on every produced segment.
110
- manager.correctBoundaryFromSegment(picture, 2, 8.1);
111
- assert.deepEqual(sound.timeline.boundaries, before);
112
- });
113
-
114
- test("a member that is not running is left alone", () => {
115
- const { manager, picture, sound } = familyAtBoundaryTwo();
116
- // A rung the viewer switched away from has no process. Moving it would start
117
- // an encoder for nobody — the failure that put three ffmpeg runs on one file.
118
- const soundRun = [...sound.runs][0];
119
- soundRun.stop("the viewer switched away");
120
- // A stopped run is no longer live, so nothing of this session begins at #2
121
- // any more which is what "left alone" means here.
122
- manager.correctBoundaryFromSegment(picture, 2, 10.5);
123
- assert.equal(sound.pendingRun, null, "a stopped member is not started again for nobody");
124
- assert.equal(soundRun.from, 2, "a stopped member keeps its place and its silence");
125
- assert.equal(soundRun.state, ENCODE_RUN_STATE.STOPPED);
126
- assert.notEqual(INITIAL_RUN_STATE, ENCODE_RUN_STATE.STOPPED);
127
- });
128
-
129
- test("a soundtrack does not move the grid the picture is cut on", () => {
130
- const { manager, picture, sound } = familyAtBoundaryTwo();
131
- const pictureBefore = [...picture.timeline.boundaries];
132
- const soundBefore = [...sound.timeline.boundaries];
133
-
134
- // The sound reports where IT began, which is where it was asked to begin, to
135
- // within one audio frame. The picture's own boundary is a keyframe of the
136
- // file and can be seconds away from that — both readings correct, about
137
- // different things.
138
- //
139
- // Field 2026-08-20: boundary #521 of one film was corrected 2086.084 →
140
- // 2084.082 by the picture and 2084.082 2086.033 by the sound 1.6 s later,
141
- // 1.951 s apart, each overwriting the other for as long as the film ran. The
142
- // table never converged, so the guard that stops a correction the table
143
- // already holds never fired.
144
- manager.correctBoundaryFromSegment(sound, 2, 10.5);
145
-
146
- assert.deepEqual(
147
- picture.timeline.boundaries,
148
- pictureBefore,
149
- "the grid is the picture's cut list and a soundtrack may not move it"
150
- );
151
- assert.deepEqual(sound.timeline.boundaries, soundBefore);
152
- assert.equal(picture.pendingRun, null, "and nothing is restarted on a soundtrack's say-so");
153
- });
1
+ /**
2
+ * @file Picture and sound must begin their runs at the same real instant.
3
+ *
4
+ * The two branches are asked for the same time and land in different places: a
5
+ * copied picture may only begin at a real keyframe and may not begin before the
6
+ * time asked for, so it moves FORWARD to the next one; a soundtrack has no
7
+ * keyframes and begins exactly where asked. Measured 2026-08-17, the difference
8
+ * was 0.58-2.96 s on one file, and the viewer got sound with no new picture for
9
+ * as long as it lasted.
10
+ *
11
+ * The picture's true start is measured from the piece it produced. This pins
12
+ * that the measurement is carried to the other members of the family.
13
+ */
14
+
15
+ import assert from "node:assert/strict";
16
+ import { SourceFile } from "../services/source/SourceFile.js";
17
+ import { startRunOn } from "./helpers/encode-run.js";
18
+ import { Timeline } from "../services/output/Timeline.js";
19
+ import test from "node:test";
20
+
21
+ import { HlsSessionManager } from "../services/hls-session-manager.js";
22
+ import { trueStartOf } from "../services/encode/run-command.js";
23
+ import { fmp4Format } from "../services/segment-formats/fmp4.js";
24
+ import { ENCODE_RUN_STATE, INITIAL_RUN_STATE } from "../services/encode/encode-run-state.js";
25
+
26
+ const BOUNDARIES = [0, 4, 8, 12, 16, 20];
27
+
28
+ /**
29
+ * A film's family: the picture, and a soundtrack rendition of it. Both runs
30
+ * begin at boundary #2, which the container's table puts at 8 s.
31
+ *
32
+ * @returns {{ manager: HlsSessionManager, picture: object, sound: object }}
33
+ */
34
+ function familyAtBoundaryTwo() {
35
+ // ONE table for the film. Where it is cut is a fact about the FILE, so the
36
+ // picture and its soundtrack hold the same array rather than a copy each.
37
+ const boundaries = [...BOUNDARIES];
38
+ const manager = new HlsSessionManager({
39
+ enabled: true,
40
+ ffmpegBin: "ffmpeg",
41
+ localBindHost: "127.0.0.1",
42
+ localPort: 9090
43
+ });
44
+ const picture = {
45
+ id: "picture",
46
+ state: "ready",
47
+ timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
48
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
49
+ // An ordinary session reads its own file, and its sound is inside it. The
50
+ // three differ only for a soundtrack shipped as a file of its own — and the
51
+ // command is built from all three, so a fixture that states only the first
52
+ // describes a session the product cannot make.
53
+ get inputFile() { return this.file; },
54
+ get audioFile() { return this.file; },
55
+ // How its pieces are packaged. The command asks the format where to cut and
56
+ // what to name the files, so a session without one is not a session.
57
+ segmentFormat: fmp4Format,
58
+ // Where its encoder has got to, which a run writes into as it works.
59
+ progress: { processedSeconds: 0 },
60
+ runs: new Set(),
61
+ pendingRun: null,
62
+ indexCheck: null
63
+ };
64
+ // The soundtrack of that picture: the same file, published on its own. Found
65
+ // by what it is, since no list of ids names it any more.
66
+ const sound = {
67
+ id: "sound",
68
+ state: "ready",
69
+ audioOnly: true,
70
+ baseSessionId: "picture",
71
+ timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
72
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
73
+ // An ordinary session reads its own file, and its sound is inside it. The
74
+ // three differ only for a soundtrack shipped as a file of its own — and the
75
+ // command is built from all three, so a fixture that states only the first
76
+ // describes a session the product cannot make.
77
+ get inputFile() { return this.file; },
78
+ get audioFile() { return this.file; },
79
+ // How its pieces are packaged. The command asks the format where to cut and
80
+ // what to name the files, so a session without one is not a session.
81
+ segmentFormat: fmp4Format,
82
+ // Where its encoder has got to, which a run writes into as it works.
83
+ progress: { processedSeconds: 0 },
84
+ runs: new Set(),
85
+ pendingRun: null,
86
+ indexCheck: null
87
+ };
88
+ startRunOn(picture, { from: 2 });
89
+ startRunOn(sound, { from: 2 });
90
+ manager.sessionsById.set("picture", picture);
91
+ manager.sessionsById.set("sound", sound);
92
+
93
+ return { manager, picture, sound };
94
+ }
95
+
96
+ test("a soundtrack follows the picture to the instant the picture really began", () => {
97
+ const { manager, picture, sound } = familyAtBoundaryTwo();
98
+ const runBefore = [...sound.runs][0];
99
+
100
+ manager.correctBoundaryFromSegment(picture, 2, 10.5);
101
+
102
+ assert.deepEqual(
103
+ picture.timeline.boundaries,
104
+ [0, 4, 10.5, 12, 16, 20],
105
+ "the family's table must hold what the file itself said"
106
+ );
107
+ assert.equal(
108
+ sound.timeline.boundaries[2],
109
+ 10.5,
110
+ "and every member's table with it — one film, one table, nothing to keep in step"
111
+ );
112
+ // THE RUN AT THE WRONG INSTANT IS STOPPED, and nothing here places its
113
+ // replacement. What has been learned is that this run is producing in the
114
+ // wrong place, which nothing but the piece it produced could say — the plan
115
+ // reasons about numbers and cannot know it. So the fact is acted on as far as
116
+ // it is known and no further: the run goes, the stretch it held returns to the
117
+ // map, and where the next one stands follows from where the viewers are.
118
+ assert.equal(runBefore.state, ENCODE_RUN_STATE.STOPPED, "the run in the wrong place goes");
119
+ assert.equal(sound.pendingRun, null, "and this path places nothing");
120
+ // The corrected instant reaches whatever the plan places there through the
121
+ // table, which every session of the file shares. It used to be passed as an
122
+ // argument from this one call site, so only a run started by that line ever
123
+ // had it — a run the plan placed at the same number landed apart again.
124
+ assert.equal(
125
+ trueStartOf(sound.timeline, 2),
126
+ 10.5,
127
+ "the live table holds the measurement and the published one the prediction"
128
+ );
129
+ });
130
+
131
+ test("where a number really begins is answered only where it has been measured", () => {
132
+ const { sound } = familyAtBoundaryTwo();
133
+
134
+ assert.equal(
135
+ trueStartOf(sound.timeline, 2),
136
+ undefined,
137
+ "the two tables agree, so there is nothing measured to prefer"
138
+ );
139
+ sound.timeline.boundaries[2] = 10.5;
140
+ assert.equal(trueStartOf(sound.timeline, 2), 10.5);
141
+ assert.equal(trueStartOf(sound.timeline, 3), undefined, "and only about the number measured");
142
+ assert.equal(trueStartOf(sound.timeline, 99), undefined, "a number the file does not have");
143
+ assert.equal(trueStartOf(null, 0), undefined, "and no table at all");
144
+ });
145
+
146
+ test("a correction the table already holds moves nobody", () => {
147
+ const { manager, picture, sound } = familyAtBoundaryTwo();
148
+ const before = [...sound.timeline.boundaries];
149
+ // Within the tolerance: the reading agrees with the table, so there is
150
+ // nothing to correct and nothing to move. This is what makes the repositioning
151
+ // converge instead of repeating on every produced segment.
152
+ manager.correctBoundaryFromSegment(picture, 2, 8.1);
153
+ assert.deepEqual(sound.timeline.boundaries, before);
154
+ });
155
+
156
+ test("a member that is not running is left alone", () => {
157
+ const { manager, picture, sound } = familyAtBoundaryTwo();
158
+ // A rung the viewer switched away from has no process. Moving it would start
159
+ // an encoder for nobody — the failure that put three ffmpeg runs on one file.
160
+ const soundRun = [...sound.runs][0];
161
+ soundRun.stop("the viewer switched away");
162
+ // A stopped run is no longer live, so nothing of this session begins at #2
163
+ // any more — which is what "left alone" means here.
164
+ manager.correctBoundaryFromSegment(picture, 2, 10.5);
165
+ assert.equal(sound.pendingRun, null, "a stopped member is not started again for nobody");
166
+ assert.equal(soundRun.from, 2, "a stopped member keeps its place and its silence");
167
+ assert.equal(soundRun.state, ENCODE_RUN_STATE.STOPPED);
168
+ assert.notEqual(INITIAL_RUN_STATE, ENCODE_RUN_STATE.STOPPED);
169
+ });
170
+
171
+ test("a soundtrack does not move the grid the picture is cut on", () => {
172
+ const { manager, picture, sound } = familyAtBoundaryTwo();
173
+ const pictureBefore = [...picture.timeline.boundaries];
174
+ const soundBefore = [...sound.timeline.boundaries];
175
+
176
+ // The sound reports where IT began, which is where it was asked to begin, to
177
+ // within one audio frame. The picture's own boundary is a keyframe of the
178
+ // file and can be seconds away from that — both readings correct, about
179
+ // different things.
180
+ //
181
+ // Field 2026-08-20: boundary #521 of one film was corrected 2086.084 →
182
+ // 2084.082 by the picture and 2084.082 → 2086.033 by the sound 1.6 s later,
183
+ // 1.951 s apart, each overwriting the other for as long as the film ran. The
184
+ // table never converged, so the guard that stops a correction the table
185
+ // already holds never fired.
186
+ manager.correctBoundaryFromSegment(sound, 2, 10.5);
187
+
188
+ assert.deepEqual(
189
+ picture.timeline.boundaries,
190
+ pictureBefore,
191
+ "the grid is the picture's cut list and a soundtrack may not move it"
192
+ );
193
+ assert.deepEqual(sound.timeline.boundaries, soundBefore);
194
+ assert.equal(picture.pendingRun, null, "and nothing is restarted on a soundtrack's say-so");
195
+ });
@@ -19,6 +19,29 @@ import { WebSocketServer } from "ws";
19
19
 
20
20
  import { createTunnelClient } from "../services/tunnel-client.js";
21
21
 
22
+ /**
23
+ * Wait for the thing being asserted, not for a length of time.
24
+ *
25
+ * A chosen interval followed by an assertion does not test — it samples, and
26
+ * what it samples is how busy the machine is. The deadline here is a backstop
27
+ * that turns a hang into a failure; it is never the measurement.
28
+ *
29
+ * @param {() => boolean} until
30
+ * @param {string} what - Named in the failure, since a timeout otherwise says
31
+ * only that something did not happen.
32
+ * @param {number} [limit]
33
+ * @returns {Promise<void>}
34
+ */
35
+ async function waitFor(until, what, limit = 10_000) {
36
+ const deadline = Date.now() + limit;
37
+ while (!until()) {
38
+ if (Date.now() > deadline) {
39
+ throw new Error(`${what} never happened`);
40
+ }
41
+ await new Promise((resolve) => { setTimeout(resolve, 5); });
42
+ }
43
+ }
44
+
22
45
  /**
23
46
  * A stand-in for the registry's tunnel endpoint, with the one behaviour that
24
47
  * matters here: a new connection for a proxy REPLACES the previous one, which
@@ -92,8 +115,10 @@ test("the connection is replaced before its lifetime runs out, without a gap", a
92
115
  });
93
116
 
94
117
  client.connect();
95
- // Long enough for several renewals at 150 ms each.
96
- await new Promise((resolve) => { setTimeout(resolve, 700); });
118
+ // Until the renewals have happened, however long this machine takes over
119
+ // them. Waited out as 700 ms instead — about four lifetimes — this asserted
120
+ // how many the scheduler had got round to.
121
+ await waitFor(() => registry.opened() >= 3, "three connections");
97
122
 
98
123
  assert.ok(registry.opened() >= 3, `expected several renewals, saw ${registry.opened()}`);
99
124
  // The property this exists for: the registry was never left with nothing.
@@ -128,11 +153,15 @@ test("a connection killed from outside is still reconnected", async (t) => {
128
153
  });
129
154
 
130
155
  client.connect();
131
- await new Promise((resolve) => { setTimeout(resolve, 200); });
156
+ await waitFor(() => registry.opened() === 1, "the first connection");
157
+ // And exactly one: the lifetime above is far longer than this test runs, so
158
+ // nothing renews and a second would mean something else had opened it.
132
159
  assert.equal(registry.opened(), 1);
133
160
 
134
161
  registry.killCurrent();
135
- await new Promise((resolve) => { setTimeout(resolve, 300); });
136
162
  // The renewal must not have taken the ordinary reconnect away with it.
137
- assert.ok(lines.some((line) => line.includes("Reconnecting in")), lines.join("\n"));
163
+ await waitFor(
164
+ () => lines.some((line) => line.includes("Reconnecting in")),
165
+ "the reconnect after a connection was killed from outside"
166
+ );
138
167
  });