@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.
- package/AGENTS.md +32 -12
- package/CHANGELOG.md +2340 -2330
- package/assets/calibration/audio/NOTICE.md +19 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/biome.json +412 -412
- package/docs/browser-proxy-contract.md +155 -155
- package/docs/container-architecture.md +273 -273
- package/docs/encode-run-state.md +1 -1
- package/docs/playback-start-forecast.md +113 -98
- package/package.json +51 -50
- package/routes/api/sources/files/get.js +106 -106
- package/scripts/build-audio-samples.mjs +31 -31
- package/services/encode/EncodeRun.js +67 -15
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/OutputOpening.js +784 -784
- package/services/encode/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- package/services/encode/encode-exit.js +2 -0
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/output/index.js +24 -24
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/run-command.js +757 -755
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/media/ContainerOrchestrator.js +202 -202
- package/services/media/KeyframeTables.js +356 -356
- package/services/media/SubtitleCues.js +592 -592
- package/services/media/audio-inventory.js +299 -299
- package/services/media/container/AviContainer.js +268 -268
- package/services/media/container/Container.js +632 -632
- package/services/media/container/ContainerFactory.js +38 -38
- package/services/media/container/MatroskaContainer.js +1031 -1031
- package/services/media/container/Mp4Container.js +1354 -1354
- package/services/media/container/SubtitleFileContainer.js +339 -339
- package/services/media/container/ebml-stream.js +174 -174
- package/services/media/container/matroska-clusters.js +1165 -1165
- package/services/media/container/unavailable.js +77 -77
- package/services/media/tracks/ContainerTrack.js +84 -84
- package/services/media/tracks/TextSubtitleTrack.js +295 -295
- package/services/storage/piece-store/shared-piece-store.js +1937 -1937
- package/services/storage/segment-store/SegmentStore.js +1294 -1294
- package/services/torrent/Contents.js +382 -382
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/torrent-pool.js +2688 -2688
- package/services/torrent/worker/client.js +836 -836
- package/services/torrent/worker/held-bytes.js +231 -231
- package/services/torrent/worker/worker.js +1137 -1137
- package/services/transport/data-channel-handler.js +1373 -1373
- package/services/viewer/PriorityMap.js +421 -421
- package/test/audio-calibration.test.js +23 -23
- package/test/auto-quality-step.test.js +762 -762
- package/test/coalesce.test.js +39 -39
- package/test/coverage-follows-the-disk.test.js +5 -0
- package/test/decode-cost.test.js +475 -475
- package/test/encode-run.test.js +58 -0
- package/test/episode-naming.test.js +131 -131
- package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
- package/test/fixtures/audio-frame-boundary.json +1 -1
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +11 -13
- package/test/held-read-ownership.test.js +77 -77
- package/test/helpers/encode-run.js +14 -6
- package/test/helpers/held-file.js +77 -77
- package/test/helpers/manager.js +62 -62
- package/test/helpers/matroska-file.js +296 -296
- package/test/keyframe-table-budget.test.js +123 -123
- package/test/matroska-cluster-discovery.test.js +323 -323
- package/test/one-output-one-session.test.js +273 -273
- package/test/output-speed.test.js +6 -1
- package/test/piece-completeness.test.js +37 -1
- package/test/quality-variants.test.js +1274 -1270
- package/test/rate-control-in-the-output.test.js +4 -3
- package/test/reads-not-yet-arrived.test.js +218 -218
- package/test/seek-frees-the-viewers-requests.test.js +1 -1
- package/test/segment-closed-on-both-branches.test.js +114 -114
- package/test/segment-judged-by-its-own-header.test.js +222 -222
- package/test/segment-serve-wiring.test.js +481 -481
- package/test/serving-existing-output.test.js +111 -111
- package/test/source-files-route.test.js +113 -113
- package/test/subtitle-cue-walk.test.js +385 -385
- package/test/subtitle-metadata.test.js +15 -15
- package/test/subtitle-plan-waits.test.js +125 -125
- package/test/subtitle-track-numbering.test.js +391 -391
- package/test/two-viewers-one-picture.test.js +508 -508
- package/test/viewer-requests-admission.test.js +92 -92
- package/test/viewer-starts-on-map.test.js +27 -27
- package/test/viewer-states.test.js +5 -2
- package/test/viewing-generations.test.js +390 -390
- package/test/whole-file-held-read.test.js +30 -30
- package/utils/coalesce.js +41 -41
|
@@ -169,9 +169,10 @@ test("a hardware output is unavailable against a measured link, and given while
|
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
test("the soundtrack the viewer hears counts toward what their link carries", () => {
|
|
172
|
-
// 720p nominal peaks at 3.64 Mbit/s
|
|
173
|
-
//
|
|
174
|
-
|
|
172
|
+
// 720p nominal peaks at 3.64 Mbit/s, and the whole measured link is compared
|
|
173
|
+
// with the stream (no share of it is set aside since proxy 2.89.4). A 3.7 Mbit/s
|
|
174
|
+
// link carries the picture, not the picture and 128 kbit/s of sound (3.768).
|
|
175
|
+
const linkMbps = 3.7;
|
|
175
176
|
assert.equal(decideFor({ mode: "manual", linkMbps }).answer.verdict, "fits");
|
|
176
177
|
assert.ok(
|
|
177
178
|
decideFor({ mode: "manual", linkMbps, audioLoad: { mbps: 0.128, peakClass: "known" } }).unavailable,
|
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A read whose bytes have not arrived is not an answer about the file.
|
|
3
|
-
*
|
|
4
|
-
* Field 2026-10-01: the subtitle plan of an episode was read while its Cues
|
|
5
|
-
* table was still downloading, the read was given up, and the plan kept "no
|
|
6
|
-
* clusters" for the life of the process — the track showed nothing for a whole
|
|
7
|
-
* session while the same table, read eleven seconds later for the keyframes,
|
|
8
|
-
* was there. The same shape was in the container choice, the track table, the
|
|
9
|
-
* keyframe table, MP4's `moov` and AVI's `idx1`. Every check here makes the
|
|
10
|
-
* bytes missing first and present afterwards, and asks the same object again.
|
|
11
|
-
*
|
|
12
|
-
* Fakes only: a buffer and a reader over it. No torrent, no thread.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
|
|
18
|
-
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
19
|
-
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
20
|
-
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
21
|
-
import { ContainerOrchestrator } from "../services/media/ContainerOrchestrator.js";
|
|
22
|
-
import { KeyframeTables } from "../services/media/KeyframeTables.js";
|
|
23
|
-
import { isUnavailable } from "../services/media/container/unavailable.js";
|
|
24
|
-
import {
|
|
25
|
-
ID,
|
|
26
|
-
buildMatroska,
|
|
27
|
-
clusterData,
|
|
28
|
-
cueBlock,
|
|
29
|
-
element,
|
|
30
|
-
readerOver,
|
|
31
|
-
stringElement,
|
|
32
|
-
trackEntry,
|
|
33
|
-
uintElement
|
|
34
|
-
} from "./helpers/matroska-file.js";
|
|
35
|
-
|
|
36
|
-
const quiet = { info() {}, warn() {} };
|
|
37
|
-
|
|
38
|
-
function episode({ subtitleLanguage = null, cuesBeforeClusters = false, cues = [1], extraTrack = null } = {}) {
|
|
39
|
-
const tracks = [
|
|
40
|
-
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "jpn" }),
|
|
41
|
-
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: subtitleLanguage })
|
|
42
|
-
];
|
|
43
|
-
if (extraTrack) {
|
|
44
|
-
tracks.push(extraTrack);
|
|
45
|
-
}
|
|
46
|
-
return buildMatroska({
|
|
47
|
-
tracks,
|
|
48
|
-
cues,
|
|
49
|
-
cuesBeforeClusters,
|
|
50
|
-
clusters: [
|
|
51
|
-
{ ticks: 0, data: clusterData({ ticks: 0, blocks: [cueBlock({ track: 3, relativeTicks: 100, durationTicks: 900, text: "one" })] }) },
|
|
52
|
-
{ ticks: 5000, data: clusterData({ ticks: 5000, blocks: [cueBlock({ track: 3, relativeTicks: 0, durationTicks: 900, text: "two" })] }) }
|
|
53
|
-
]
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
test("a subtitle plan read before the Cues table arrived is not kept; read again, it names the clusters", async () => {
|
|
58
|
-
const { file, cuesAt, clusterAt } = episode({ cues: [1, 3] });
|
|
59
|
-
let tailHere = false;
|
|
60
|
-
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
61
|
-
const container = new MatroskaContainer({ readRange: read, fileSize: file.length });
|
|
62
|
-
|
|
63
|
-
await assert.rejects(container.readSubtitlePlan(), (error) => isUnavailable(error));
|
|
64
|
-
|
|
65
|
-
tailHere = true;
|
|
66
|
-
const plan = await container.readSubtitlePlan();
|
|
67
|
-
assert.equal(plan.cuesState, "complete");
|
|
68
|
-
assert.deepEqual(plan.tracks[0].clusterPositions, clusterAt, "both clusters the table names for the track");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("a read that returns fewer bytes than asked is not taken for the element", async () => {
|
|
72
|
-
const { file } = episode();
|
|
73
|
-
const { read } = readerOver(file, { short: true });
|
|
74
|
-
await assert.rejects(new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks(), (error) =>
|
|
75
|
-
isUnavailable(error)
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("a head that has not arrived leaves no container behind, and the next ask builds one", async () => {
|
|
80
|
-
const { file } = episode();
|
|
81
|
-
let headHere = false;
|
|
82
|
-
const { read } = readerOver(file, { missing: (start) => !headHere && start === 0 });
|
|
83
|
-
const containers = new ContainerOrchestrator();
|
|
84
|
-
const params = { sourceKey: "s", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
85
|
-
|
|
86
|
-
await assert.rejects(containers.containerFor(params), (error) => isUnavailable(error));
|
|
87
|
-
assert.equal(containers.known("s", 0), undefined, "nothing was kept for a head that is not here");
|
|
88
|
-
assert.deepEqual(await containers.getTracks(params), [], "the track table answers nothing yet");
|
|
89
|
-
|
|
90
|
-
headHere = true;
|
|
91
|
-
const container = await containers.containerFor(params);
|
|
92
|
-
assert.equal(container.formatName, "matroska");
|
|
93
|
-
assert.equal((await containers.getTracks(params)).length, 2);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("a keyframe table asked for while its Cues are missing stays unanswered, and is filled when they arrive", async () => {
|
|
97
|
-
const { file, cuesAt } = episode();
|
|
98
|
-
let tailHere = false;
|
|
99
|
-
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
100
|
-
const containers = new ContainerOrchestrator();
|
|
101
|
-
const params = { sourceKey: "k", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
102
|
-
const tables = new KeyframeTables({
|
|
103
|
-
readTable: async () => {
|
|
104
|
-
const index = await containers.getKeyframeIndex(params);
|
|
105
|
-
return { times: index?.times ?? null, tolerance: 0, format: "matroska" };
|
|
106
|
-
},
|
|
107
|
-
logger: quiet
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await assert.rejects(tables.read({ sourceKey: "k", fileIndex: 0 }), (error) => isUnavailable(error));
|
|
111
|
-
const table = tables.of({ sourceKey: "k", fileIndex: 0 });
|
|
112
|
-
assert.equal(table.answered, false, "a read that did not arrive is not an answer");
|
|
113
|
-
|
|
114
|
-
tailHere = true;
|
|
115
|
-
tables.readAgainIfUnanswered({ sourceKey: "k", fileIndex: 0 });
|
|
116
|
-
await tables.read({ sourceKey: "k", fileIndex: 0 });
|
|
117
|
-
assert.equal(table.answered, true);
|
|
118
|
-
assert.deepEqual(table.times, [0, 5], "the same object the sessions hold now carries the times");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a Cues element stored before the first cluster, with no SeekHead entry, is found", async () => {
|
|
122
|
-
const { file } = episode({ cuesBeforeClusters: true });
|
|
123
|
-
const { read } = readerOver(file);
|
|
124
|
-
const index = await new MatroskaContainer({ readRange: read, fileSize: file.length }).parseKeyframeIndex();
|
|
125
|
-
assert.deepEqual(index.times, [0, 5]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("a Tracks element larger than the head window is read by its own size", async () => {
|
|
129
|
-
// A CodecPrivate of 80 KB pushes the end of Tracks past the 64 KB head.
|
|
130
|
-
const big = element(
|
|
131
|
-
ID.TRACK_ENTRY,
|
|
132
|
-
Buffer.concat([
|
|
133
|
-
uintElement(ID.TRACK_NUMBER, 4),
|
|
134
|
-
uintElement(ID.TRACK_TYPE, 17),
|
|
135
|
-
stringElement(ID.CODEC_ID, "S_TEXT/ASS"),
|
|
136
|
-
element(0x63a2, Buffer.alloc(80 * 1024, 0x20))
|
|
137
|
-
])
|
|
138
|
-
);
|
|
139
|
-
const { file } = episode({ extraTrack: big });
|
|
140
|
-
const { read } = readerOver(file);
|
|
141
|
-
const tracks = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
142
|
-
assert.deepEqual(tracks.map((track) => track.trackNumber), [1, 3, 4]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("the language: LanguageBCP47 wins, then Language, then the format's default `eng`; `und` stays unknown", async () => {
|
|
146
|
-
const cases = [
|
|
147
|
-
{ entry: { language: null }, language: "eng", source: "default" },
|
|
148
|
-
{ entry: { language: "" }, language: "eng", source: "language" },
|
|
149
|
-
{ entry: { language: "rus" }, language: "rus", source: "language" },
|
|
150
|
-
{ entry: { language: null, languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
151
|
-
{ entry: { language: "por", languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
152
|
-
{ entry: { language: "und" }, language: "und", source: "language" }
|
|
153
|
-
];
|
|
154
|
-
for (const { entry, language, source } of cases) {
|
|
155
|
-
const { file } = buildMatroska({
|
|
156
|
-
tracks: [trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", ...entry })],
|
|
157
|
-
cues: null,
|
|
158
|
-
clusters: [{ ticks: 0, data: clusterData({ ticks: 0, blocks: [] }) }]
|
|
159
|
-
});
|
|
160
|
-
const { read } = readerOver(file);
|
|
161
|
-
const [track] = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
162
|
-
assert.equal(track.language, language, JSON.stringify(entry));
|
|
163
|
-
assert.equal(track.languageSource, source, JSON.stringify(entry));
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
/** @param {string} type @param {Buffer} payload */
|
|
168
|
-
function box(type, payload) {
|
|
169
|
-
const header = Buffer.alloc(8);
|
|
170
|
-
header.writeUInt32BE(8 + payload.length, 0);
|
|
171
|
-
header.write(type, 4, "latin1");
|
|
172
|
-
return Buffer.concat([header, payload]);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
test("an MP4 whose `moov` has not arrived keeps nothing; read again, it answers", async () => {
|
|
176
|
-
const mvhd = Buffer.alloc(100);
|
|
177
|
-
mvhd.writeUInt32BE(1000, 12); // timescale
|
|
178
|
-
mvhd.writeUInt32BE(5000, 16); // duration
|
|
179
|
-
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
180
|
-
const moov = box("moov", box("mvhd", mvhd));
|
|
181
|
-
const file = Buffer.concat([ftyp, moov]);
|
|
182
|
-
let moovHere = false;
|
|
183
|
-
const { read } = readerOver(file, { missing: (start) => !moovHere && start >= ftyp.length });
|
|
184
|
-
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
185
|
-
|
|
186
|
-
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
187
|
-
moovHere = true;
|
|
188
|
-
assert.equal((await container.readMediaInfo()).durationSeconds, 5);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("an MP4 `moov` larger than this reads whole is refused by name, not reported absent", async () => {
|
|
192
|
-
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
193
|
-
const header = Buffer.alloc(8);
|
|
194
|
-
header.writeUInt32BE(40 * 1024 * 1024, 0);
|
|
195
|
-
header.write("moov", 4, "latin1");
|
|
196
|
-
const file = Buffer.concat([ftyp, header, Buffer.alloc(64)]);
|
|
197
|
-
const { read } = readerOver(file);
|
|
198
|
-
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
199
|
-
assert.equal(await container.readSubtitlePlan(), null);
|
|
200
|
-
assert.deepEqual(container.moovRefused, { size: 40 * 1024 * 1024, limit: 32 * 1024 * 1024 });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("an AVI head that has not arrived leaves no media info behind", async () => {
|
|
204
|
-
const avih = Buffer.alloc(56);
|
|
205
|
-
avih.writeUInt32LE(40_000, 0); // 25 frames a second
|
|
206
|
-
avih.writeUInt32LE(250, 16); // ten seconds
|
|
207
|
-
const chunk = Buffer.concat([Buffer.from("avih", "latin1"), Buffer.from([56, 0, 0, 0]), avih]);
|
|
208
|
-
const list = Buffer.concat([Buffer.from("LIST", "latin1"), Buffer.alloc(4), Buffer.from("hdrl", "latin1"), chunk]);
|
|
209
|
-
list.writeUInt32LE(list.length - 8, 4);
|
|
210
|
-
const riff = Buffer.concat([Buffer.from("RIFF", "latin1"), Buffer.alloc(4), Buffer.from("AVI ", "latin1"), list]);
|
|
211
|
-
riff.writeUInt32LE(riff.length - 8, 4);
|
|
212
|
-
let here = false;
|
|
213
|
-
const { read } = readerOver(riff, { missing: () => !here });
|
|
214
|
-
const container = new AviContainer({ readRange: read, fileSize: riff.length });
|
|
215
|
-
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
216
|
-
here = true;
|
|
217
|
-
assert.equal((await container.readMediaInfo()).durationSeconds, 10);
|
|
218
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file A read whose bytes have not arrived is not an answer about the file.
|
|
3
|
+
*
|
|
4
|
+
* Field 2026-10-01: the subtitle plan of an episode was read while its Cues
|
|
5
|
+
* table was still downloading, the read was given up, and the plan kept "no
|
|
6
|
+
* clusters" for the life of the process — the track showed nothing for a whole
|
|
7
|
+
* session while the same table, read eleven seconds later for the keyframes,
|
|
8
|
+
* was there. The same shape was in the container choice, the track table, the
|
|
9
|
+
* keyframe table, MP4's `moov` and AVI's `idx1`. Every check here makes the
|
|
10
|
+
* bytes missing first and present afterwards, and asks the same object again.
|
|
11
|
+
*
|
|
12
|
+
* Fakes only: a buffer and a reader over it. No torrent, no thread.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
19
|
+
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
20
|
+
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
21
|
+
import { ContainerOrchestrator } from "../services/media/ContainerOrchestrator.js";
|
|
22
|
+
import { KeyframeTables } from "../services/media/KeyframeTables.js";
|
|
23
|
+
import { isUnavailable } from "../services/media/container/unavailable.js";
|
|
24
|
+
import {
|
|
25
|
+
ID,
|
|
26
|
+
buildMatroska,
|
|
27
|
+
clusterData,
|
|
28
|
+
cueBlock,
|
|
29
|
+
element,
|
|
30
|
+
readerOver,
|
|
31
|
+
stringElement,
|
|
32
|
+
trackEntry,
|
|
33
|
+
uintElement
|
|
34
|
+
} from "./helpers/matroska-file.js";
|
|
35
|
+
|
|
36
|
+
const quiet = { info() {}, warn() {} };
|
|
37
|
+
|
|
38
|
+
function episode({ subtitleLanguage = null, cuesBeforeClusters = false, cues = [1], extraTrack = null } = {}) {
|
|
39
|
+
const tracks = [
|
|
40
|
+
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "jpn" }),
|
|
41
|
+
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: subtitleLanguage })
|
|
42
|
+
];
|
|
43
|
+
if (extraTrack) {
|
|
44
|
+
tracks.push(extraTrack);
|
|
45
|
+
}
|
|
46
|
+
return buildMatroska({
|
|
47
|
+
tracks,
|
|
48
|
+
cues,
|
|
49
|
+
cuesBeforeClusters,
|
|
50
|
+
clusters: [
|
|
51
|
+
{ ticks: 0, data: clusterData({ ticks: 0, blocks: [cueBlock({ track: 3, relativeTicks: 100, durationTicks: 900, text: "one" })] }) },
|
|
52
|
+
{ ticks: 5000, data: clusterData({ ticks: 5000, blocks: [cueBlock({ track: 3, relativeTicks: 0, durationTicks: 900, text: "two" })] }) }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("a subtitle plan read before the Cues table arrived is not kept; read again, it names the clusters", async () => {
|
|
58
|
+
const { file, cuesAt, clusterAt } = episode({ cues: [1, 3] });
|
|
59
|
+
let tailHere = false;
|
|
60
|
+
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
61
|
+
const container = new MatroskaContainer({ readRange: read, fileSize: file.length });
|
|
62
|
+
|
|
63
|
+
await assert.rejects(container.readSubtitlePlan(), (error) => isUnavailable(error));
|
|
64
|
+
|
|
65
|
+
tailHere = true;
|
|
66
|
+
const plan = await container.readSubtitlePlan();
|
|
67
|
+
assert.equal(plan.cuesState, "complete");
|
|
68
|
+
assert.deepEqual(plan.tracks[0].clusterPositions, clusterAt, "both clusters the table names for the track");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a read that returns fewer bytes than asked is not taken for the element", async () => {
|
|
72
|
+
const { file } = episode();
|
|
73
|
+
const { read } = readerOver(file, { short: true });
|
|
74
|
+
await assert.rejects(new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks(), (error) =>
|
|
75
|
+
isUnavailable(error)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a head that has not arrived leaves no container behind, and the next ask builds one", async () => {
|
|
80
|
+
const { file } = episode();
|
|
81
|
+
let headHere = false;
|
|
82
|
+
const { read } = readerOver(file, { missing: (start) => !headHere && start === 0 });
|
|
83
|
+
const containers = new ContainerOrchestrator();
|
|
84
|
+
const params = { sourceKey: "s", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
85
|
+
|
|
86
|
+
await assert.rejects(containers.containerFor(params), (error) => isUnavailable(error));
|
|
87
|
+
assert.equal(containers.known("s", 0), undefined, "nothing was kept for a head that is not here");
|
|
88
|
+
assert.deepEqual(await containers.getTracks(params), [], "the track table answers nothing yet");
|
|
89
|
+
|
|
90
|
+
headHere = true;
|
|
91
|
+
const container = await containers.containerFor(params);
|
|
92
|
+
assert.equal(container.formatName, "matroska");
|
|
93
|
+
assert.equal((await containers.getTracks(params)).length, 2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a keyframe table asked for while its Cues are missing stays unanswered, and is filled when they arrive", async () => {
|
|
97
|
+
const { file, cuesAt } = episode();
|
|
98
|
+
let tailHere = false;
|
|
99
|
+
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
100
|
+
const containers = new ContainerOrchestrator();
|
|
101
|
+
const params = { sourceKey: "k", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
102
|
+
const tables = new KeyframeTables({
|
|
103
|
+
readTable: async () => {
|
|
104
|
+
const index = await containers.getKeyframeIndex(params);
|
|
105
|
+
return { times: index?.times ?? null, tolerance: 0, format: "matroska" };
|
|
106
|
+
},
|
|
107
|
+
logger: quiet
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await assert.rejects(tables.read({ sourceKey: "k", fileIndex: 0 }), (error) => isUnavailable(error));
|
|
111
|
+
const table = tables.of({ sourceKey: "k", fileIndex: 0 });
|
|
112
|
+
assert.equal(table.answered, false, "a read that did not arrive is not an answer");
|
|
113
|
+
|
|
114
|
+
tailHere = true;
|
|
115
|
+
tables.readAgainIfUnanswered({ sourceKey: "k", fileIndex: 0 });
|
|
116
|
+
await tables.read({ sourceKey: "k", fileIndex: 0 });
|
|
117
|
+
assert.equal(table.answered, true);
|
|
118
|
+
assert.deepEqual(table.times, [0, 5], "the same object the sessions hold now carries the times");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a Cues element stored before the first cluster, with no SeekHead entry, is found", async () => {
|
|
122
|
+
const { file } = episode({ cuesBeforeClusters: true });
|
|
123
|
+
const { read } = readerOver(file);
|
|
124
|
+
const index = await new MatroskaContainer({ readRange: read, fileSize: file.length }).parseKeyframeIndex();
|
|
125
|
+
assert.deepEqual(index.times, [0, 5]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a Tracks element larger than the head window is read by its own size", async () => {
|
|
129
|
+
// A CodecPrivate of 80 KB pushes the end of Tracks past the 64 KB head.
|
|
130
|
+
const big = element(
|
|
131
|
+
ID.TRACK_ENTRY,
|
|
132
|
+
Buffer.concat([
|
|
133
|
+
uintElement(ID.TRACK_NUMBER, 4),
|
|
134
|
+
uintElement(ID.TRACK_TYPE, 17),
|
|
135
|
+
stringElement(ID.CODEC_ID, "S_TEXT/ASS"),
|
|
136
|
+
element(0x63a2, Buffer.alloc(80 * 1024, 0x20))
|
|
137
|
+
])
|
|
138
|
+
);
|
|
139
|
+
const { file } = episode({ extraTrack: big });
|
|
140
|
+
const { read } = readerOver(file);
|
|
141
|
+
const tracks = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
142
|
+
assert.deepEqual(tracks.map((track) => track.trackNumber), [1, 3, 4]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("the language: LanguageBCP47 wins, then Language, then the format's default `eng`; `und` stays unknown", async () => {
|
|
146
|
+
const cases = [
|
|
147
|
+
{ entry: { language: null }, language: "eng", source: "default" },
|
|
148
|
+
{ entry: { language: "" }, language: "eng", source: "language" },
|
|
149
|
+
{ entry: { language: "rus" }, language: "rus", source: "language" },
|
|
150
|
+
{ entry: { language: null, languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
151
|
+
{ entry: { language: "por", languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
152
|
+
{ entry: { language: "und" }, language: "und", source: "language" }
|
|
153
|
+
];
|
|
154
|
+
for (const { entry, language, source } of cases) {
|
|
155
|
+
const { file } = buildMatroska({
|
|
156
|
+
tracks: [trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", ...entry })],
|
|
157
|
+
cues: null,
|
|
158
|
+
clusters: [{ ticks: 0, data: clusterData({ ticks: 0, blocks: [] }) }]
|
|
159
|
+
});
|
|
160
|
+
const { read } = readerOver(file);
|
|
161
|
+
const [track] = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
162
|
+
assert.equal(track.language, language, JSON.stringify(entry));
|
|
163
|
+
assert.equal(track.languageSource, source, JSON.stringify(entry));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/** @param {string} type @param {Buffer} payload */
|
|
168
|
+
function box(type, payload) {
|
|
169
|
+
const header = Buffer.alloc(8);
|
|
170
|
+
header.writeUInt32BE(8 + payload.length, 0);
|
|
171
|
+
header.write(type, 4, "latin1");
|
|
172
|
+
return Buffer.concat([header, payload]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
test("an MP4 whose `moov` has not arrived keeps nothing; read again, it answers", async () => {
|
|
176
|
+
const mvhd = Buffer.alloc(100);
|
|
177
|
+
mvhd.writeUInt32BE(1000, 12); // timescale
|
|
178
|
+
mvhd.writeUInt32BE(5000, 16); // duration
|
|
179
|
+
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
180
|
+
const moov = box("moov", box("mvhd", mvhd));
|
|
181
|
+
const file = Buffer.concat([ftyp, moov]);
|
|
182
|
+
let moovHere = false;
|
|
183
|
+
const { read } = readerOver(file, { missing: (start) => !moovHere && start >= ftyp.length });
|
|
184
|
+
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
185
|
+
|
|
186
|
+
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
187
|
+
moovHere = true;
|
|
188
|
+
assert.equal((await container.readMediaInfo()).durationSeconds, 5);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("an MP4 `moov` larger than this reads whole is refused by name, not reported absent", async () => {
|
|
192
|
+
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
193
|
+
const header = Buffer.alloc(8);
|
|
194
|
+
header.writeUInt32BE(40 * 1024 * 1024, 0);
|
|
195
|
+
header.write("moov", 4, "latin1");
|
|
196
|
+
const file = Buffer.concat([ftyp, header, Buffer.alloc(64)]);
|
|
197
|
+
const { read } = readerOver(file);
|
|
198
|
+
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
199
|
+
assert.equal(await container.readSubtitlePlan(), null);
|
|
200
|
+
assert.deepEqual(container.moovRefused, { size: 40 * 1024 * 1024, limit: 32 * 1024 * 1024 });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an AVI head that has not arrived leaves no media info behind", async () => {
|
|
204
|
+
const avih = Buffer.alloc(56);
|
|
205
|
+
avih.writeUInt32LE(40_000, 0); // 25 frames a second
|
|
206
|
+
avih.writeUInt32LE(250, 16); // ten seconds
|
|
207
|
+
const chunk = Buffer.concat([Buffer.from("avih", "latin1"), Buffer.from([56, 0, 0, 0]), avih]);
|
|
208
|
+
const list = Buffer.concat([Buffer.from("LIST", "latin1"), Buffer.alloc(4), Buffer.from("hdrl", "latin1"), chunk]);
|
|
209
|
+
list.writeUInt32LE(list.length - 8, 4);
|
|
210
|
+
const riff = Buffer.concat([Buffer.from("RIFF", "latin1"), Buffer.alloc(4), Buffer.from("AVI ", "latin1"), list]);
|
|
211
|
+
riff.writeUInt32LE(riff.length - 8, 4);
|
|
212
|
+
let here = false;
|
|
213
|
+
const { read } = readerOver(riff, { missing: () => !here });
|
|
214
|
+
const container = new AviContainer({ readRange: read, fileSize: riff.length });
|
|
215
|
+
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
216
|
+
here = true;
|
|
217
|
+
assert.equal((await container.readMediaInfo()).durationSeconds, 10);
|
|
218
|
+
});
|
|
@@ -28,7 +28,7 @@ import { rm, writeFile } from "node:fs/promises";
|
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import { setTimeout as delay } from "node:timers/promises";
|
|
30
30
|
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
31
|
-
import { waitForSessionFile } from "../
|
|
31
|
+
import { waitForSessionFile } from "../services/server/transcode-session-files.js";
|
|
32
32
|
import { SourceFile } from "../services/media/SourceFile.js";
|
|
33
33
|
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
34
34
|
import { Output } from "../services/encode/output/Output.js";
|