@torrent-tv/proxy 2.89.6 → 2.89.8

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 (92) hide show
  1. package/AGENTS.md +32 -12
  2. package/CHANGELOG.md +2340 -2330
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/biome.json +412 -412
  6. package/docs/browser-proxy-contract.md +155 -155
  7. package/docs/container-architecture.md +273 -273
  8. package/docs/encode-run-state.md +1 -1
  9. package/docs/playback-start-forecast.md +113 -98
  10. package/package.json +51 -50
  11. package/routes/api/sources/files/get.js +106 -106
  12. package/scripts/build-audio-samples.mjs +31 -31
  13. package/services/encode/EncodeRun.js +67 -15
  14. package/services/encode/EncodeRuns.js +19 -7
  15. package/services/encode/OutputOpening.js +784 -784
  16. package/services/encode/audio-calibration.js +67 -67
  17. package/services/encode/audio-work-rate.js +15 -15
  18. package/services/encode/encode-exit.js +2 -0
  19. package/services/encode/hwaccel.js +1894 -1894
  20. package/services/encode/output/index.js +24 -24
  21. package/services/encode/piece-completeness.js +60 -2
  22. package/services/encode/run-command.js +757 -755
  23. package/services/encode/segment-formats/index.js +1 -0
  24. package/services/encode/segment-formats/mp4-boxes.js +8 -1
  25. package/services/media/ContainerOrchestrator.js +202 -202
  26. package/services/media/KeyframeTables.js +356 -356
  27. package/services/media/SubtitleCues.js +592 -592
  28. package/services/media/audio-inventory.js +299 -299
  29. package/services/media/container/AviContainer.js +268 -268
  30. package/services/media/container/Container.js +632 -632
  31. package/services/media/container/ContainerFactory.js +38 -38
  32. package/services/media/container/MatroskaContainer.js +1031 -1031
  33. package/services/media/container/Mp4Container.js +1354 -1354
  34. package/services/media/container/SubtitleFileContainer.js +339 -339
  35. package/services/media/container/ebml-stream.js +174 -174
  36. package/services/media/container/matroska-clusters.js +1165 -1165
  37. package/services/media/container/unavailable.js +77 -77
  38. package/services/media/tracks/ContainerTrack.js +84 -84
  39. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  40. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  41. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  42. package/services/torrent/Contents.js +382 -382
  43. package/services/torrent/episode-naming.js +182 -182
  44. package/services/torrent/torrent-pool.js +2688 -2688
  45. package/services/torrent/worker/client.js +836 -836
  46. package/services/torrent/worker/held-bytes.js +231 -231
  47. package/services/torrent/worker/worker.js +1137 -1137
  48. package/services/transport/data-channel-handler.js +1373 -1373
  49. package/services/viewer/PriorityMap.js +421 -421
  50. package/test/audio-calibration.test.js +23 -23
  51. package/test/auto-quality-step.test.js +762 -762
  52. package/test/coalesce.test.js +39 -39
  53. package/test/coverage-follows-the-disk.test.js +5 -0
  54. package/test/decode-cost.test.js +475 -475
  55. package/test/encode-run.test.js +58 -0
  56. package/test/episode-naming.test.js +131 -131
  57. package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
  58. package/test/fixtures/audio-frame-boundary.json +1 -1
  59. package/test/fixtures/refused-audio-20261002.json +10 -0
  60. package/test/fmp4-presentation-clock.test.js +4 -2
  61. package/test/format-keyed-outputs.test.js +78 -78
  62. package/test/health-metrics.test.js +11 -13
  63. package/test/held-read-ownership.test.js +77 -77
  64. package/test/helpers/encode-run.js +14 -6
  65. package/test/helpers/held-file.js +77 -77
  66. package/test/helpers/manager.js +62 -62
  67. package/test/helpers/matroska-file.js +296 -296
  68. package/test/keyframe-table-budget.test.js +123 -123
  69. package/test/matroska-cluster-discovery.test.js +323 -323
  70. package/test/one-output-one-session.test.js +273 -273
  71. package/test/output-speed.test.js +6 -1
  72. package/test/piece-completeness.test.js +37 -1
  73. package/test/quality-variants.test.js +1274 -1270
  74. package/test/rate-control-in-the-output.test.js +4 -3
  75. package/test/reads-not-yet-arrived.test.js +218 -218
  76. package/test/seek-frees-the-viewers-requests.test.js +1 -1
  77. package/test/segment-closed-on-both-branches.test.js +114 -114
  78. package/test/segment-judged-by-its-own-header.test.js +222 -222
  79. package/test/segment-serve-wiring.test.js +481 -481
  80. package/test/serving-existing-output.test.js +111 -111
  81. package/test/source-files-route.test.js +113 -113
  82. package/test/subtitle-cue-walk.test.js +385 -385
  83. package/test/subtitle-metadata.test.js +15 -15
  84. package/test/subtitle-plan-waits.test.js +125 -125
  85. package/test/subtitle-track-numbering.test.js +391 -391
  86. package/test/two-viewers-one-picture.test.js +508 -508
  87. package/test/viewer-requests-admission.test.js +92 -92
  88. package/test/viewer-starts-on-map.test.js +27 -27
  89. package/test/viewer-states.test.js +5 -2
  90. package/test/viewing-generations.test.js +390 -390
  91. package/test/whole-file-held-read.test.js +30 -30
  92. package/utils/coalesce.js +41 -41
