@torrent-tv/proxy 2.74.1 → 2.76.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +1504 -1453
  2. package/CLAUDE.md +19 -6
  3. package/biome.json +182 -1
  4. package/docs/container-architecture.md +27 -6
  5. package/docs/encode-run-state.md +1 -1
  6. package/knip.json +14 -0
  7. package/package.json +1 -1
  8. package/routes/api/sources/warm/post.js +1 -1
  9. package/routes/api/transcode-sessions/post.js +185 -185
  10. package/routes/api/transcode-sessions/progress/get.js +5 -1
  11. package/routes/transcode/audio-file/get.js +11 -1
  12. package/routes/transcode/audio-warm/get.js +11 -1
  13. package/routes/transcode/session-file/get.js +1 -1
  14. package/routes/transcode/variant-file/get.js +10 -1
  15. package/scripts/render-run-graph.js +2 -2
  16. package/server.js +25 -0
  17. package/services/audio-inventory.js +9 -214
  18. package/services/container/AviContainer.js +266 -81
  19. package/services/container/Container.js +281 -80
  20. package/services/container/ContainerFactory.js +67 -0
  21. package/services/container/MatroskaContainer.js +327 -8
  22. package/services/container/Mp4Container.js +373 -27
  23. package/services/container/SubtitleFileContainer.js +0 -1
  24. package/services/controllers/SubtitleController.js +128 -128
  25. package/services/demand/index.js +7 -10
  26. package/services/download/registry.js +0 -14
  27. package/services/encode/CoverageMap.js +281 -0
  28. package/services/encode/EncodePlan.js +255 -0
  29. package/services/encode/EncodeRun.js +587 -0
  30. package/services/encode/Encoder.js +84 -0
  31. package/services/encode/NvencEncoder.js +45 -0
  32. package/services/encode/QsvEncoder.js +47 -0
  33. package/services/encode/SegmentDemand.js +0 -0
  34. package/services/encode/SegmentStore.js +529 -0
  35. package/services/encode/SoftwareEncoder.js +111 -0
  36. package/services/encode/V4l2m2mEncoder.js +53 -0
  37. package/services/encode/VaapiEncoder.js +53 -0
  38. package/services/encode/args.js +200 -0
  39. package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
  40. package/services/encode/index.js +9 -0
  41. package/services/encode/run-command.js +647 -0
  42. package/services/hls-session-manager.js +11073 -10711
  43. package/services/hwaccel.js +1688 -1992
  44. package/services/orchestrators/EncodeOrchestrator.js +359 -0
  45. package/services/output/LiveOutputs.js +213 -0
  46. package/services/output/Output.js +94 -0
  47. package/services/output/OutputSpec.js +195 -0
  48. package/services/output/Timeline.js +220 -0
  49. package/services/output/index.js +1 -0
  50. package/services/output/ladder.js +26 -0
  51. package/services/playback-planner.js +806 -747
  52. package/services/produced-index.js +222 -300
  53. package/services/source/SourceFile.js +346 -0
  54. package/services/{sidecar-files.js → torrent/files.js} +107 -11
  55. package/services/torrent/naming.js +619 -0
  56. package/services/torrent-worker/client.js +10 -0
  57. package/services/torrent-worker/container-tracks.js +71 -43
  58. package/services/torrent-worker/pool-adapter.js +370 -333
  59. package/services/torrent-worker/protocol.js +7 -0
  60. package/services/torrent-worker/subtitle-cues.js +549 -549
  61. package/services/torrent-worker/worker.js +18 -0
  62. package/services/tracks/AudioTrack.js +131 -40
  63. package/services/tracks/TextSubtitleTrack.js +287 -287
  64. package/services/tracks/index.js +15 -14
  65. package/services/viewer/Viewer.js +145 -0
  66. package/services/viewer/Viewers.js +124 -0
  67. package/test/audio-inventory.test.js +176 -177
  68. package/test/auto-quality-step.test.js +508 -506
  69. package/test/behind-head-repair.test.js +17 -7
  70. package/test/coverage-map.test.js +153 -0
  71. package/test/cut-times-timeline.test.js +6 -5
  72. package/test/cuts-follow-published-grid.test.js +4 -4
  73. package/test/decode-cost.test.js +31 -12
  74. package/test/encode-exit.test.js +1 -1
  75. package/test/encode-orchestrator.test.js +196 -0
  76. package/test/encode-plan.test.js +245 -0
  77. package/test/encode-run-state.test.js +2 -2
  78. package/test/encode-run.test.js +168 -0
  79. package/test/encoder-kinds.test.js +122 -0
  80. package/test/held-request-width.test.js +9 -3
  81. package/test/helpers/encode-run.js +128 -0
  82. package/test/keyframe-index-accuracy.test.js +19 -12
  83. package/test/keyframes-belong-to-the-file.test.js +132 -0
  84. package/test/matroska-cues-track.test.js +192 -192
  85. package/test/mp4-composition-times.test.js +0 -0
  86. package/test/orchestrator-wired.test.js +164 -0
  87. package/test/output-shape.test.js +68 -0
  88. package/test/output-spec.test.js +157 -0
  89. package/test/produced-copy-choice.test.js +58 -92
  90. package/test/produced-index.test.js +142 -188
  91. package/test/quality-variants.test.js +1079 -1075
  92. package/test/run-graph-drift.test.js +1 -1
  93. package/test/run-intervals.test.js +329 -0
  94. package/test/run-position-follows-published-grid.test.js +4 -4
  95. package/test/seek-landing.test.js +8 -8
  96. package/test/seek-target-not-superseded.test.js +21 -9
  97. package/test/segment-demand.test.js +82 -0
  98. package/test/segment-serve-wiring.test.js +47 -52
  99. package/test/segment-store.test.js +187 -0
  100. package/test/segments-are-shared.test.js +175 -0
  101. package/test/sidecar-naming.test.js +142 -0
  102. package/test/source-file.test.js +133 -0
  103. package/test/stale-request-after-seek.test.js +18 -12
  104. package/test/subtitle-language.test.js +252 -252
  105. package/test/timeline.test.js +95 -0
  106. package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
  107. package/test/torrent-naming.test.js +255 -0
  108. package/test/tracks-begin-together.test.js +44 -32
  109. package/test/two-viewers-one-picture.test.js +347 -0
  110. package/test/video-facts.test.js +102 -0
  111. package/test/viewer-outputs.test.js +273 -0
  112. package/test/viewer.test.js +91 -0
  113. package/utils/perf.js +1 -63
  114. package/services/container/index.js +0 -6
  115. package/services/container-index/avi.js +0 -167
  116. package/services/container-index/index.js +0 -118
  117. package/services/container-index/matroska.js +0 -336
  118. package/services/container-index/mp4.js +0 -358
  119. package/services/controllers/index.js +0 -2
  120. package/services/download/index.js +0 -8
  121. package/services/orchestrators/index.js +0 -2
  122. /package/services/{container-index → container}/ebml-reader.js +0 -0
  123. /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
  124. /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
