@flow-player/test-streams 0.6.0 → 0.6.2
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/{src/cli/extend-vod.ts → dist/cli/extend-vod.js} +39 -61
- package/dist/cli/generate.js +473 -0
- package/dist/cli/serve.js +644 -0
- package/dist/config.d.ts +29 -0
- package/dist/fixtures/catalog.d.ts +37 -0
- package/dist/fixtures/catalog.js +63 -0
- package/dist/fixtures/codec.d.ts +10 -0
- package/dist/fixtures/format.d.ts +13 -0
- package/dist/fixtures/generate.d.ts +11 -0
- package/dist/fixtures/generate.js +361 -0
- package/dist/fixtures/matrix.d.ts +18 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +894 -0
- package/dist/server/live.d.ts +14 -0
- package/dist/server/routes.d.ts +18 -0
- package/dist/server/server.d.ts +18 -0
- package/dist/server/server.js +534 -0
- package/dist/server/static.d.ts +13 -0
- package/dist/vite-plugin.d.ts +12 -0
- package/dist/vite-plugin.js +58 -0
- package/package.json +24 -11
- package/src/cli/generate.ts +0 -138
- package/src/cli/serve.ts +0 -112
- package/src/config.ts +0 -34
- package/src/fixtures/catalog.ts +0 -99
- package/src/fixtures/codec.ts +0 -43
- package/src/fixtures/format.ts +0 -118
- package/src/fixtures/generate.ts +0 -209
- package/src/fixtures/matrix.ts +0 -94
- package/src/index.ts +0 -41
- package/src/server/live.ts +0 -220
- package/src/server/routes.ts +0 -141
- package/src/server/server.ts +0 -193
- package/src/server/static.ts +0 -83
- package/src/visual/analyze.ts +0 -234
- package/src/visual/dhash.ts +0 -76
- package/src/visual/index.ts +0 -18
- package/src/visual/report.ts +0 -80
- package/src/vite-plugin.d.ts +0 -10
- package/src/vite-plugin.ts +0 -76
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/fixtures/catalog.ts
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
var ALL_CODECS = ["h264", "hevc"];
|
|
7
|
+
var ALL_FORMATS = ["flv", "hls", "mp4", "fmp4", "ts"];
|
|
8
|
+
|
|
9
|
+
// src/fixtures/catalog.ts
|
|
10
|
+
function loadManifest(fixtureDir) {
|
|
11
|
+
const path = resolve(fixtureDir, "manifest.json");
|
|
12
|
+
if (!existsSync(path)) return null;
|
|
13
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
14
|
+
return raw;
|
|
15
|
+
}
|
|
16
|
+
function lookupFixture(manifest, codec, format) {
|
|
17
|
+
if (!manifest) return { files: [], mainFile: null, exists: false };
|
|
18
|
+
const item = manifest.fixtures.find((f) => f.codec === codec && f.format === format);
|
|
19
|
+
if (!item) return { files: [], mainFile: null, exists: false };
|
|
20
|
+
const mainFile = item.entries[0] ?? null;
|
|
21
|
+
return { files: item.entries, mainFile, exists: true };
|
|
22
|
+
}
|
|
23
|
+
function summarizeCatalog(fixtureDir) {
|
|
24
|
+
const manifest = loadManifest(fixtureDir);
|
|
25
|
+
if (!manifest) {
|
|
26
|
+
return {
|
|
27
|
+
codecs: ALL_CODECS,
|
|
28
|
+
formats: ALL_FORMATS,
|
|
29
|
+
fixtureCount: 0,
|
|
30
|
+
exists: false,
|
|
31
|
+
reason: `manifest.json not found at ${resolve(fixtureDir, "manifest.json")}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let total = 0;
|
|
35
|
+
for (const item of manifest.fixtures) total += item.entries.length;
|
|
36
|
+
return {
|
|
37
|
+
codecs: ALL_CODECS,
|
|
38
|
+
formats: ALL_FORMATS,
|
|
39
|
+
fixtureCount: total,
|
|
40
|
+
exists: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function verifyEntryOnDisk(fixtureDir, entry) {
|
|
44
|
+
const abs = resolve(fixtureDir, entry.path);
|
|
45
|
+
if (!existsSync(abs)) {
|
|
46
|
+
return { ok: false, size: 0, reason: `missing file: ${entry.path}` };
|
|
47
|
+
}
|
|
48
|
+
const size = statSync(abs).size;
|
|
49
|
+
if (size !== entry.size) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
size,
|
|
53
|
+
reason: `size mismatch for ${entry.path}: manifest=${entry.size} disk=${size}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, size };
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
loadManifest,
|
|
60
|
+
lookupFixture,
|
|
61
|
+
summarizeCatalog,
|
|
62
|
+
verifyEntryOnDisk
|
|
63
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CodecId } from "../config.js";
|
|
2
|
+
export type CodecSpec = {
|
|
3
|
+
codec: CodecId;
|
|
4
|
+
encoder: string;
|
|
5
|
+
videoArgs: ReadonlyArray<string>;
|
|
6
|
+
audioArgs: ReadonlyArray<string>;
|
|
7
|
+
pixelFormat: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const CODEC_SPEC: Record<CodecId, CodecSpec>;
|
|
10
|
+
export declare function containerExtraArgs(format: "flv" | "hls" | "mp4" | "fmp4" | "ts", codec: CodecId): ReadonlyArray<string>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CodecId, FormatId } from "../config.js";
|
|
2
|
+
export type FormatLayout = {
|
|
3
|
+
format: FormatId;
|
|
4
|
+
mime: string;
|
|
5
|
+
mainFileName: string;
|
|
6
|
+
containerArgs: ReadonlyArray<string>;
|
|
7
|
+
isContainerDirectory: boolean;
|
|
8
|
+
sideFiles: ReadonlyArray<string>;
|
|
9
|
+
};
|
|
10
|
+
export declare function layoutFor(codec: CodecId, format: FormatId): FormatLayout;
|
|
11
|
+
export declare function singleFileRelative(codec: CodecId, format: FormatId): string;
|
|
12
|
+
export declare function containerRelativeDir(codec: CodecId, format: FormatId): string;
|
|
13
|
+
export declare function containerRelativePath(codec: CodecId, format: FormatId, name: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type GenerateOptions } from "../config.js";
|
|
2
|
+
import { type Manifest } from "./catalog.js";
|
|
3
|
+
export type GenerateResult = {
|
|
4
|
+
fixtureDir: string;
|
|
5
|
+
manifest: Manifest;
|
|
6
|
+
generated: number;
|
|
7
|
+
skipped: number;
|
|
8
|
+
ffmpegPath: string;
|
|
9
|
+
ffmpegMissing: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function generateFixtures(opts?: GenerateOptions): Promise<GenerateResult>;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// src/fixtures/generate.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
statSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { join as join3, resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
var DEFAULT_FIXTURE_DIR = "fixtures";
|
|
17
|
+
var DEFAULT_DURATION_SECONDS = 5;
|
|
18
|
+
var ALL_CODECS = ["h264", "hevc"];
|
|
19
|
+
var ALL_FORMATS = ["flv", "hls", "mp4", "fmp4", "ts"];
|
|
20
|
+
|
|
21
|
+
// src/fixtures/matrix.ts
|
|
22
|
+
import { join as join2 } from "node:path";
|
|
23
|
+
|
|
24
|
+
// src/fixtures/codec.ts
|
|
25
|
+
var CODEC_SPEC = {
|
|
26
|
+
h264: {
|
|
27
|
+
codec: "h264",
|
|
28
|
+
encoder: "libx264",
|
|
29
|
+
videoArgs: ["-profile:v", "high", "-preset", "veryfast"],
|
|
30
|
+
audioArgs: ["-c:a", "aac", "-b:a", "128k"],
|
|
31
|
+
pixelFormat: "yuv420p"
|
|
32
|
+
},
|
|
33
|
+
hevc: {
|
|
34
|
+
codec: "hevc",
|
|
35
|
+
encoder: "libx265",
|
|
36
|
+
// 注意:FLV / TS 容器用 enhanced-rtmp 1cvh tag,不能强行写 hvc1;
|
|
37
|
+
// MP4 / fMP4 / HLS 走 hvc1。containerArgs 在 buildPlan 时按 format 决定。
|
|
38
|
+
videoArgs: ["-preset", "veryfast"],
|
|
39
|
+
audioArgs: ["-c:a", "aac", "-b:a", "128k"],
|
|
40
|
+
pixelFormat: "yuv420p"
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function containerExtraArgs(format, codec) {
|
|
44
|
+
if (codec !== "hevc") return [];
|
|
45
|
+
if (format === "mp4") return ["-tag:v", "hvc1"];
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/fixtures/format.ts
|
|
50
|
+
import { join } from "node:path";
|
|
51
|
+
function formatSpecific(format) {
|
|
52
|
+
if (format === "flv") return ["-f", "flv"];
|
|
53
|
+
if (format === "hls") {
|
|
54
|
+
return [
|
|
55
|
+
"-f",
|
|
56
|
+
"hls",
|
|
57
|
+
"-hls_time",
|
|
58
|
+
"2",
|
|
59
|
+
"-hls_playlist_type",
|
|
60
|
+
"vod",
|
|
61
|
+
"-hls_segment_type",
|
|
62
|
+
"mpegts",
|
|
63
|
+
"-hls_flags",
|
|
64
|
+
"independent_segments"
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
if (format === "mp4") return ["-f", "mp4", "-movflags", "+faststart"];
|
|
68
|
+
if (format === "fmp4") {
|
|
69
|
+
return [
|
|
70
|
+
"-f",
|
|
71
|
+
"mp4",
|
|
72
|
+
"-movflags",
|
|
73
|
+
"frag_keyframe+empty_moov+default_base_moof",
|
|
74
|
+
"-frag_duration",
|
|
75
|
+
"2000000"
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (format === "ts") return ["-f", "mpegts"];
|
|
79
|
+
throw new Error(`Unknown format: ${String(format)}`);
|
|
80
|
+
}
|
|
81
|
+
function layoutFor(codec, format) {
|
|
82
|
+
if (format === "hls") {
|
|
83
|
+
return {
|
|
84
|
+
format,
|
|
85
|
+
mime: "application/vnd.apple.mpegurl",
|
|
86
|
+
mainFileName: "master.m3u8",
|
|
87
|
+
containerArgs: formatSpecific(format),
|
|
88
|
+
isContainerDirectory: true,
|
|
89
|
+
sideFiles: ["seg-00000.ts", "seg-00001.ts", "seg-00002.ts"]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (format === "fmp4") {
|
|
93
|
+
return {
|
|
94
|
+
format,
|
|
95
|
+
mime: "video/iso.segment",
|
|
96
|
+
mainFileName: "init.mp4",
|
|
97
|
+
containerArgs: formatSpecific(format),
|
|
98
|
+
isContainerDirectory: true,
|
|
99
|
+
sideFiles: ["seg-00000.m4s", "seg-00001.m4s", "seg-00002.m4s"]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (format === "flv") {
|
|
103
|
+
return {
|
|
104
|
+
format,
|
|
105
|
+
mime: "video/x-flv",
|
|
106
|
+
mainFileName: `${codec}.flv`,
|
|
107
|
+
containerArgs: formatSpecific(format),
|
|
108
|
+
isContainerDirectory: false,
|
|
109
|
+
sideFiles: []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (format === "mp4") {
|
|
113
|
+
return {
|
|
114
|
+
format,
|
|
115
|
+
mime: "video/mp4",
|
|
116
|
+
mainFileName: `${codec}.mp4`,
|
|
117
|
+
containerArgs: formatSpecific(format),
|
|
118
|
+
isContainerDirectory: false,
|
|
119
|
+
sideFiles: []
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (format === "ts") {
|
|
123
|
+
return {
|
|
124
|
+
format,
|
|
125
|
+
mime: "video/mp2t",
|
|
126
|
+
mainFileName: `${codec}.ts`,
|
|
127
|
+
containerArgs: formatSpecific(format),
|
|
128
|
+
isContainerDirectory: false,
|
|
129
|
+
sideFiles: []
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Unknown format: ${String(format)}`);
|
|
133
|
+
}
|
|
134
|
+
function singleFileRelative(codec, format) {
|
|
135
|
+
return join(format, `${codec}.${format}`);
|
|
136
|
+
}
|
|
137
|
+
function containerRelativeDir(codec, format) {
|
|
138
|
+
return join(format, codec);
|
|
139
|
+
}
|
|
140
|
+
function containerRelativePath(codec, format, name) {
|
|
141
|
+
return join(containerRelativeDir(codec, format), name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/fixtures/matrix.ts
|
|
145
|
+
function buildPlan(codec, format, durationSeconds) {
|
|
146
|
+
const layout = layoutFor(codec, format);
|
|
147
|
+
const spec = CODEC_SPEC[codec];
|
|
148
|
+
const gop = String(durationSeconds * 25);
|
|
149
|
+
const videoInput = `testsrc2=size=1280x720:rate=25,format=yuv420p`;
|
|
150
|
+
const audioInput = `sine=frequency=1000:sample_rate=44100`;
|
|
151
|
+
const ffmpegOutputArgs = [
|
|
152
|
+
"-y",
|
|
153
|
+
"-f",
|
|
154
|
+
"lavfi",
|
|
155
|
+
"-i",
|
|
156
|
+
videoInput,
|
|
157
|
+
"-f",
|
|
158
|
+
"lavfi",
|
|
159
|
+
"-i",
|
|
160
|
+
audioInput,
|
|
161
|
+
"-t",
|
|
162
|
+
String(durationSeconds),
|
|
163
|
+
"-c:v",
|
|
164
|
+
spec.encoder,
|
|
165
|
+
...spec.videoArgs,
|
|
166
|
+
"-pix_fmt",
|
|
167
|
+
spec.pixelFormat,
|
|
168
|
+
"-g",
|
|
169
|
+
gop,
|
|
170
|
+
"-keyint_min",
|
|
171
|
+
gop,
|
|
172
|
+
"-sc_threshold",
|
|
173
|
+
"0",
|
|
174
|
+
...containerExtraArgs(format, codec),
|
|
175
|
+
...spec.audioArgs,
|
|
176
|
+
...layout.containerArgs
|
|
177
|
+
];
|
|
178
|
+
if (layout.isContainerDirectory) {
|
|
179
|
+
const containerDir2 = containerRelativeDir(codec, format);
|
|
180
|
+
const mainFile2 = containerRelativePath(codec, format, layout.mainFileName);
|
|
181
|
+
const files = [
|
|
182
|
+
mainFile2,
|
|
183
|
+
...layout.sideFiles.map((s) => containerRelativePath(codec, format, s))
|
|
184
|
+
];
|
|
185
|
+
return { codec, format, layout, containerDir: containerDir2, mainFile: mainFile2, files, ffmpegOutputArgs };
|
|
186
|
+
}
|
|
187
|
+
const mainFile = singleFileRelative(codec, format);
|
|
188
|
+
const containerDir = join2(mainFile.split("/").slice(0, -1).join("/"));
|
|
189
|
+
return { codec, format, layout, containerDir, mainFile, files: [mainFile], ffmpegOutputArgs };
|
|
190
|
+
}
|
|
191
|
+
function expandMatrix(opts) {
|
|
192
|
+
const codecs = opts?.codecs ?? ALL_CODECS;
|
|
193
|
+
const formats = opts?.formats ?? ALL_FORMATS;
|
|
194
|
+
const duration = opts?.durationSeconds ?? 5;
|
|
195
|
+
const plans = [];
|
|
196
|
+
for (const codec of codecs) {
|
|
197
|
+
for (const format of formats) {
|
|
198
|
+
plans.push(buildPlan(codec, format, duration));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return plans;
|
|
202
|
+
}
|
|
203
|
+
var VOD_SAMPLE_RELATIVE = "mp4/h264.mp4";
|
|
204
|
+
|
|
205
|
+
// src/fixtures/generate.ts
|
|
206
|
+
var FFMPEG_PROBE_ARGS = ["-version"];
|
|
207
|
+
async function generateFixtures(opts = {}) {
|
|
208
|
+
const ffmpegPath = opts.ffmpegPath ?? await findFfmpeg();
|
|
209
|
+
if (!ffmpegPath) {
|
|
210
|
+
return {
|
|
211
|
+
fixtureDir: resolve(opts.fixtureDir ?? DEFAULT_FIXTURE_DIR),
|
|
212
|
+
manifest: emptyManifest(opts.durationSeconds ?? DEFAULT_DURATION_SECONDS),
|
|
213
|
+
generated: 0,
|
|
214
|
+
skipped: 0,
|
|
215
|
+
ffmpegPath: "ffmpeg",
|
|
216
|
+
ffmpegMissing: true
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const fixtureDir = resolve(opts.fixtureDir ?? DEFAULT_FIXTURE_DIR);
|
|
220
|
+
mkdirSync(fixtureDir, { recursive: true });
|
|
221
|
+
const duration = opts.durationSeconds ?? DEFAULT_DURATION_SECONDS;
|
|
222
|
+
const plans = expandMatrix({
|
|
223
|
+
codecs: opts.codecs ?? ALL_CODECS,
|
|
224
|
+
formats: opts.formats ?? ALL_FORMATS,
|
|
225
|
+
durationSeconds: duration
|
|
226
|
+
});
|
|
227
|
+
const manifestItems = [];
|
|
228
|
+
let generated = 0;
|
|
229
|
+
let skipped = 0;
|
|
230
|
+
for (const plan of plans) {
|
|
231
|
+
const item = await generateOne(
|
|
232
|
+
plan,
|
|
233
|
+
fixtureDir,
|
|
234
|
+
ffmpegPath,
|
|
235
|
+
opts.quiet ?? false,
|
|
236
|
+
opts.overwrite ?? false
|
|
237
|
+
);
|
|
238
|
+
if (item) {
|
|
239
|
+
manifestItems.push(item);
|
|
240
|
+
generated += 1;
|
|
241
|
+
} else {
|
|
242
|
+
skipped += 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
copyVodSamples(fixtureDir);
|
|
246
|
+
const manifest = {
|
|
247
|
+
version: 1,
|
|
248
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
249
|
+
durationSeconds: duration,
|
|
250
|
+
fixtures: manifestItems
|
|
251
|
+
};
|
|
252
|
+
writeFileSync(join3(fixtureDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
253
|
+
return { fixtureDir, manifest, generated, skipped, ffmpegPath, ffmpegMissing: false };
|
|
254
|
+
}
|
|
255
|
+
async function generateOne(plan, fixtureDir, ffmpegPath, quiet, overwrite) {
|
|
256
|
+
const outDir = join3(fixtureDir, plan.containerDir);
|
|
257
|
+
mkdirSync(outDir, { recursive: true });
|
|
258
|
+
const outFile = join3(fixtureDir, plan.mainFile);
|
|
259
|
+
if (!overwrite && existsSync(outFile)) {
|
|
260
|
+
return collectExisting(plan, fixtureDir);
|
|
261
|
+
}
|
|
262
|
+
if (plan.layout.isContainerDirectory) {
|
|
263
|
+
for (const f of readdirSync(outDir)) {
|
|
264
|
+
if (f === plan.layout.mainFileName) continue;
|
|
265
|
+
rmIfExists(join3(outDir, f));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const args = plan.ffmpegOutputArgs.concat([outFile]);
|
|
269
|
+
if (!quiet) {
|
|
270
|
+
process.stdout.write(`[generate] ${plan.codec}/${plan.format} -> ${plan.mainFile}
|
|
271
|
+
`);
|
|
272
|
+
}
|
|
273
|
+
const code = await runFfmpeg(ffmpegPath, args, quiet);
|
|
274
|
+
if (code !== 0) {
|
|
275
|
+
throw new Error(`ffmpeg exited with code ${code} for ${plan.codec}/${plan.format}`);
|
|
276
|
+
}
|
|
277
|
+
return collectExisting(plan, fixtureDir);
|
|
278
|
+
}
|
|
279
|
+
function collectExisting(plan, fixtureDir) {
|
|
280
|
+
const entries = [];
|
|
281
|
+
for (const rel of plan.files) {
|
|
282
|
+
const abs = join3(fixtureDir, rel);
|
|
283
|
+
if (!existsSync(abs)) continue;
|
|
284
|
+
entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
|
|
285
|
+
}
|
|
286
|
+
if (plan.layout.isContainerDirectory) {
|
|
287
|
+
const absDir = join3(fixtureDir, plan.containerDir);
|
|
288
|
+
if (existsSync(absDir)) {
|
|
289
|
+
for (const name of readdirSync(absDir).sort()) {
|
|
290
|
+
if (name === plan.layout.mainFileName) continue;
|
|
291
|
+
const rel = join3(plan.containerDir, name);
|
|
292
|
+
if (entries.find((e) => e.path === rel)) continue;
|
|
293
|
+
const abs = join3(absDir, name);
|
|
294
|
+
if (!statSync(abs).isFile()) continue;
|
|
295
|
+
entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { codec: plan.codec, format: plan.format, entries };
|
|
300
|
+
}
|
|
301
|
+
function copyVodSamples(fixtureDir) {
|
|
302
|
+
const src = join3(fixtureDir, VOD_SAMPLE_RELATIVE);
|
|
303
|
+
if (!existsSync(src)) return;
|
|
304
|
+
const vodDir = join3(fixtureDir, "vod");
|
|
305
|
+
mkdirSync(vodDir, { recursive: true });
|
|
306
|
+
for (const name of ["sample-1.mp4", "sample-2.mp4"]) {
|
|
307
|
+
const dst = join3(vodDir, name);
|
|
308
|
+
writeFileSync(dst, readFileSync(src));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function rmIfExists(abs) {
|
|
312
|
+
if (!existsSync(abs)) return;
|
|
313
|
+
try {
|
|
314
|
+
unlinkSync(abs);
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function runFfmpeg(ffmpegPath, args, quiet) {
|
|
319
|
+
const { promise, resolve: resolve2, reject } = Promise.withResolvers();
|
|
320
|
+
const proc = spawn(ffmpegPath, args, {
|
|
321
|
+
stdio: quiet ? ["ignore", "ignore", "ignore"] : "inherit"
|
|
322
|
+
});
|
|
323
|
+
proc.once("error", reject);
|
|
324
|
+
proc.once("exit", (code) => resolve2(code ?? 1));
|
|
325
|
+
return promise;
|
|
326
|
+
}
|
|
327
|
+
async function findFfmpeg() {
|
|
328
|
+
const candidates = [
|
|
329
|
+
"ffmpeg",
|
|
330
|
+
"/opt/homebrew/bin/ffmpeg",
|
|
331
|
+
"/usr/local/bin/ffmpeg",
|
|
332
|
+
"/usr/bin/ffmpeg"
|
|
333
|
+
];
|
|
334
|
+
for (const candidate of candidates) {
|
|
335
|
+
if (await canRun(candidate)) return candidate;
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
async function canRun(path) {
|
|
340
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
341
|
+
const proc = spawn(path, FFMPEG_PROBE_ARGS, { stdio: "ignore" });
|
|
342
|
+
proc.once("error", () => resolve2(false));
|
|
343
|
+
proc.once("exit", (code) => resolve2(code === 0));
|
|
344
|
+
return promise;
|
|
345
|
+
}
|
|
346
|
+
function hashFile(abs) {
|
|
347
|
+
const hash = createHash("sha256");
|
|
348
|
+
hash.update(readFileSync(abs));
|
|
349
|
+
return hash.digest("hex");
|
|
350
|
+
}
|
|
351
|
+
function emptyManifest(duration) {
|
|
352
|
+
return {
|
|
353
|
+
version: 1,
|
|
354
|
+
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
355
|
+
durationSeconds: duration,
|
|
356
|
+
fixtures: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
export {
|
|
360
|
+
generateFixtures
|
|
361
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type CodecId, type FormatId } from "../config.js";
|
|
2
|
+
import { type FormatLayout } from "./format.js";
|
|
3
|
+
export type FixturePlan = {
|
|
4
|
+
codec: CodecId;
|
|
5
|
+
format: FormatId;
|
|
6
|
+
layout: FormatLayout;
|
|
7
|
+
containerDir: string;
|
|
8
|
+
mainFile: string;
|
|
9
|
+
files: ReadonlyArray<string>;
|
|
10
|
+
ffmpegOutputArgs: ReadonlyArray<string>;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildPlan(codec: CodecId, format: FormatId, durationSeconds: number): FixturePlan;
|
|
13
|
+
export declare function expandMatrix(opts?: {
|
|
14
|
+
codecs?: ReadonlyArray<CodecId>;
|
|
15
|
+
formats?: ReadonlyArray<FormatId>;
|
|
16
|
+
durationSeconds?: number;
|
|
17
|
+
}): ReadonlyArray<FixturePlan>;
|
|
18
|
+
export declare const VOD_SAMPLE_RELATIVE = "mp4/h264.mp4";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { buildPlan, expandMatrix, VOD_SAMPLE_RELATIVE, type FixturePlan, } from "./fixtures/matrix.js";
|
|
2
|
+
export { loadManifest, lookupFixture, summarizeCatalog, verifyEntryOnDisk, type CatalogSummary, type FixtureLookup, type Manifest, type ManifestEntry, type ManifestItem, } from "./fixtures/catalog.js";
|
|
3
|
+
export { CODEC_SPEC, type CodecSpec } from "./fixtures/codec.js";
|
|
4
|
+
export { layoutFor, containerRelativeDir, containerRelativePath, type FormatLayout, } from "./fixtures/format.js";
|
|
5
|
+
export { ALL_CODECS, ALL_FORMATS, DEFAULT_DURATION_SECONDS, DEFAULT_FIXTURE_DIR, DEFAULT_HLS_SEGMENT_SECONDS, DEFAULT_HOST, DEFAULT_PORT, type CodecId, type FormatId, type GenerateOptions, type ServeOptions, } from "./config.js";
|
|
6
|
+
export { createStreamsServer, type StreamsServerHandle } from "./server/server.js";
|
|
7
|
+
export { generateFixtures, type GenerateResult } from "./fixtures/generate.js";
|
|
8
|
+
export { handleLiveStream, matchLivePath, type LiveStreamOptions } from "./server/live.js";
|