@torrent-tv/proxy 2.80.19 → 2.81.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/research/double-spawn-2026-09-10.md +171 -0
  4. package/services/disk/DiskSpace.js +150 -0
  5. package/services/disk/wire.js +60 -0
  6. package/services/encode/EncodeRun.js +25 -3
  7. package/services/encode/SegmentStore.js +137 -9
  8. package/services/hls-session-manager.js +26 -45
  9. package/services/orchestrators/EncodeOrchestrator.js +4 -1
  10. package/services/piece-store/allowance.js +107 -0
  11. package/services/piece-store/piece-disk-store.js +365 -0
  12. package/services/piece-store/shared-piece-store.js +1549 -1535
  13. package/services/torrent-worker/client.js +32 -0
  14. package/services/torrent-worker/pool-adapter.js +15 -0
  15. package/services/torrent-worker/protocol.js +9 -0
  16. package/services/torrent-worker/worker.js +8 -1
  17. package/services/viewer/positions.js +48 -0
  18. package/test/audio-inventory.test.js +176 -176
  19. package/test/auto-quality-step.test.js +514 -514
  20. package/test/concurrent-cost.test.js +138 -138
  21. package/test/coverage-follows-the-disk.test.js +191 -191
  22. package/test/coverage-map.test.js +195 -195
  23. package/test/declared-tracks.test.js +35 -35
  24. package/test/disk-space.test.js +150 -0
  25. package/test/encode-orchestrator.test.js +0 -3
  26. package/test/encode-run.test.js +5 -12
  27. package/test/held-request-width.test.js +155 -155
  28. package/test/helpers/encode-run.js +2 -2
  29. package/test/matroska-blocks.test.js +0 -0
  30. package/test/matroska-cues-track.test.js +192 -192
  31. package/test/mp4-composition-times.test.js +0 -0
  32. package/test/mp4-subtitles.test.js +173 -173
  33. package/test/one-authority.test.js +281 -220
  34. package/test/orchestrator-wired.test.js +199 -199
  35. package/test/packet-witness-ring.test.js +236 -236
  36. package/test/packet-witness.test.js +148 -148
  37. package/test/piece-disk-store.test.js +267 -0
  38. package/test/piece-reader.test.js +4 -4
  39. package/test/piece-store-eviction.test.js +17 -17
  40. package/test/piece-store-reservations.test.js +20 -1
  41. package/test/piece-store-slow-disk.test.js +16 -1
  42. package/test/read-window.test.js +6 -6
  43. package/test/run-intervals.test.js +100 -100
  44. package/test/seek-landing.test.js +109 -109
  45. package/test/segment-store-eviction.test.js +232 -0
  46. package/test/segments-are-shared.test.js +1 -1
  47. package/test/shared-piece-store.test.js +12 -12
  48. package/test/sidecar-naming.test.js +142 -142
  49. package/test/subtitle-cue-framing.test.js +200 -200
  50. package/test/subtitle-cue-walk.test.js +369 -369
  51. package/test/subtitle-defaults.test.js +97 -97
  52. package/test/subtitle-track-numbering.test.js +370 -370
  53. package/test/tail-duplication.test.js +167 -167
  54. package/test/tracks-begin-together.test.js +195 -195
  55. package/test/two-viewers-one-picture.test.js +374 -374
  56. package/test/video-facts.test.js +102 -102
  57. package/test/wedge-certainty.test.js +131 -131
  58. package/services/piece-store/disk-tier.js +0 -151
@@ -1,195 +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 { 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
- });
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
+ });