@@ -1,273 +1,273 @@
1
- /**
2
- * @file Many viewers of one output are handed one session.
3
- *
4
- * This is the product behaviour the whole output key exists for: two people
5
- * watching the same film, in the same form, at whatever moments, are served by
6
- * one encode and one set of pieces. It had no check of any kind — every test
7
- * that needed a session either built one by hand or replaced
8
- * `createOrGetSession` with a stub — so the one line that decides it could have
9
- * been broken by any edit without a word.
10
- *
11
- * What is checked here is the decision, not the creation: a session under the
12
- * name this request produces already exists, and the request must be answered
13
- * with it rather than with a new one. The name is the format produced, so the
14
- * request is first taken as far as the format — the file's facts and its
15
- * keyframe table are given as already known — and making the FIRST session,
16
- * which reaches the disk, is a different subject.
17
- */
18
-
19
- import test from "node:test";
20
- import assert from "node:assert/strict";
21
- import { wireOutputs } from "../services/server/wire-outputs.js";
22
- import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/encode/output/OutputSpec.js";
23
- import { Timeline } from "../services/encode/output/Timeline.js";
24
- import { SourceFile } from "../services/media/SourceFile.js";
25
-
26
- const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
27
-
28
- /**
29
- * What a browser asks for: this file, copied, sound inside the stream.
30
- *
31
- * @param {object} [over]
32
- * @returns {object}
33
- */
34
- function request(over = {}) {
35
- return {
36
- sourceKey: TORRENT,
37
- fileIndex: 0,
38
- fileName: "film.mkv",
39
- transcodeVideo: false,
40
- transcodeAudio: false,
41
- audioTrackIndex: 0,
42
- segmentFormatId: "fmp4",
43
- ...over
44
- };
45
- }
46
-
47
- /**
48
- * The name the manager will look that request up under, built the way it builds
49
- * it: a copied picture carrying its own sound, cut at the file's keyframes.
50
- *
51
- * @returns {string}
52
- */
53
- function specOfThatOutput({ trackIndex = 0, transcode = false } = {}) {
54
- return new OutputSpec({
55
- sourceKey: TORRENT,
56
- segmentFormatId: "fmp4",
57
- grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
58
- video: new VideoOutput({ fileIndex: 0, encode: null }),
59
- audio: new AudioOutput({ fileIndex: 0, trackIndex, transcode })
60
- });
61
- }
62
-
63
- /** @returns {string} */
64
- function nameOfThatOutput(sound) {
65
- return specOfThatOutput(sound).toName();
66
- }
67
-
68
- /**
69
- * @param {object} manager
70
- * @param {{ trackIndex?: number, transcode?: boolean }} [sound] - The soundtrack
71
- * the seeded output carries; the file's own, copied, when left out.
72
- * @returns {object} The session it is seeded with.
73
- */
74
- function seedOneSession(manager, sound = {}) {
75
- const session = {
76
- id: nameOfThatOutput(sound),
77
- spec: specOfThatOutput(sound),
78
- // Its own address, so two seeded outputs are two places for viewers.
79
- outputKey: specOfThatOutput(sound).toKey(),
80
- state: "ready",
81
- file: new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }),
82
- timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
83
- runs: new Set(),
84
- progress: { processedSeconds: 0, startPositionSeconds: 0, updatedAt: Date.now() },
85
- lastAccessedAt: Date.now(),
86
- // READY, because a joining viewer waits for the session to be able to
87
- // answer them, and this file is about which session answers rather than
88
- // about that wait. A session publishing its own playlist from the probed
89
- // duration has nothing to wait for; left out, every check here would sit
90
- // out the warm-up deadline and would be measuring that timer.
91
- useSyntheticPlaylist: true
92
- };
93
- manager.outputs.set(session.id, session);
94
- return session;
95
- }
96
-
97
- /**
98
- * What the proxy already knows about the file when a second viewer arrives:
99
- * what a probe said, and where its keyframes are.
100
- *
101
- * @param {object} manager
102
- */
103
- function knownFile(manager) {
104
- // This check exercises output identity. Machine admission has its own tests.
105
- manager.admission.admitsWatching = () => ({ admitted: true, reason: "", speedX: 8 });
106
- manager.admission.previewCandidate = () => ({ admitted: true, reason: "", speedX: 8 });
107
- manager.getCachedMediaInfo = () => ({ durationSeconds: 8, width: 1920, height: 1080, fps: 24 });
108
- // The soundtracks the plan listed: the file's own, at the rate it states, and
109
- // a dub beside it that states nothing — which no link can be asked about as a
110
- // copy, so it is sent re-encoded.
111
- manager.getCachedAudioTracks = () => [
112
- { index: 0, fileIndex: 0, sourceTrackIndex: 0, bitrateKbps: 128 },
113
- { index: 1, fileIndex: 0, sourceTrackIndex: 1, bitrateKbps: null, peakKbps: null }
114
- ];
115
- manager.keyframeTables.learn({ sourceKey: TORRENT, fileIndex: 0 }, { times: [0, 4], format: "test" });
116
- }
117
-
118
- test("a second viewer of one output is served by the session that exists", async (t) => {
119
- const manager = wireOutputs({
120
- enabled: true,
121
- ffmpegBin: "ffmpeg",
122
- localBindHost: "127.0.0.1",
123
- localPort: 9090
124
- });
125
- t.after(() => manager.lifecycle.disposeAll());
126
- // No plan runs here: this file is about which session answers a request, and
127
- // placing encoders would spawn real processes.
128
- manager.encodeRuns.planEncodersNow = () => {};
129
- manager.encodeRuns.planEncodersSoon = () => {};
130
- knownFile(manager);
131
- const seeded = seedOneSession(manager);
132
-
133
- const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
134
-
135
- assert.equal(answered, seeded, "one output, one session");
136
- assert.ok(manager.viewers.forOutput(seeded).has("viewer-two"), "and the second viewer is on it");
137
- });
138
-
139
- test("a request whose parameters differ is not that session", async (t) => {
140
- // The other half, and it is what makes the first mean anything: the name is
141
- // the output's, so a request producing something else must not find it. Here
142
- // the sound is to be re-encoded, which is a different output and a different
143
- // set of bytes.
144
- const manager = wireOutputs({
145
- enabled: true,
146
- ffmpegBin: "ffmpeg",
147
- localBindHost: "127.0.0.1",
148
- localPort: 9090,
149
- // A session made for real here waits for a playlist that nothing is going
150
- // to write. Zero, so this check spends no time on a deadline it is not
151
- // about — the deadline has its own subject elsewhere.
152
- startupWaitMs: 0
153
- });
154
- t.after(() => manager.lifecycle.disposeAll());
155
- manager.encodeRuns.planEncodersNow = () => {};
156
- manager.encodeRuns.planEncodersSoon = () => {};
157
- const seeded = seedOneSession(manager);
158
-
159
- let answered = null;
160
- try {
161
- answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two", transcodeAudio: true }));
162
- } catch {
163
- // Making a session for real needs a probe and a disk, which this file does
164
- // not give it. Failing there is the proof: it did not take the seeded one.
165
- answered = null;
166
- }
167
-
168
- assert.notEqual(answered, seeded);
169
- assert.ok(!manager.viewers.forOutput(seeded).has("viewer-two"), "and nobody was added to somebody else's output");
170
- });
171
-
172
- test("two screens that come to the same format share one output, and a different format does not", async (t) => {
173
- const manager = wireOutputs({
174
- enabled: true,
175
- ffmpegBin: "ffmpeg",
176
- localBindHost: "127.0.0.1",
177
- localPort: 9090,
178
- startupWaitMs: 0,
179
- // A host with no ladder to choose from: the box asked for, fitted to the
180
- // source, is what is produced.
181
- videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
182
- });
183
- t.after(() => manager.lifecycle.disposeAll());
184
- manager.encodeRuns.planEncodersNow = () => {};
185
- manager.encodeRuns.planEncodersSoon = () => {};
186
- knownFile(manager);
187
- const spec = new OutputSpec({
188
- sourceKey: TORRENT,
189
- segmentFormatId: "fmp4",
190
- grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
191
- video: new VideoOutput({
192
- fileIndex: 0,
193
- encode: { encoder: "h264_vaapi", width: 1920, height: 1080, fps: 24, preset: null, tonemap: false }
194
- }),
195
- audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: false })
196
- });
197
- const seeded = { ...seedOneSession(manager), id: spec.toName(), spec, timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "uniform" }) };
198
- manager.outputs.delete(nameOfThatOutput());
199
- manager.outputs.set(seeded.id, seeded);
200
-
201
- // A taller window than the film: fitted to the source it is the same 1920x1080.
202
- const joined = await manager.viewerRequests.createOrGetSession(
203
- request({ consumerId: "tall-window", transcodeVideo: true, targetWidth: 1920, targetHeight: 1200 })
204
- );
205
- assert.equal(joined, seeded, "the key names the format produced, not the window it was asked for");
206
-
207
- let other = null;
208
- try {
209
- other = await manager.viewerRequests.createOrGetSession(
210
- request({ consumerId: "small-window", transcodeVideo: true, targetWidth: 1280, targetHeight: 720 })
211
- );
212
- } catch {
213
- other = null;
214
- }
215
- assert.notEqual(other, seeded, "a smaller picture is another output");
216
- assert.ok(!manager.viewers.forOutput(seeded).has("small-window"));
217
- });
218
-
219
- test("a remembered dub with no stated rate opens on the output that sends it as AAC, not on its copy", async (t) => {
220
- // The field case of 2026-10-01: the next episode opened on a dub the page
221
- // remembered, which its browser plays as it stands and nothing states a rate
222
- // for. Opened as a copy, no link could be asked about it and the episode was
223
- // refused; it is the AAC output that answers.
224
- const manager = wireOutputs({
225
- enabled: true,
226
- ffmpegBin: "ffmpeg",
227
- localBindHost: "127.0.0.1",
228
- localPort: 9090,
229
- // A measured copy speed, so this machine can price the output and the
230
- // admission answers on room rather than on knowing nothing.
231
- copySpeedX: 20
232
- });
233
- t.after(() => manager.lifecycle.disposeAll());
234
- manager.encodeRuns.planEncodersNow = () => {};
235
- manager.encodeRuns.planEncodersSoon = () => {};
236
- knownFile(manager);
237
- const copied = seedOneSession(manager, { trackIndex: 1, transcode: false });
238
- const encoded = seedOneSession(manager, { trackIndex: 1, transcode: true });
239
-
240
- const answered = await manager.viewerRequests.createOrGetSession(
241
- request({ consumerId: "viewer-two", audioTrackIndex: 1, transcodeAudio: false })
242
- );
243
-
244
- assert.equal(answered, encoded, "the dub is re-encoded");
245
- assert.notEqual(answered, copied);
246
- assert.deepEqual(
247
- manager.renditions.soundtrackOf(answered, "viewer-two"),
248
- { trackIndex: 1, transcode: true },
249
- "and the viewer is recorded as receiving AAC"
250
- );
251
- });
252
-
253
- test("a track whose rate is stated is still copied for a browser that plays it", async (t) => {
254
- const manager = wireOutputs({
255
- enabled: true,
256
- ffmpegBin: "ffmpeg",
257
- localBindHost: "127.0.0.1",
258
- localPort: 9090,
259
- // A measured copy speed, so this machine can price the output and the
260
- // admission answers on room rather than on knowing nothing.
261
- copySpeedX: 20
262
- });
263
- t.after(() => manager.lifecycle.disposeAll());
264
- manager.encodeRuns.planEncodersNow = () => {};
265
- manager.encodeRuns.planEncodersSoon = () => {};
266
- knownFile(manager);
267
- const copied = seedOneSession(manager);
268
- seedOneSession(manager, { trackIndex: 0, transcode: true });
269
-
270
- const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
271
-
272
- assert.equal(answered, copied);
273
- });
1
+ /**
2
+ * @file Many viewers of one output are handed one session.
3
+ *
4
+ * This is the product behaviour the whole output key exists for: two people
5
+ * watching the same film, in the same form, at whatever moments, are served by
6
+ * one encode and one set of pieces. It had no check of any kind — every test
7
+ * that needed a session either built one by hand or replaced
8
+ * `createOrGetSession` with a stub — so the one line that decides it could have
9
+ * been broken by any edit without a word.
10
+ *
11
+ * What is checked here is the decision, not the creation: a session under the
12
+ * name this request produces already exists, and the request must be answered
13
+ * with it rather than with a new one. The name is the format produced, so the
14
+ * request is first taken as far as the format — the file's facts and its
15
+ * keyframe table are given as already known — and making the FIRST session,
16
+ * which reaches the disk, is a different subject.
17
+ */
18
+
19
+ import test from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { wireOutputs } from "../services/server/wire-outputs.js";
22
+ import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/encode/output/OutputSpec.js";
23
+ import { Timeline } from "../services/encode/output/Timeline.js";
24
+ import { SourceFile } from "../services/media/SourceFile.js";
25
+
26
+ const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
27
+
28
+ /**
29
+ * What a browser asks for: this file, copied, sound inside the stream.
30
+ *
31
+ * @param {object} [over]
32
+ * @returns {object}
33
+ */
34
+ function request(over = {}) {
35
+ return {
36
+ sourceKey: TORRENT,
37
+ fileIndex: 0,
38
+ fileName: "film.mkv",
39
+ transcodeVideo: false,
40
+ transcodeAudio: false,
41
+ audioTrackIndex: 0,
42
+ segmentFormatId: "fmp4",
43
+ ...over
44
+ };
45
+ }
46
+
47
+ /**
48
+ * The name the manager will look that request up under, built the way it builds
49
+ * it: a copied picture carrying its own sound, cut at the file's keyframes.
50
+ *
51
+ * @returns {string}
52
+ */
53
+ function specOfThatOutput({ trackIndex = 0, transcode = false } = {}) {
54
+ return new OutputSpec({
55
+ sourceKey: TORRENT,
56
+ segmentFormatId: "fmp4",
57
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
58
+ video: new VideoOutput({ fileIndex: 0, encode: null }),
59
+ audio: new AudioOutput({ fileIndex: 0, trackIndex, transcode })
60
+ });
61
+ }
62
+
63
+ /** @returns {string} */
64
+ function nameOfThatOutput(sound) {
65
+ return specOfThatOutput(sound).toName();
66
+ }
67
+
68
+ /**
69
+ * @param {object} manager
70
+ * @param {{ trackIndex?: number, transcode?: boolean }} [sound] - The soundtrack
71
+ * the seeded output carries; the file's own, copied, when left out.
72
+ * @returns {object} The session it is seeded with.
73
+ */
74
+ function seedOneSession(manager, sound = {}) {
75
+ const session = {
76
+ id: nameOfThatOutput(sound),
77
+ spec: specOfThatOutput(sound),
78
+ // Its own address, so two seeded outputs are two places for viewers.
79
+ outputKey: specOfThatOutput(sound).toKey(),
80
+ state: "ready",
81
+ file: new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }),
82
+ timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
83
+ runs: new Set(),
84
+ progress: { processedSeconds: 0, startPositionSeconds: 0, updatedAt: Date.now() },
85
+ lastAccessedAt: Date.now(),
86
+ // READY, because a joining viewer waits for the session to be able to
87
+ // answer them, and this file is about which session answers rather than
88
+ // about that wait. A session publishing its own playlist from the probed
89
+ // duration has nothing to wait for; left out, every check here would sit
90
+ // out the warm-up deadline and would be measuring that timer.
91
+ useSyntheticPlaylist: true
92
+ };
93
+ manager.outputs.set(session.id, session);
94
+ return session;
95
+ }
96
+
97
+ /**
98
+ * What the proxy already knows about the file when a second viewer arrives:
99
+ * what a probe said, and where its keyframes are.
100
+ *
101
+ * @param {object} manager
102
+ */
103
+ function knownFile(manager) {
104
+ // This check exercises output identity. Machine admission has its own tests.
105
+ manager.admission.admitsWatching = () => ({ admitted: true, reason: "", speedX: 8 });
106
+ manager.admission.previewCandidate = () => ({ admitted: true, reason: "", speedX: 8 });
107
+ manager.getCachedMediaInfo = () => ({ durationSeconds: 8, width: 1920, height: 1080, fps: 24 });
108
+ // The soundtracks the plan listed: the file's own, at the rate it states, and
109
+ // a dub beside it that states nothing — which no link can be asked about as a
110
+ // copy, so it is sent re-encoded.
111
+ manager.getCachedAudioTracks = () => [
112
+ { index: 0, fileIndex: 0, sourceTrackIndex: 0, bitrateKbps: 128 },
113
+ { index: 1, fileIndex: 0, sourceTrackIndex: 1, bitrateKbps: null, peakKbps: null }
114
+ ];
115
+ manager.keyframeTables.learn({ sourceKey: TORRENT, fileIndex: 0 }, { times: [0, 4], format: "test" });
116
+ }
117
+
118
+ test("a second viewer of one output is served by the session that exists", async (t) => {
119
+ const manager = wireOutputs({
120
+ enabled: true,
121
+ ffmpegBin: "ffmpeg",
122
+ localBindHost: "127.0.0.1",
123
+ localPort: 9090
124
+ });
125
+ t.after(() => manager.lifecycle.disposeAll());
126
+ // No plan runs here: this file is about which session answers a request, and
127
+ // placing encoders would spawn real processes.
128
+ manager.encodeRuns.planEncodersNow = () => {};
129
+ manager.encodeRuns.planEncodersSoon = () => {};
130
+ knownFile(manager);
131
+ const seeded = seedOneSession(manager);
132
+
133
+ const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
134
+
135
+ assert.equal(answered, seeded, "one output, one session");
136
+ assert.ok(manager.viewers.forOutput(seeded).has("viewer-two"), "and the second viewer is on it");
137
+ });
138
+
139
+ test("a request whose parameters differ is not that session", async (t) => {
140
+ // The other half, and it is what makes the first mean anything: the name is
141
+ // the output's, so a request producing something else must not find it. Here
142
+ // the sound is to be re-encoded, which is a different output and a different
143
+ // set of bytes.
144
+ const manager = wireOutputs({
145
+ enabled: true,
146
+ ffmpegBin: "ffmpeg",
147
+ localBindHost: "127.0.0.1",
148
+ localPort: 9090,
149
+ // A session made for real here waits for a playlist that nothing is going
150
+ // to write. Zero, so this check spends no time on a deadline it is not
151
+ // about — the deadline has its own subject elsewhere.
152
+ startupWaitMs: 0
153
+ });
154
+ t.after(() => manager.lifecycle.disposeAll());
155
+ manager.encodeRuns.planEncodersNow = () => {};
156
+ manager.encodeRuns.planEncodersSoon = () => {};
157
+ const seeded = seedOneSession(manager);
158
+
159
+ let answered = null;
160
+ try {
161
+ answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two", transcodeAudio: true }));
162
+ } catch {
163
+ // Making a session for real needs a probe and a disk, which this file does
164
+ // not give it. Failing there is the proof: it did not take the seeded one.
165
+ answered = null;
166
+ }
167
+
168
+ assert.notEqual(answered, seeded);
169
+ assert.ok(!manager.viewers.forOutput(seeded).has("viewer-two"), "and nobody was added to somebody else's output");
170
+ });
171
+
172
+ test("two screens that come to the same format share one output, and a different format does not", async (t) => {
173
+ const manager = wireOutputs({
174
+ enabled: true,
175
+ ffmpegBin: "ffmpeg",
176
+ localBindHost: "127.0.0.1",
177
+ localPort: 9090,
178
+ startupWaitMs: 0,
179
+ // A host with no ladder to choose from: the box asked for, fitted to the
180
+ // source, is what is produced.
181
+ videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
182
+ });
183
+ t.after(() => manager.lifecycle.disposeAll());
184
+ manager.encodeRuns.planEncodersNow = () => {};
185
+ manager.encodeRuns.planEncodersSoon = () => {};
186
+ knownFile(manager);
187
+ const spec = new OutputSpec({
188
+ sourceKey: TORRENT,
189
+ segmentFormatId: "fmp4",
190
+ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
191
+ video: new VideoOutput({
192
+ fileIndex: 0,
193
+ encode: { encoder: "h264_vaapi", width: 1920, height: 1080, fps: 24, preset: null, tonemap: false }
194
+ }),
195
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: false })
196
+ });
197
+ const seeded = { ...seedOneSession(manager), id: spec.toName(), spec, timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "uniform" }) };
198
+ manager.outputs.delete(nameOfThatOutput());
199
+ manager.outputs.set(seeded.id, seeded);
200
+
201
+ // A taller window than the film: fitted to the source it is the same 1920x1080.
202
+ const joined = await manager.viewerRequests.createOrGetSession(
203
+ request({ consumerId: "tall-window", transcodeVideo: true, targetWidth: 1920, targetHeight: 1200 })
204
+ );
205
+ assert.equal(joined, seeded, "the key names the format produced, not the window it was asked for");
206
+
207
+ let other = null;
208
+ try {
209
+ other = await manager.viewerRequests.createOrGetSession(
210
+ request({ consumerId: "small-window", transcodeVideo: true, targetWidth: 1280, targetHeight: 720 })
211
+ );
212
+ } catch {
213
+ other = null;
214
+ }
215
+ assert.notEqual(other, seeded, "a smaller picture is another output");
216
+ assert.ok(!manager.viewers.forOutput(seeded).has("small-window"));
217
+ });
218
+
219
+ test("a remembered dub with no stated rate opens on the output that sends it as AAC, not on its copy", async (t) => {
220
+ // The field case of 2026-10-01: the next episode opened on a dub the page
221
+ // remembered, which its browser plays as it stands and nothing states a rate
222
+ // for. Opened as a copy, no link could be asked about it and the episode was
223
+ // refused; it is the AAC output that answers.
224
+ const manager = wireOutputs({
225
+ enabled: true,
226
+ ffmpegBin: "ffmpeg",
227
+ localBindHost: "127.0.0.1",
228
+ localPort: 9090,
229
+ // A measured copy speed, so this machine can price the output and the
230
+ // admission answers on room rather than on knowing nothing.
231
+ copySpeedX: 20
232
+ });
233
+ t.after(() => manager.lifecycle.disposeAll());
234
+ manager.encodeRuns.planEncodersNow = () => {};
235
+ manager.encodeRuns.planEncodersSoon = () => {};
236
+ knownFile(manager);
237
+ const copied = seedOneSession(manager, { trackIndex: 1, transcode: false });
238
+ const encoded = seedOneSession(manager, { trackIndex: 1, transcode: true });
239
+
240
+ const answered = await manager.viewerRequests.createOrGetSession(
241
+ request({ consumerId: "viewer-two", audioTrackIndex: 1, transcodeAudio: false })
242
+ );
243
+
244
+ assert.equal(answered, encoded, "the dub is re-encoded");
245
+ assert.notEqual(answered, copied);
246
+ assert.deepEqual(
247
+ manager.renditions.soundtrackOf(answered, "viewer-two"),
248
+ { trackIndex: 1, transcode: true },
249
+ "and the viewer is recorded as receiving AAC"
250
+ );
251
+ });
252
+
253
+ test("a track whose rate is stated is still copied for a browser that plays it", async (t) => {
254
+ const manager = wireOutputs({
255
+ enabled: true,
256
+ ffmpegBin: "ffmpeg",
257
+ localBindHost: "127.0.0.1",
258
+ localPort: 9090,
259
+ // A measured copy speed, so this machine can price the output and the
260
+ // admission answers on room rather than on knowing nothing.
261
+ copySpeedX: 20
262
+ });
263
+ t.after(() => manager.lifecycle.disposeAll());
264
+ manager.encodeRuns.planEncodersNow = () => {};
265
+ manager.encodeRuns.planEncodersSoon = () => {};
266
+ knownFile(manager);
267
+ const copied = seedOneSession(manager);
268
+ seedOneSession(manager, { trackIndex: 0, transcode: true });
269
+
270
+ const answered = await manager.viewerRequests.createOrGetSession(request({ consumerId: "viewer-two" }));
271
+
272
+ assert.equal(answered, copied);
273
+ });
@@ -11,6 +11,7 @@ import test from "node:test";
11
11
  import assert from "node:assert/strict";