@@ -1,188 +1,142 @@
1
- /**
2
- * @file The session's one statement of what it has produced.
3
- *
4
- * Every check builds real directories and real files, because what is under
5
- * test is exactly the reading of them: which run's copy wins, what an empty
6
- * file counts as, and whether a directory that has not changed is read again.
7
- */
8
-
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import path from "node:path";
12
- import os from "node:os";
13
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, unlinkSync } from "node:fs";
14
-
15
- import { ProducedIndex } from "../services/produced-index.js";
16
-
17
- /** The naming the fMP4 format uses, reduced to what the index asks of it. */
18
- const segmentFormat = {
19
- isSegmentFileName: (name) => /^segment-\d{5}\.mp4$/.test(name),
20
- segmentIndexFromName: (name) => {
21
- const match = /^segment-(\d{5})\.mp4$/.exec(name);
22
- return match ? Number(match[1]) : -1;
23
- }
24
- };
25
-
26
- const nameOf = (index) => `segment-${String(index).padStart(5, "0")}.mp4`;
27
-
28
- function session() {
29
- const dirPath = mkdtempSync(path.join(os.tmpdir(), "produced-index-"));
30
- return {
31
- dirPath,
32
- run(number) {
33
- const dir = path.join(dirPath, `run-${number}`);
34
- mkdirSync(dir, { recursive: true });
35
- return {
36
- dir,
37
- write(index, contents) {
38
- writeFileSync(path.join(dir, nameOf(index)), contents);
39
- return path.join(dir, nameOf(index));
40
- },
41
- writeNamed(name, contents) {
42
- writeFileSync(path.join(dir, name), contents);
43
- return path.join(dir, name);
44
- }
45
- };
46
- },
47
- index: () => new ProducedIndex({ dirPath, segmentFormat }),
48
- remove: () => rmSync(dirPath, { recursive: true, force: true })
49
- };
50
- }
51
-
52
- test("a file with bytes in it is produced; one that was only opened is not", () => {
53
- const s = session();
54
- try {
55
- const run = s.run(1);
56
- run.write(1, "a piece");
57
- run.write(2, "");
58
- const index = s.index();
59
-
60
- assert.deepEqual([...index.segmentNumbers()], [1]);
61
- assert.equal(index.pathOf(nameOf(1)), path.join(run.dir, nameOf(1)));
62
- // The empty file exists and is not an answer — this is the pair of beliefs
63
- // that stopped playback for ten minutes on 2026-09-03, now one belief.
64
- assert.equal(index.pathOf(nameOf(2)), null);
65
- } finally {
66
- s.remove();
67
- }
68
- });
69
-
70
- test("the newest run's copy wins, whichever run was read last", () => {
71
- const s = session();
72
- try {
73
- const first = s.run(1);
74
- first.write(7, "old");
75
- const second = s.run(2);
76
- second.write(7, "new");
77
- const index = s.index();
78
-
79
- assert.equal(index.pathOf(nameOf(7)), path.join(second.dir, nameOf(7)));
80
-
81
- // Touching the OLDER run must not make it the answer: the rule is applied
82
- // when the question is asked, not when a directory happens to be read.
83
- first.write(8, "older still");
84
- assert.equal(index.pathOf(nameOf(7)), path.join(second.dir, nameOf(7)));
85
- assert.equal(index.pathOf(nameOf(8)), path.join(first.dir, nameOf(8)));
86
- } finally {
87
- s.remove();
88
- }
89
- });
90
-
91
- test("the union of every run is what the session holds", () => {
92
- const s = session();
93
- try {
94
- s.run(1).write(1, "x");
95
- s.run(2).write(2, "y");
96
- s.run(3).write(3, "z");
97
- const index = s.index();
98
- assert.deepEqual([...index.segmentNumbers()].sort((a, b) => a - b), [1, 2, 3]);
99
- } finally {
100
- s.remove();
101
- }
102
- });
103
-
104
- test("a file that is not a segment is found by name", () => {
105
- const s = session();
106
- try {
107
- const run = s.run(1);
108
- run.writeNamed("init.mp4", "header");
109
- const index = s.index();
110
- assert.equal(index.pathOf("init.mp4"), path.join(run.dir, "init.mp4"));
111
- assert.ok(index.fileNames().includes("init.mp4"));
112
- } finally {
113
- s.remove();
114
- }
115
- });
116
-
117
- test("a run whose directory is gone stops answering", () => {
118
- const s = session();
119
- try {
120
- const run = s.run(1);
121
- run.write(4, "piece");
122
- const index = s.index();
123
- assert.equal(index.segmentNumbers().size, 1);
124
-
125
- rmSync(run.dir, { recursive: true, force: true });
126
- assert.equal(index.segmentNumbers().size, 0);
127
- assert.equal(index.pathOf(nameOf(4)), null);
128
- } finally {
129
- s.remove();
130
- }
131
- });
132
-
133
- test("a file removed on purpose is not still an answer", () => {
134
- const s = session();
135
- try {
136
- const run = s.run(1);
137
- const written = run.write(9, "piece");
138
- const index = s.index();
139
- assert.equal(index.pathOf(nameOf(9)), written);
140
-
141
- unlinkSync(written);
142
- // The directory's own time has moved, but a caller that deletes and asks in
143
- // the same tick must not be told the file is there.
144
- index.invalidate();
145
- assert.equal(index.pathOf(nameOf(9)), null);
146
- } finally {
147
- s.remove();
148
- }
149
- });
150
-
151
- test("an unchanged run is not listed again", () => {
152
- const s = session();
153
- try {
154
- const run = s.run(1);
155
- run.write(1, "piece");
156
- const index = s.index();
157
-
158
- index.segmentNumbers();
159
- const afterFirst = index.directoryReads;
160
- assert.equal(afterFirst, 1, "the first question reads the run");
161
-
162
- for (let asked = 0; asked < 20; asked += 1) {
163
- index.segmentNumbers();
164
- index.pathOf(nameOf(1));
165
- }
166
- assert.equal(
167
- index.directoryReads,
168
- afterFirst,
169
- "forty more questions about a run that has not changed must read nothing"
170
- );
171
-
172
- run.write(2, "another piece");
173
- index.segmentNumbers();
174
- assert.equal(index.directoryReads, afterFirst + 1, "a run that gained a file is read once more");
175
- } finally {
176
- s.remove();
177
- }
178
- });
179
-
180
- test("a session directory that does not exist answers with nothing", () => {
181
- const index = new ProducedIndex({
182
- dirPath: path.join(os.tmpdir(), "produced-index-absent-0000"),
183
- segmentFormat
184
- });
185
- assert.deepEqual(index.runDirs(), []);
186
- assert.equal(index.pathOf(nameOf(1)), null);
187
- assert.deepEqual([...index.segmentNumbers()], []);
188
- });
1
+ /**
2
+ * @file The session's one statement of what it has produced.
3
+ *
4
+ * Every check builds real directories and real files, because what is under
5
+ * test is exactly the reading of them: which run's copy wins, what an empty
6
+ * file counts as, and whether a directory that has not changed is read again.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import path from "node:path";
12
+ import os from "node:os";
13
+ import { mkdtempSync, writeFileSync, rmSync, unlinkSync } from "node:fs";
14
+
15
+ import { ProducedIndex } from "../services/produced-index.js";
16
+
17
+ /** The naming the fMP4 format uses, reduced to what the index asks of it. */
18
+ const segmentFormat = {
19
+ isSegmentFileName: (name) => /^segment-\d{5}\.mp4$/.test(name),
20
+ segmentIndexFromName: (name) => {
21
+ const match = /^segment-(\d{5})\.mp4$/.exec(name);
22
+ return match ? Number(match[1]) : -1;
23
+ }
24
+ };
25
+
26
+ const nameOf = (index) => `segment-${String(index).padStart(5, "0")}.mp4`;
27
+
28
+ function output() {
29
+ const dirPath = mkdtempSync(path.join(os.tmpdir(), "produced-index-"));
30
+ return {
31
+ dirPath,
32
+ write(index, contents) {
33
+ writeFileSync(path.join(dirPath, nameOf(index)), contents);
34
+ return path.join(dirPath, nameOf(index));
35
+ },
36
+ writeNamed(name, contents) {
37
+ writeFileSync(path.join(dirPath, name), contents);
38
+ return path.join(dirPath, name);
39
+ },
40
+ index: () => new ProducedIndex({ dirPath, segmentFormat }),
41
+ remove: () => rmSync(dirPath, { recursive: true, force: true })
42
+ };
43
+ }
44
+
45
+ test("a file with bytes in it is produced; one that was only opened is not", () => {
46
+ const s = output();
47
+ try {
48
+ s.write(1, "a piece");
49
+ s.write(2, "");
50
+ const index = s.index();
51
+
52
+ assert.deepEqual([...index.segmentNumbers()], [1]);
53
+ assert.equal(index.pathOf(nameOf(1)), path.join(s.dirPath, nameOf(1)));
54
+ // The empty file exists and is not an answer — this is the pair of beliefs
55
+ // that stopped playback for ten minutes on 2026-09-03, now one belief.
56
+ assert.equal(index.pathOf(nameOf(2)), null);
57
+ } finally {
58
+ s.remove();
59
+ }
60
+ });
61
+
62
+
63
+
64
+ test("a file that is not a segment is found by name", () => {
65
+ const s = output();
66
+ try {
67
+ s.writeNamed("init.mp4", "header");
68
+ const index = s.index();
69
+ assert.equal(index.pathOf("init.mp4"), path.join(s.dirPath, "init.mp4"));
70
+ assert.ok(index.fileNames().includes("init.mp4"));
71
+ } finally {
72
+ s.remove();
73
+ }
74
+ });
75
+
76
+ test("a directory that is gone stops answering", () => {
77
+ const s = output();
78
+ try {
79
+ s.write(4, "piece");
80
+ const index = s.index();
81
+ assert.equal(index.segmentNumbers().size, 1);
82
+
83
+ rmSync(s.dirPath, { recursive: true, force: true });
84
+ assert.equal(index.segmentNumbers().size, 0);
85
+ assert.equal(index.pathOf(nameOf(4)), null);
86
+ } finally {
87
+ s.remove();
88
+ }
89
+ });
90
+
91
+ test("a file removed on purpose is not still an answer", () => {
92
+ const s = output();
93
+ try {
94
+ const written = s.write(9, "piece");
95
+ const index = s.index();
96
+ assert.equal(index.pathOf(nameOf(9)), written);
97
+
98
+ unlinkSync(written);
99
+ // The directory's own time has moved, but a caller that deletes and asks in
100
+ // the same tick must not be told the file is there.
101
+ index.invalidate();
102
+ assert.equal(index.pathOf(nameOf(9)), null);
103
+ } finally {
104
+ s.remove();
105
+ }
106
+ });
107
+
108
+ test("an unchanged directory is not listed again", () => {
109
+ const s = output();
110
+ try {
111
+ s.write(1, "piece");
112
+ const index = s.index();
113
+
114
+ index.segmentNumbers();
115
+ assert.equal(index.directoryReads, 1, "the first question reads what is there and no more");
116
+
117
+ for (let asked = 0; asked < 20; asked += 1) {
118
+ index.segmentNumbers();
119
+ index.pathOf(nameOf(1));
120
+ }
121
+ assert.equal(
122
+ index.directoryReads,
123
+ 1,
124
+ "forty more questions about a directory that has not changed must read nothing"
125
+ );
126
+
127
+ s.write(2, "another piece");
128
+ index.segmentNumbers();
129
+ assert.equal(index.directoryReads, 2, "a directory that gained a file is read once more");
130
+ } finally {
131
+ s.remove();
132
+ }
133
+ });
134
+
135
+ test("an output directory that does not exist answers with nothing", () => {
136
+ const index = new ProducedIndex({
137
+ dirPath: path.join(os.tmpdir(), "produced-index-absent-0000"),
138
+ segmentFormat
139
+ });
140
+ assert.equal(index.pathOf(nameOf(1)), null);
141
+ assert.deepEqual([...index.segmentNumbers()], []);
142
+ });