@torrent-tv/proxy 2.75.0 → 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 (113) hide show
  1. package/CHANGELOG.md +1504 -1461
  2. package/CLAUDE.md +11 -1
  3. package/biome.json +182 -1
  4. package/docs/container-architecture.md +2 -1
  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 +206 -201
  18. package/services/container/AviContainer.js +1 -1
  19. package/services/container/Container.js +33 -1
  20. package/services/container/MatroskaContainer.js +1 -1
  21. package/services/container/Mp4Container.js +1 -1
  22. package/services/container/SubtitleFileContainer.js +0 -1
  23. package/services/controllers/SubtitleController.js +128 -128
  24. package/services/demand/index.js +7 -10
  25. package/services/download/registry.js +0 -14
  26. package/services/encode/CoverageMap.js +281 -0
  27. package/services/encode/EncodePlan.js +255 -0
  28. package/services/encode/EncodeRun.js +587 -0
  29. package/services/encode/Encoder.js +84 -0
  30. package/services/encode/NvencEncoder.js +45 -0
  31. package/services/encode/QsvEncoder.js +47 -0
  32. package/services/encode/SegmentDemand.js +0 -0
  33. package/services/encode/SegmentStore.js +529 -0
  34. package/services/encode/SoftwareEncoder.js +111 -0
  35. package/services/encode/V4l2m2mEncoder.js +53 -0
  36. package/services/encode/VaapiEncoder.js +53 -0
  37. package/services/encode/args.js +200 -0
  38. package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
  39. package/services/encode/index.js +9 -0
  40. package/services/encode/run-command.js +647 -0
  41. package/services/hls-session-manager.js +11073 -10711
  42. package/services/hwaccel.js +1688 -1992
  43. package/services/orchestrators/EncodeOrchestrator.js +359 -0
  44. package/services/output/LiveOutputs.js +213 -0
  45. package/services/output/Output.js +94 -0
  46. package/services/output/OutputSpec.js +195 -0
  47. package/services/output/Timeline.js +220 -0
  48. package/services/output/index.js +1 -0
  49. package/services/output/ladder.js +26 -0
  50. package/services/playback-planner.js +806 -775
  51. package/services/produced-index.js +222 -300
  52. package/services/source/SourceFile.js +346 -0
  53. package/services/{sidecar-files.js → torrent/files.js} +107 -11
  54. package/services/torrent/naming.js +619 -0
  55. package/services/torrent-worker/client.js +10 -0
  56. package/services/torrent-worker/container-tracks.js +71 -43
  57. package/services/torrent-worker/pool-adapter.js +18 -0
  58. package/services/torrent-worker/protocol.js +7 -0
  59. package/services/torrent-worker/subtitle-cues.js +549 -549
  60. package/services/torrent-worker/worker.js +18 -0
  61. package/services/tracks/TextSubtitleTrack.js +287 -287
  62. package/services/tracks/index.js +15 -14
  63. package/services/viewer/Viewer.js +145 -0
  64. package/services/viewer/Viewers.js +124 -0
  65. package/test/auto-quality-step.test.js +508 -506
  66. package/test/behind-head-repair.test.js +17 -7
  67. package/test/coverage-map.test.js +153 -0
  68. package/test/cut-times-timeline.test.js +6 -5
  69. package/test/cuts-follow-published-grid.test.js +4 -4
  70. package/test/decode-cost.test.js +31 -12
  71. package/test/encode-exit.test.js +1 -1
  72. package/test/encode-orchestrator.test.js +196 -0
  73. package/test/encode-plan.test.js +245 -0
  74. package/test/encode-run-state.test.js +2 -2
  75. package/test/encode-run.test.js +168 -0
  76. package/test/encoder-kinds.test.js +122 -0
  77. package/test/held-request-width.test.js +9 -3
  78. package/test/helpers/encode-run.js +128 -0
  79. package/test/keyframe-index-accuracy.test.js +19 -12
  80. package/test/keyframes-belong-to-the-file.test.js +132 -0
  81. package/test/orchestrator-wired.test.js +164 -0
  82. package/test/output-shape.test.js +68 -0
  83. package/test/output-spec.test.js +157 -0
  84. package/test/produced-copy-choice.test.js +58 -92
  85. package/test/produced-index.test.js +142 -188
  86. package/test/quality-variants.test.js +1079 -1075
  87. package/test/run-graph-drift.test.js +1 -1
  88. package/test/run-intervals.test.js +329 -0
  89. package/test/run-position-follows-published-grid.test.js +4 -4
  90. package/test/seek-landing.test.js +8 -8
  91. package/test/seek-target-not-superseded.test.js +21 -9
  92. package/test/segment-demand.test.js +82 -0
  93. package/test/segment-serve-wiring.test.js +47 -52
  94. package/test/segment-store.test.js +187 -0
  95. package/test/segments-are-shared.test.js +175 -0
  96. package/test/sidecar-naming.test.js +142 -0
  97. package/test/source-file.test.js +133 -0
  98. package/test/stale-request-after-seek.test.js +18 -12
  99. package/test/subtitle-language.test.js +252 -252
  100. package/test/timeline.test.js +95 -0
  101. package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
  102. package/test/torrent-naming.test.js +255 -0
  103. package/test/tracks-begin-together.test.js +44 -32
  104. package/test/two-viewers-one-picture.test.js +347 -0
  105. package/test/viewer-outputs.test.js +273 -0
  106. package/test/viewer.test.js +91 -0
  107. package/utils/perf.js +1 -63
  108. package/services/container/index.js +0 -6
  109. package/services/controllers/index.js +0 -2
  110. package/services/download/index.js +0 -8
  111. package/services/orchestrators/index.js +0 -2
  112. /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
  113. /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