12
12
  import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
13
13
  import { OutputCatalog } from "../services/encode/output/OutputCatalog.js";
14
+ import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
14
15
  import { outputSpec } from "./helpers/output-spec.js";
15
16
 
16
17
  const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
@@ -21,7 +22,11 @@ const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
21
22
  */
22
23
  function costOf(sessions, host = {}) {
23
24
  const outputs = new OutputCatalog();
24
- sessions.forEach((session, index) => outputs.set(String(index), session));
25
+ sessions.forEach(({ lastAloneSpeed, ...session }, index) => {
26
+ // What a run measured belongs to the quality component's state of the output.
27
+ qualityStateOf(session).lastAloneSpeed = lastAloneSpeed ?? undefined;
28
+ outputs.set(String(index), session);
29
+ });
25
30
  return new EncodeCost({
26
31
  runsFor: () => [],
27
32
  stateFor: () => "IDLE",
@@ -7,8 +7,13 @@
7
7
 
8
8
  import test from "node:test";
9
9
  import assert from "node:assert/strict";
10
- import { cutOf, judgePiece } from "../services/encode/piece-completeness.js";
10
+ import { cutOf, judgePiece, originOf, productionOf } from "../services/encode/piece-completeness.js";
11
11
  import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
12
+ import { readFileSync } from "node:fs";
13
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
14
+
15
+ const evidence = JSON.parse(readFileSync(new URL("./fixtures/refused-audio-20261002.json", import.meta.url)));
16
+ const captured = (name) => fmp4Format.readMediaRanges(Buffer.from(evidence.files[name], "base64"));
12
17
 
13
18
  /** Media ending where stated, in the shape a format's reading has. */
14
19
  const endingAt = (seconds) => ({ end: seconds });
@@ -40,3 +45,34 @@ test("a piece closed short of its cut is not whole", () => {
40
45
  test("a piece with no playable media is not whole, whatever its cut", () => {
41
46
  assert.deepEqual(judgePiece(format, null, undefined), { whole: false, throughSeconds: null });
42
47
  });
48
+
49
+ test("captured whole AAC pieces survive movie-time truncation and decimal cut subtraction", () => {
50
+ for (const [name, next] of [["making-0-00013.mp4", 81.415], ["making-0-00090.mp4", 618.076]]) {
51
+ const cut = cutOf({ segmentCount: 2, publishedStartOf: () => next }, 0);
52
+ const ranges = captured(name);
53
+ assert.equal(ranges.tracks[0].positionErrorTicks, 48n);
54
+ assert.equal(judgePiece(fmp4Format, ranges, cut).whole, true, name);
55
+ ranges.tracks[0].ranges.at(-1).end -= 1024n;
56
+ assert.equal(judgePiece(fmp4Format, ranges, cut).whole, false, "one lost AAC frame remains a refusal");
57
+ }
58
+ });
59
+
60
+ test("a restarted AAC cut follows its first packet rather than the requested seek", () => {
61
+ const ranges = captured("repair-13.mp4");
62
+ assert.equal(originOf(ranges, 81_364_000n, "soun"), 71_550_667n);
63
+ // The captured first packet is 71.5506667 s, 21.333 ms before -ss 71.572.
64
+ // segment.c adds it to the relative cut (81.415 - 71.572 = 9.843).
65
+ ranges.production = productionOf({ originMicros: 71_550_667n, endMicros: 81_364_000n }, [9.843], 0, "soun");
66
+ assert.equal(ranges.production.cutMicros, 81_343_667n);
67
+ assert.equal(judgePiece(fmp4Format, ranges, 81.365).whole, true);
68
+ ranges.production.endMicros -= 1024n * 1_000_000n / 48_000n;
69
+ assert.equal(judgePiece(fmp4Format, ranges, 81.365).whole, false);
70
+ });
71
+
72
+ test("a muxer's reference clock cannot conceal a shorter multiplexed track", () => {
73
+ const ranges = captured("making-0-00013.mp4");
74
+ ranges.production = productionOf({ originMicros: 0n, endMicros: 81_365_333n }, [81.415], 0, "soun");
75
+ const audio = ranges.tracks[0];
76
+ ranges.tracks.push({ ...audio, kind: "vide", ranges: audio.ranges.map(range => ({ ...range, end: range.end - 4800n })) });
77
+ assert.equal(judgePiece(fmp4Format, ranges, 81.365).whole, false);
78
+ });