@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.
Files changed (40) hide show
  1. package/{src/cli/extend-vod.ts → dist/cli/extend-vod.js} +39 -61
  2. package/dist/cli/generate.js +473 -0
  3. package/dist/cli/serve.js +644 -0
  4. package/dist/config.d.ts +29 -0
  5. package/dist/fixtures/catalog.d.ts +37 -0
  6. package/dist/fixtures/catalog.js +63 -0
  7. package/dist/fixtures/codec.d.ts +10 -0
  8. package/dist/fixtures/format.d.ts +13 -0
  9. package/dist/fixtures/generate.d.ts +11 -0
  10. package/dist/fixtures/generate.js +361 -0
  11. package/dist/fixtures/matrix.d.ts +18 -0
  12. package/dist/index.d.ts +8 -0
  13. package/dist/index.js +894 -0
  14. package/dist/server/live.d.ts +14 -0
  15. package/dist/server/routes.d.ts +18 -0
  16. package/dist/server/server.d.ts +18 -0
  17. package/dist/server/server.js +534 -0
  18. package/dist/server/static.d.ts +13 -0
  19. package/dist/vite-plugin.d.ts +12 -0
  20. package/dist/vite-plugin.js +58 -0
  21. package/package.json +24 -11
  22. package/src/cli/generate.ts +0 -138
  23. package/src/cli/serve.ts +0 -112
  24. package/src/config.ts +0 -34
  25. package/src/fixtures/catalog.ts +0 -99
  26. package/src/fixtures/codec.ts +0 -43
  27. package/src/fixtures/format.ts +0 -118
  28. package/src/fixtures/generate.ts +0 -209
  29. package/src/fixtures/matrix.ts +0 -94
  30. package/src/index.ts +0 -41
  31. package/src/server/live.ts +0 -220
  32. package/src/server/routes.ts +0 -141
  33. package/src/server/server.ts +0 -193
  34. package/src/server/static.ts +0 -83
  35. package/src/visual/analyze.ts +0 -234
  36. package/src/visual/dhash.ts +0 -76
  37. package/src/visual/index.ts +0 -18
  38. package/src/visual/report.ts +0 -80
  39. package/src/vite-plugin.d.ts +0 -10
  40. 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";
@@ -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";