@@ -0,0 +1,157 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/output/OutputSpec.js";
4
+
5
+ const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
6
+
7
+ /**
8
+ * The picture of a file, copied, cut at that file's own keyframes.
9
+ *
10
+ * @param {object} [over]
11
+ * @returns {OutputSpec}
12
+ */
13
+ function copiedPicture(over = {}) {
14
+ return new OutputSpec({
15
+ sourceKey: TORRENT,
16
+ segmentFormatId: "fmp4",
17
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
18
+ video: new VideoOutput({ fileIndex: 0, encode: null }),
19
+ audio: null,
20
+ ...over
21
+ });
22
+ }
23
+
24
+ test("two viewers who chose different soundtracks share one copied picture", () => {
25
+ // The measurement of 2026-09-03: both sessions described the same thing word
26
+ // for word and answered `segment-00000.mp4` with the same 4141899 bytes. The
27
+ // soundtrack they picked is not a property of a picture that carries no
28
+ // sound, so it cannot tell the two apart.
29
+ assert.equal(copiedPicture().toKey(), copiedPicture().toKey());
30
+ assert.equal(copiedPicture().carries, "video-only");
31
+ });
32
+
33
+ test("the viewer's viewport does not fork a picture that is copied", () => {
34
+ // A copy is the source's own size whatever box was asked for, so there is
35
+ // nowhere for a target to appear in the key.
36
+ const key = copiedPicture().toKey();
37
+ assert.ok(!key.includes("x"), `a copied picture states no box: ${key}`);
38
+ });
39
+
40
+ test("two heights of one picture are two outputs", () => {
41
+ const at720 = copiedPicture({
42
+ video: new VideoOutput({ fileIndex: 0, encode: { width: 1280, height: 720, manual: true } })
43
+ });
44
+ const at480 = copiedPicture({
45
+ video: new VideoOutput({ fileIndex: 0, encode: { width: 854, height: 480, manual: true } })
46
+ });
47
+ assert.notEqual(at720.toKey(), at480.toKey());
48
+ });
49
+
50
+ test("a height the viewer forced is not the same output as one the budget may move", () => {
51
+ const forced = copiedPicture({
52
+ video: new VideoOutput({ fileIndex: 0, encode: { width: 1280, height: 720, manual: true } })
53
+ });
54
+ const chosen = copiedPicture({
55
+ video: new VideoOutput({ fileIndex: 0, encode: { width: 1280, height: 720, manual: false } })
56
+ });
57
+ assert.notEqual(forced.toKey(), chosen.toKey());
58
+ });
59
+
60
+ test("a soundtrack is named by the file it lives in and the track inside it", () => {
61
+ // Not by the flat number the browser sends: that number spans the picture's
62
+ // own tracks and the files beside it, so it means different things for
63
+ // different pictures of one torrent.
64
+ const dub = new OutputSpec({
65
+ sourceKey: TORRENT,
66
+ segmentFormatId: "fmp4",
67
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
68
+ audio: new AudioOutput({ fileIndex: 7, trackIndex: 0, transcode: true })
69
+ });
70
+ assert.equal(dub.carries, "audio-only");
71
+ assert.ok(dub.toKey().includes("a=7/0/aac"), dub.toKey());
72
+ });
73
+
74
+ test("one soundtrack cut for two different pictures is two outputs", () => {
75
+ // The grid of a soundtrack is the picture's, so a rendition made for episode
76
+ // one cannot stand in for one made for episode two even when the dub is the
77
+ // same file.
78
+ const forFirst = new OutputSpec({
79
+ sourceKey: TORRENT,
80
+ segmentFormatId: "fmp4",
81
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
82
+ audio: new AudioOutput({ fileIndex: 7, trackIndex: 0, transcode: true })
83
+ });
84
+ const forSecond = new OutputSpec({
85
+ sourceKey: TORRENT,
86
+ segmentFormatId: "fmp4",
87
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 1 }),
88
+ audio: new AudioOutput({ fileIndex: 7, trackIndex: 0, transcode: true })
89
+ });
90
+ assert.notEqual(forFirst.toKey(), forSecond.toKey());
91
+ });
92
+
93
+ test("a copied soundtrack and a re-encoded one are two outputs", () => {
94
+ const copied = new OutputSpec({
95
+ sourceKey: TORRENT,
96
+ segmentFormatId: "fmp4",
97
+ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
98
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 1, transcode: false })
99
+ });
100
+ const encoded = new OutputSpec({
101
+ sourceKey: TORRENT,
102
+ segmentFormatId: "fmp4",
103
+ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
104
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 1, transcode: true })
105
+ });
106
+ assert.notEqual(copied.toKey(), encoded.toKey());
107
+ });
108
+
109
+ test("a browser without rendition groups gets an output carrying both tracks", () => {
110
+ const muxed = new OutputSpec({
111
+ sourceKey: TORRENT,
112
+ segmentFormatId: "mpegts",
113
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
114
+ video: new VideoOutput({ fileIndex: 0, encode: null }),
115
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 2, transcode: true })
116
+ });
117
+ assert.equal(muxed.carries, "muxed");
118
+ // And there the soundtrack DOES tell two of them apart, because the output
119
+ // really carries it.
120
+ const other = new OutputSpec({
121
+ sourceKey: TORRENT,
122
+ segmentFormatId: "mpegts",
123
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
124
+ video: new VideoOutput({ fileIndex: 0, encode: null }),
125
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 3, transcode: true })
126
+ });
127
+ assert.notEqual(muxed.toKey(), other.toKey());
128
+ });
129
+
130
+ test("the picture, its soundtrack and the two of them muxed are three outputs", () => {
131
+ const video = copiedPicture();
132
+ const audio = new OutputSpec({
133
+ sourceKey: TORRENT,
134
+ segmentFormatId: "fmp4",
135
+ grid: new CutGrid({ kind: "keyframe", fileIndex: 0 }),
136
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: true })
137
+ });
138
+ const muxed = copiedPicture({
139
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: true })
140
+ });
141
+ assert.equal(new Set([video.toKey(), audio.toKey(), muxed.toKey()]).size, 3);
142
+ });
143
+
144
+ test("two containers of the same tracks are two outputs", () => {
145
+ assert.notEqual(copiedPicture().toKey(), copiedPicture({ segmentFormatId: "mpegts" }).toKey());
146
+ });
147
+
148
+ test("the same tracks of two different torrents are two outputs", () => {
149
+ assert.notEqual(copiedPicture().toKey(), copiedPicture({ sourceKey: "torrent:0000" }).toKey());
150
+ });
151
+
152
+ test("a picture cut at keyframes and the same picture cut on the even grid are two outputs", () => {
153
+ assert.notEqual(
154
+ copiedPicture().toKey(),
155
+ copiedPicture({ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }) }).toKey()
156
+ );
157
+ });
@@ -1,6 +1,5 @@
1
1
  /**
2
- * @file When several runs have written the same segment number, which copy
3
- * answers — and what happens to the one that cannot.
2
+ * @file A piece that cannot be played: waited for, or removed and made again.
4
3
  *
5
4
  * Field 2026-09-03. A run was suspended 548 ms after it started with
6
5
  * `segment-00025.mp4` newly opened, so the file stayed at zero bytes. From then
@@ -8,17 +7,23 @@
8
7
  * other's reason: the look-ahead counted the NAME, found the numbering unbroken
9
8
  * through #83, called it `420s ahead of the viewer` and kept the encoder
10
9
  * stopped; the serving path read the FILE, found it short of a track, and
11
- * waited for a run that had produced nothing to finish it. A complete copy of
12
- * #25 lay in the previous run's directory the whole time and was never reached,
13
- * because the search returned the first name it found and stopped.
10
+ * waited for a run that had produced nothing to finish it.
14
11
  *
15
- * The viewer's picture stood still for ten minutes with the bytes they needed
16
- * already on the disk, on a transport measuring 3-9 ms round trip.
12
+ * The viewer's picture stood still for ten minutes on a transport measuring
13
+ * 3-9 ms round trip.
14
+ *
15
+ * The question of WHICH copy answers is gone with the directory-per-run scheme:
16
+ * there is one directory per output, runs are kept apart by their stretches,
17
+ * and one name is one file. What is left is the question that remains real —
18
+ * whether a file that cannot be played is being written now or was left by a
19
+ * run that has ended.
17
20
  */
