@torrent-tv/proxy 2.75.0 → 2.76.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.
- package/CHANGELOG.md +1509 -1461
- package/CLAUDE.md +11 -1
- package/biome.json +182 -1
- package/docs/container-architecture.md +2 -1
- package/docs/encode-run-state.md +1 -1
- package/knip.json +14 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +1 -1
- package/routes/api/transcode-sessions/post.js +185 -185
- package/routes/api/transcode-sessions/progress/get.js +5 -1
- package/routes/transcode/audio-file/get.js +11 -1
- package/routes/transcode/audio-warm/get.js +11 -1
- package/routes/transcode/session-file/get.js +1 -1
- package/routes/transcode/variant-file/get.js +10 -1
- package/scripts/render-run-graph.js +2 -2
- package/server.js +25 -0
- package/services/audio-inventory.js +206 -201
- package/services/container/AviContainer.js +1 -1
- package/services/container/Container.js +33 -1
- package/services/container/MatroskaContainer.js +1 -1
- package/services/container/Mp4Container.js +1 -1
- package/services/container/SubtitleFileContainer.js +0 -1
- package/services/controllers/SubtitleController.js +128 -128
- package/services/demand/index.js +7 -10
- package/services/download/registry.js +0 -14
- package/services/encode/CoverageMap.js +281 -0
- package/services/encode/EncodePlan.js +255 -0
- package/services/encode/EncodeRun.js +587 -0
- package/services/encode/Encoder.js +84 -0
- package/services/encode/NvencEncoder.js +45 -0
- package/services/encode/QsvEncoder.js +47 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +529 -0
- package/services/encode/SoftwareEncoder.js +111 -0
- package/services/encode/V4l2m2mEncoder.js +53 -0
- package/services/encode/VaapiEncoder.js +53 -0
- package/services/encode/args.js +200 -0
- package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
- package/services/encode/index.js +9 -0
- package/services/encode/run-command.js +647 -0
- package/services/hls-session-manager.js +11100 -10711
- package/services/hwaccel.js +1688 -1992
- package/services/orchestrators/EncodeOrchestrator.js +359 -0
- package/services/output/LiveOutputs.js +213 -0
- package/services/output/Output.js +94 -0
- package/services/output/OutputSpec.js +195 -0
- package/services/output/Timeline.js +220 -0
- package/services/output/index.js +1 -0
- package/services/output/ladder.js +26 -0
- package/services/playback-planner.js +806 -775
- package/services/produced-index.js +222 -300
- package/services/source/SourceFile.js +346 -0
- package/services/{sidecar-files.js → torrent/files.js} +107 -11
- package/services/torrent/naming.js +619 -0
- package/services/torrent-worker/client.js +10 -0
- package/services/torrent-worker/container-tracks.js +71 -43
- package/services/torrent-worker/pool-adapter.js +18 -0
- package/services/torrent-worker/protocol.js +7 -0
- package/services/torrent-worker/subtitle-cues.js +549 -549
- package/services/torrent-worker/worker.js +18 -0
- package/services/tracks/TextSubtitleTrack.js +287 -287
- package/services/tracks/index.js +15 -14
- package/services/viewer/Viewer.js +145 -0
- package/services/viewer/Viewers.js +124 -0
- package/test/auto-quality-step.test.js +508 -506
- package/test/behind-head-repair.test.js +17 -7
- package/test/coverage-map.test.js +153 -0
- package/test/cut-times-timeline.test.js +6 -5
- package/test/cuts-follow-published-grid.test.js +4 -4
- package/test/decode-cost.test.js +31 -12
- package/test/encode-exit.test.js +1 -1
- package/test/encode-orchestrator.test.js +196 -0
- package/test/encode-plan.test.js +245 -0
- package/test/encode-run-state.test.js +2 -2
- package/test/encode-run.test.js +168 -0
- package/test/encoder-kinds.test.js +122 -0
- package/test/held-request-width.test.js +9 -3
- package/test/helpers/encode-run.js +128 -0
- package/test/keyframe-index-accuracy.test.js +19 -12
- package/test/keyframes-belong-to-the-file.test.js +132 -0
- package/test/orchestrator-wired.test.js +164 -0
- package/test/output-shape.test.js +68 -0
- package/test/output-spec.test.js +157 -0
- package/test/produced-copy-choice.test.js +58 -92
- package/test/produced-index.test.js +142 -188
- package/test/quality-variants.test.js +1079 -1075
- package/test/run-graph-drift.test.js +1 -1
- package/test/run-intervals.test.js +329 -0
- package/test/run-position-follows-published-grid.test.js +4 -4
- package/test/seek-landing.test.js +8 -8
- package/test/seek-target-not-superseded.test.js +21 -9
- package/test/segment-demand.test.js +82 -0
- package/test/segment-serve-wiring.test.js +47 -52
- package/test/segment-store.test.js +187 -0
- package/test/segments-are-shared.test.js +175 -0
- package/test/sidecar-naming.test.js +142 -0
- package/test/source-file.test.js +133 -0
- package/test/stale-request-after-seek.test.js +18 -12
- package/test/subtitle-language.test.js +252 -252
- package/test/timeline.test.js +95 -0
- package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
- package/test/torrent-naming.test.js +255 -0
- package/test/tracks-begin-together.test.js +44 -32
- package/test/two-viewers-one-picture.test.js +347 -0
- package/test/viewer-outputs.test.js +273 -0
- package/test/viewer.test.js +91 -0
- package/utils/perf.js +1 -63
- package/services/container/index.js +0 -6
- package/services/controllers/index.js +0 -2
- package/services/download/index.js +0 -8
- package/services/orchestrators/index.js +0 -2
- /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
- /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
|
|
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.
|
|
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
|
|
16
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
await writeFile(path.join(dirPath, "
|
|
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(
|
|
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
|
|
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
|
-
|
|
259
|
-
|
|
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(
|
|
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
|
-
|
|
274
|
-
|
|
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([
|
|
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
|
|
287
|
-
await writeFile(path.join(
|
|
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([
|
|
257
|
+
[...usableSegmentIndices([dirPath], fmp4Format, new Set())].sort((a, b) => a - b),
|
|
290
258
|
[0, 1],
|
|
291
|
-
"
|
|
259
|
+
"a copy with bytes in it, which is what the serving path will find"
|
|
292
260
|
);
|
|
293
261
|
});
|
|
294
262
|
|
|
295
|
-
test("what
|
|
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
|
-
|
|
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([
|
|
271
|
+
usableSegmentIndices([dirPath], fmp4Format, known);
|
|
306
272
|
|
|
307
273
|
assert.deepEqual(
|
|
308
274
|
[...known],
|
|
309
|
-
[path.join(
|
|
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"
|