18
21
 
19
22
  import test from "node:test";
20
23
  import assert from "node:assert/strict";
21
- import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
24
+ import { SourceFile } from "../services/source/SourceFile.js";
25
+ import { Timeline } from "../services/output/Timeline.js";
26
+ import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
22
27
  import os from "node:os";
23
28
  import path from "node:path";
24
29
  import {
@@ -26,6 +31,7 @@ import {
26
31
  discardOpenPiece,
27
32
  usableSegmentIndices
28
33
  } from "../services/hls-session-manager.js";
34
+ import { startRunOn } from "./helpers/encode-run.js";
29
35
  import { fmp4Format } from "../services/segment-formats/fmp4.js";
30
36
 
31
37
  const MOVIE_TIMESCALE = 1000;
@@ -137,14 +143,12 @@ function halfPiece(offsetSeconds) {
137
143
  }
138
144
 
139
145
  /**
140
- * A session whose runs are laid out on disk exactly as the field case was.
146
+ * A session with the output directory every run writes into.
141
147
  *
142
148
  * @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string }>}
143
149
  */
144
- async function sessionWithTwoRuns() {
150
+ async function sessionOnOneDirectory() {
145
151
  const dirPath = await mkdtemp(path.join(os.tmpdir(), "produced-copy-"));
146
- await mkdir(path.join(dirPath, "run-1"), { recursive: true });
147
- await mkdir(path.join(dirPath, "run-2"), { recursive: true });
148
152
  const manager = new HlsSessionManager({
149
153
  enabled: true,
150
154
  ffmpegBin: "ffmpeg",
@@ -154,90 +158,56 @@ async function sessionWithTwoRuns() {
154
158
  const session = {
155
159
  id: SESSION_ID,
156
160
  dirPath,
157
- runDirPath: path.join(dirPath, "run-2"),
158
- runSerial: 2,
161
+ // Where this file is cut, held by the file. A fixture that stated it
162
+ // on the session was describing what production no longer does.
163
+ timeline: new Timeline({
164
+ boundaries: [0, SEGMENT_SECONDS, 25, 37.5, 50],
165
+ cutGrid: "uniform"
166
+ }),
159
167
  state: "ready",
160
- fileName: "Drifters - 04.mkv",
168
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "Drifters - 04.mkv" }),
169
+ // An ordinary session reads its own file, and its sound is inside it. The
170
+ // three differ only for a soundtrack shipped as a file of its own.
171
+ get inputFile() { return this.file; },
172
+ get audioFile() { return this.file; },
161
173
  startedAt: Date.now(),
162
174
  createEntryMs: Date.now(),
163
175
  lastAccessedAt: Date.now(),
164
- // A run exists and is alive — the state the field case was in, and the one
165
- // in which the old test called every leftover "still being written".
166
- ffmpeg: { pid: 0, killed: false, kill() {} },
176
+ runs: new Set(),
167
177
  lastError: "",
168
178
  consumers: new Set(),
169
- netReports: new Map(),
179
+ viewers: new Map(),
170
180
  segmentFormat: fmp4Format,
171
- usesExplicitCuts: true,
172
181
  useSyntheticPlaylist: true,
173
182
  playlistText: "#EXTM3U\n",
174
- segmentBoundaries: [0, SEGMENT_SECONDS, 25, 37.5, 50],
175
183
  initBytes: fmp4Format.extractInit(wholePiece(0)),
176
- encodeStartIndex: 0,
177
184
  firstSegmentLogged: false,
178
185
  waitEpoch: 0
179
186
  };
180
187
  manager.sessionsById.set(SESSION_ID, session);
188
+ // A run exists and is alive — the state the field case was in, and the one in
189
+ // which the old test called every leftover "still being written".
190
+ startRunOn(session, { from: 0, usesExplicitCuts: true });
181
191
  return { manager, session, dirPath };
182
192
  }
183
193
 
184
- test("the complete copy answers when the newest run's is empty", async (t) => {
185
- const { manager, dirPath } = await sessionWithTwoRuns();
186
- t.after(async () => {
187
- await manager.disposeAll();
188
- await rm(dirPath, { recursive: true, force: true });
189
- });
190
- // run-1 made this segment and finished it; run-2 opened it and was stopped.
191
- await writeFile(path.join(dirPath, "run-1", "segment-00001.mp4"), wholePiece(SEGMENT_SECONDS));
192
- await writeFile(path.join(dirPath, "run-1", "segment-00002.mp4"), wholePiece(25));
193
- await writeFile(path.join(dirPath, "run-2", "segment-00001.mp4"), Buffer.alloc(0));
194
194
 
195
- const result = await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
196
-
197
- assert.equal(
198
- result.kind,
199
- "file",
200
- "a complete copy exists one run away; holding here is what froze the picture for ten minutes"
201
- );
202
- const chunks = [];
203
- for await (const chunk of result.stream) {
204
- chunks.push(chunk);
205
- }
206
- assert.ok(Buffer.concat(chunks).length > 0, "and it must be the copy with bytes in it");
207
- });
208
-
209
- test("a copy short of a track is passed over for an older one that is whole", async (t) => {
210
- const { manager, dirPath } = await sessionWithTwoRuns();
211
- t.after(async () => {
212
- await manager.disposeAll();
213
- await rm(dirPath, { recursive: true, force: true });
214
- });
215
- // Not empty — a terminated run closes its file properly, with only what it
216
- // had muxed by then, which after a seek-restart is routinely one track of two.
217
- await writeFile(path.join(dirPath, "run-1", "segment-00001.mp4"), wholePiece(SEGMENT_SECONDS));
218
- await writeFile(path.join(dirPath, "run-1", "segment-00002.mp4"), wholePiece(25));
219
- await writeFile(path.join(dirPath, "run-2", "segment-00001.mp4"), halfPiece(SEGMENT_SECONDS));
220
-
221
- const result = await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
222
-
223
- assert.equal(result.kind, "file", "a piece carrying one track of two is not servable; the whole one is");
224
- });
225
195
 
226
196
  test("a leftover of a run that has ended is removed, not waited on", async (t) => {
227
- const { manager, dirPath } = await sessionWithTwoRuns();
197
+ const { manager, session, dirPath } = await sessionOnOneDirectory();
228
198
  t.after(async () => {
229
199
  await manager.disposeAll();
230
200
  await rm(dirPath, { recursive: true, force: true });
231
201
  });
232
- // The only copy, and it belongs to a run that is gone. The current run
233
- // run-2 has written nothing, so calling this "still being written" waits on
234
- // a process that will never touch it.
235
- await writeFile(path.join(dirPath, "run-1", "segment-00001.mp4"), Buffer.alloc(0));
202
+ // Opened by a run that is gone, and nothing is encoding now so calling it
203
+ // "still being written" waits on a process that will never touch it.
204
+ session.runs = new Set();
205
+ await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
236
206
 
237
207
  const result = await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
238
208
 
239
209
  assert.equal(result.kind, "warming-up", "nothing servable exists yet, so the viewer waits");
240
- const left = await readdir(path.join(dirPath, "run-1"));
210
+ const left = (await readdir(dirPath)).filter((name) => name === "segment-00001.mp4");
241
211
  assert.deepEqual(
242
212
  left,
243
213
  [],
@@ -246,7 +216,7 @@ test("a leftover of a run that has ended is removed, not waited on", async (t) =
246
216
  });
247
217
 
248
218
  test("the current run's own unfinished piece is waited for, never deleted", async (t) => {
249
- const { manager, session, dirPath } = await sessionWithTwoRuns();
219
+ const { manager, session, dirPath } = await sessionOnOneDirectory();
250
220
  t.after(async () => {
251
221
  await manager.disposeAll();
252
222
  await rm(dirPath, { recursive: true, force: true });
@@ -255,13 +225,15 @@ test("the current run's own unfinished piece is waited for, never deleted", asyn
255
225
  // written right now. Deleting it is the 2026-08-06 incident: #225 was removed
256
226
  // 14 s into the run producing it, which then wrote on into a file nobody
257
227
  // could open, and the segment never appeared.
258
- session.encodeStartIndex = 1;
259
- await writeFile(path.join(dirPath, "run-2", "segment-00001.mp4"), Buffer.alloc(0));
228
+ //
229
+ // The live run begins at #1, so #1 is its own first piece.
230
+ startRunOn(session, { from: 1, producing: false, usesExplicitCuts: true });
231
+ await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
260
232
 
261
233
  const result = await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
262
234
 
263
235
  assert.equal(result.kind, "warming-up");
264
- const left = await readdir(path.join(dirPath, "run-2"));
236
+ const left = (await readdir(dirPath)).filter((name) => name === "segment-00001.mp4");
265
237
  assert.deepEqual(left, ["segment-00001.mp4"], "the live run's own output must be left alone");
266
238
  });
267
239
 
@@ -270,43 +242,37 @@ test("the look-ahead does not count a file with nothing in it", async (t) => {
270
242
  t.after(async () => {
271
243
  await rm(dirPath, { recursive: true, force: true });
272
244
  });
273
- const runTwo = path.join(dirPath, "run-2");
274
- const runOne = path.join(dirPath, "run-1");
275
- await mkdir(runTwo, { recursive: true });
276
- await mkdir(runOne, { recursive: true });
277
- await writeFile(path.join(runTwo, "segment-00000.mp4"), wholePiece(0));
278
- await writeFile(path.join(runTwo, "segment-00001.mp4"), Buffer.alloc(0));
245
+ await writeFile(path.join(dirPath, "segment-00000.mp4"), wholePiece(0));
246
+ await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
279
247
 
280
248
  assert.deepEqual(
281
- [...usableSegmentIndices([runTwo, runOne], fmp4Format, new Set())].sort((a, b) => a - b),
249
+ [...usableSegmentIndices([dirPath], fmp4Format, new Set())].sort((a, b) => a - b),
282
250
  [0],
283
251
  "an empty file bridged the hole and bought the encoder a suspension it had not earned"
284
252
  );
285
253
 
286
- // The same number, made properly by an earlier run: now it genuinely is ready.
287
- await writeFile(path.join(runOne, "segment-00001.mp4"), wholePiece(SEGMENT_SECONDS));
254
+ // The same number, made properly: now it genuinely is ready.
255
+ await writeFile(path.join(dirPath, "segment-00001.mp4"), wholePiece(SEGMENT_SECONDS));
288
256
  assert.deepEqual(
289
- [...usableSegmentIndices([runTwo, runOne], fmp4Format, new Set())].sort((a, b) => a - b),
257
+ [...usableSegmentIndices([dirPath], fmp4Format, new Set())].sort((a, b) => a - b),
290
258
  [0, 1],
291
- "some run holds a copy with bytes in it, which is what the serving path will find"
259
+ "a copy with bytes in it, which is what the serving path will find"
292
260
  );
293
261
  });
294
262
 
295
- test("what a run holds is asked of the filesystem once per file", async (t) => {
263
+ test("what the output holds is asked of the filesystem once per file", async (t) => {
296
264
  const dirPath = await mkdtemp(path.join(os.tmpdir(), "usable-memo-"));
297
265
  t.after(async () => {
298
266
  await rm(dirPath, { recursive: true, force: true });
299
267
  });
300
- const runOne = path.join(dirPath, "run-1");
301
- await mkdir(runOne, { recursive: true });
302
- await writeFile(path.join(runOne, "segment-00000.mp4"), wholePiece(0));
268
+ await writeFile(path.join(dirPath, "segment-00000.mp4"), wholePiece(0));
303
269
  const known = new Set();
304
270
 
305
- usableSegmentIndices([runOne], fmp4Format, known);
271
+ usableSegmentIndices([dirPath], fmp4Format, known);
306
272
 
307
273
  assert.deepEqual(
308
274
  [...known],
309
- [path.join(runOne, "segment-00000.mp4")],
275
+ [path.join(dirPath, "segment-00000.mp4")],
310
276
  "a piece that has bytes never loses them, and this runs on the thread carrying the data channel"
311
277
  );
312
278
  });
@@ -319,7 +285,7 @@ test("a run killed with a piece open leaves nothing behind", async (t) => {
319
285
  await writeFile(path.join(dirPath, "segment-00000.mp4"), wholePiece(0));
320
286
  await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
321
287
 
322
- assert.equal(await discardOpenPiece(dirPath, fmp4Format, null), 1);
288
+ assert.equal(await discardOpenPiece(dirPath, fmp4Format, null, null), 1);
323
289
  assert.deepEqual(
324
290
  await readdir(dirPath),
325
291
  ["segment-00000.mp4"],
@@ -336,7 +302,7 @@ test("a run that finished its last piece keeps it", async (t) => {
336
302
  await writeFile(path.join(dirPath, "segment-00001.mp4"), wholePiece(SEGMENT_SECONDS));
337
303
 
338
304
  assert.equal(
339
- await discardOpenPiece(dirPath, fmp4Format, () => true),
305
+ await discardOpenPiece(dirPath, fmp4Format, null, () => true),
340
306
  null,
341
307
  "a stop between two cuts leaves good output; deleting it means encoding it twice"
342
308
  );
@@ -353,7 +319,7 @@ test("a last piece short of a track goes, even though it has bytes", async (t) =
353
319
  const init = fmp4Format.extractInit(wholePiece(0));
354
320
 
355
321
  assert.equal(
356
- await discardOpenPiece(dirPath, fmp4Format, (raw) =>
322
+ await discardOpenPiece(dirPath, fmp4Format, null, (raw) =>
357
323
  fmp4Format.hasEveryTrack(fmp4Format.stripInit(raw), init)),
358
324
  1,
359
325
  "a size above zero is not the same as a piece that can be played"