@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow-player/test-streams",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "ffmpeg-driven local test stream service for Flow Player integration testing: generates and serves H.264/HEVC × HTTP-FLV / HLS / fMP4 / MP4 / MPEG-TS fixtures on 127.0.0.1:4177 (live infinite-loop + 5s VOD fixtures).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esm",
|
|
@@ -15,24 +15,37 @@
|
|
|
15
15
|
],
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"test-streams": "./dist/cli/serve.js",
|
|
20
|
+
"test-streams-generate": "./dist/cli/generate.js"
|
|
21
|
+
},
|
|
18
22
|
"exports": {
|
|
19
|
-
".":
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./server": {
|
|
28
|
+
"types": "./dist/server/server.d.ts",
|
|
29
|
+
"default": "./dist/server/server.js"
|
|
30
|
+
},
|
|
31
|
+
"./fixtures/generate": {
|
|
32
|
+
"types": "./dist/fixtures/generate.d.ts",
|
|
33
|
+
"default": "./dist/fixtures/generate.js"
|
|
34
|
+
},
|
|
35
|
+
"./fixtures/catalog": {
|
|
36
|
+
"types": "./dist/fixtures/catalog.d.ts",
|
|
37
|
+
"default": "./dist/fixtures/catalog.js"
|
|
38
|
+
},
|
|
23
39
|
"./vite-plugin": {
|
|
24
|
-
"types": "./
|
|
25
|
-
"default": "./
|
|
40
|
+
"types": "./dist/vite-plugin.d.ts",
|
|
41
|
+
"default": "./dist/vite-plugin.js"
|
|
26
42
|
}
|
|
27
43
|
},
|
|
28
44
|
"files": [
|
|
29
45
|
"README.md",
|
|
30
46
|
"LICENSE",
|
|
31
|
-
"
|
|
47
|
+
"dist/"
|
|
32
48
|
],
|
|
33
|
-
"engines": {
|
|
34
|
-
"node": ">=24.0.0"
|
|
35
|
-
},
|
|
36
49
|
"dependencies": {},
|
|
37
50
|
"publishConfig": {
|
|
38
51
|
"access": "public",
|
package/src/cli/generate.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
ALL_CODECS,
|
|
6
|
-
ALL_FORMATS,
|
|
7
|
-
DEFAULT_DURATION_SECONDS,
|
|
8
|
-
DEFAULT_FIXTURE_DIR,
|
|
9
|
-
type CodecId,
|
|
10
|
-
type FormatId,
|
|
11
|
-
} from "../config.ts";
|
|
12
|
-
import { generateFixtures } from "../fixtures/generate.ts";
|
|
13
|
-
|
|
14
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const repoRoot = resolve(here, "..", "..", "..");
|
|
16
|
-
|
|
17
|
-
const CODEC_SET: Record<string, true> = { h264: true, hevc: true };
|
|
18
|
-
const FORMAT_SET: Record<string, true> = { flv: true, hls: true, mp4: true, fmp4: true, ts: true };
|
|
19
|
-
|
|
20
|
-
type ParsedArgs = {
|
|
21
|
-
codecs: ReadonlyArray<CodecId>;
|
|
22
|
-
formats: ReadonlyArray<FormatId>;
|
|
23
|
-
durationSeconds: number;
|
|
24
|
-
fixtureDir: string;
|
|
25
|
-
overwrite: boolean;
|
|
26
|
-
help: boolean;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function parseArgs(argv: ReadonlyArray<string>): ParsedArgs {
|
|
30
|
-
const parsed: ParsedArgs = {
|
|
31
|
-
codecs: ALL_CODECS,
|
|
32
|
-
formats: ALL_FORMATS,
|
|
33
|
-
durationSeconds: DEFAULT_DURATION_SECONDS,
|
|
34
|
-
fixtureDir: resolve(here, "..", "..", DEFAULT_FIXTURE_DIR),
|
|
35
|
-
overwrite: false,
|
|
36
|
-
help: false,
|
|
37
|
-
};
|
|
38
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
39
|
-
const arg = argv[i];
|
|
40
|
-
switch (arg) {
|
|
41
|
-
case "--codecs": {
|
|
42
|
-
const value = argv[++i] ?? "";
|
|
43
|
-
const list = value
|
|
44
|
-
.split(",")
|
|
45
|
-
.map((s) => s.trim())
|
|
46
|
-
.filter((s): s is CodecId => CODEC_SET[s] === true);
|
|
47
|
-
if (list.length === 0) throw new Error(`--codecs invalid: ${value}`);
|
|
48
|
-
parsed.codecs = list;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case "--formats": {
|
|
52
|
-
const value = argv[++i] ?? "";
|
|
53
|
-
const list = value
|
|
54
|
-
.split(",")
|
|
55
|
-
.map((s) => s.trim())
|
|
56
|
-
.filter((s): s is FormatId => FORMAT_SET[s] === true);
|
|
57
|
-
if (list.length === 0) throw new Error(`--formats invalid: ${value}`);
|
|
58
|
-
parsed.formats = list;
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
case "--duration": {
|
|
62
|
-
const value = Number(argv[++i]);
|
|
63
|
-
if (!Number.isFinite(value) || value <= 0)
|
|
64
|
-
throw new Error(`--duration must be positive, got ${argv[i]}`);
|
|
65
|
-
parsed.durationSeconds = value;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
case "--fixture-dir": {
|
|
69
|
-
parsed.fixtureDir = resolve(argv[++i] ?? "");
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
case "--overwrite":
|
|
73
|
-
parsed.overwrite = true;
|
|
74
|
-
break;
|
|
75
|
-
case "--help":
|
|
76
|
-
case "-h":
|
|
77
|
-
parsed.help = true;
|
|
78
|
-
break;
|
|
79
|
-
default:
|
|
80
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return parsed;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const USAGE = `Usage:
|
|
87
|
-
pnpm --filter @flow-player/test-streams run generate [-- <args>]
|
|
88
|
-
|
|
89
|
-
Options:
|
|
90
|
-
--codecs <h264,hevc> Codecs to generate (default: h264,hevc)
|
|
91
|
-
--formats <flv,hls,mp4,fmp4,ts>
|
|
92
|
-
Container formats to generate (default: all 5)
|
|
93
|
-
--duration <seconds> Per-fixture duration (default: 5)
|
|
94
|
-
--fixture-dir <path> Output directory (default: packages/test-streams/fixtures)
|
|
95
|
-
--overwrite Re-run ffmpeg even if output exists
|
|
96
|
-
--help, -h Show this help
|
|
97
|
-
`;
|
|
98
|
-
|
|
99
|
-
async function main(): Promise<number> {
|
|
100
|
-
let args: ParsedArgs;
|
|
101
|
-
try {
|
|
102
|
-
args = parseArgs(process.argv.slice(2));
|
|
103
|
-
} catch (err) {
|
|
104
|
-
process.stderr.write(`${(err as Error).message}\n${USAGE}`);
|
|
105
|
-
return 1;
|
|
106
|
-
}
|
|
107
|
-
if (args.help) {
|
|
108
|
-
process.stdout.write(USAGE);
|
|
109
|
-
return 0;
|
|
110
|
-
}
|
|
111
|
-
const result = await generateFixtures({
|
|
112
|
-
codecs: args.codecs,
|
|
113
|
-
formats: args.formats,
|
|
114
|
-
durationSeconds: args.durationSeconds,
|
|
115
|
-
fixtureDir: args.fixtureDir,
|
|
116
|
-
overwrite: args.overwrite,
|
|
117
|
-
});
|
|
118
|
-
if (result.ffmpegMissing) {
|
|
119
|
-
process.stderr.write(
|
|
120
|
-
"ffmpeg not found in PATH. Install ffmpeg (brew install ffmpeg) and re-run.\n",
|
|
121
|
-
);
|
|
122
|
-
return 0;
|
|
123
|
-
}
|
|
124
|
-
process.stdout.write(
|
|
125
|
-
`Generated ${result.generated} fixture(s) under ${result.fixtureDir}\n` +
|
|
126
|
-
`Manifest: ${resolve(result.fixtureDir, "manifest.json")}\n`,
|
|
127
|
-
);
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
void repoRoot;
|
|
132
|
-
main().then(
|
|
133
|
-
(code) => process.exit(code),
|
|
134
|
-
(err) => {
|
|
135
|
-
process.stderr.write(`${(err as Error).message}\n`);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
},
|
|
138
|
-
);
|
package/src/cli/serve.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { DEFAULT_FIXTURE_DIR, DEFAULT_PORT, type ServeOptions } from "../config.ts";
|
|
5
|
-
import { createStreamsServer, listen } from "../server/server.ts";
|
|
6
|
-
|
|
7
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
type ParsedArgs = ServeOptions & { help: boolean };
|
|
10
|
-
|
|
11
|
-
function parseArgs(argv: ReadonlyArray<string>): ParsedArgs {
|
|
12
|
-
const parsed: ParsedArgs = {
|
|
13
|
-
port: DEFAULT_PORT,
|
|
14
|
-
fixtureDir: resolve(here, "..", "..", DEFAULT_FIXTURE_DIR),
|
|
15
|
-
help: false,
|
|
16
|
-
};
|
|
17
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
18
|
-
const arg = argv[i];
|
|
19
|
-
switch (arg) {
|
|
20
|
-
case "--port": {
|
|
21
|
-
const v = Number(argv[++i]);
|
|
22
|
-
if (!Number.isInteger(v) || v < 1 || v > 65_535)
|
|
23
|
-
throw new Error(`--port invalid: ${argv[i]}`);
|
|
24
|
-
parsed.port = v;
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
case "--host":
|
|
28
|
-
parsed.host = argv[++i];
|
|
29
|
-
break;
|
|
30
|
-
case "--fixture-dir":
|
|
31
|
-
parsed.fixtureDir = resolve(argv[++i] ?? "");
|
|
32
|
-
break;
|
|
33
|
-
case "--cert":
|
|
34
|
-
parsed.cert = argv[++i];
|
|
35
|
-
break;
|
|
36
|
-
case "--key":
|
|
37
|
-
parsed.key = argv[++i];
|
|
38
|
-
break;
|
|
39
|
-
case "--help":
|
|
40
|
-
case "-h":
|
|
41
|
-
parsed.help = true;
|
|
42
|
-
break;
|
|
43
|
-
default:
|
|
44
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return parsed;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const USAGE = `Usage:
|
|
51
|
-
pnpm --filter @flow-player/test-streams run serve [-- <args>]
|
|
52
|
-
|
|
53
|
-
Options:
|
|
54
|
-
--port <port> Listen port (default: 4177)
|
|
55
|
-
--host <host> Bind host (default: 127.0.0.1)
|
|
56
|
-
--fixture-dir <path> Fixture root (default: packages/test-streams/fixtures)
|
|
57
|
-
--cert <path> TLS cert (with --key, enables HTTPS)
|
|
58
|
-
--key <path> TLS private key (with --cert, enables HTTPS)
|
|
59
|
-
--help, -h Show this help
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
async function main(): Promise<number> {
|
|
63
|
-
let args: ParsedArgs;
|
|
64
|
-
try {
|
|
65
|
-
args = parseArgs(process.argv.slice(2));
|
|
66
|
-
} catch (err) {
|
|
67
|
-
process.stderr.write(`${(err as Error).message}\n${USAGE}`);
|
|
68
|
-
return 1;
|
|
69
|
-
}
|
|
70
|
-
if (args.help) {
|
|
71
|
-
process.stdout.write(USAGE);
|
|
72
|
-
return 0;
|
|
73
|
-
}
|
|
74
|
-
const server = createStreamsServer({
|
|
75
|
-
port: args.port,
|
|
76
|
-
host: args.host,
|
|
77
|
-
// parseArgs guarantees a default; treat as string.
|
|
78
|
-
fixtureDir: args.fixtureDir as string,
|
|
79
|
-
cert: args.cert,
|
|
80
|
-
key: args.key,
|
|
81
|
-
});
|
|
82
|
-
const listening = await listen(server);
|
|
83
|
-
const protocol = args.cert && args.key ? "https" : "http";
|
|
84
|
-
process.stdout.write(
|
|
85
|
-
`streams server listening on ${protocol}://${listening.host}:${listening.port}\n`,
|
|
86
|
-
);
|
|
87
|
-
process.stdout.write(
|
|
88
|
-
` health: ${protocol}://${listening.host}:${listening.port}/streams/health\n`,
|
|
89
|
-
);
|
|
90
|
-
process.stdout.write(` fixture: ${resolve(args.fixtureDir ?? "")}\n`);
|
|
91
|
-
// 等待 SIGINT / SIGTERM 或未捕获异常,再决定是否 exit。
|
|
92
|
-
const { promise, resolve: resolveWait } = Promise.withResolvers<number>();
|
|
93
|
-
const shutdown = (code: number): void => {
|
|
94
|
-
process.stdout.write("\nshutting down streams server...\n");
|
|
95
|
-
listening.close().finally(() => resolveWait(code));
|
|
96
|
-
};
|
|
97
|
-
process.once("SIGINT", () => shutdown(0));
|
|
98
|
-
process.once("SIGTERM", () => shutdown(0));
|
|
99
|
-
process.once("uncaughtException", (err) => {
|
|
100
|
-
process.stderr.write(`uncaught: ${err.stack ?? err.message}\n`);
|
|
101
|
-
shutdown(1);
|
|
102
|
-
});
|
|
103
|
-
return promise;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
void main().then(
|
|
107
|
-
(code) => process.exit(code),
|
|
108
|
-
(err) => {
|
|
109
|
-
process.stderr.write(`${(err as Error).message}\n`);
|
|
110
|
-
process.exit(1);
|
|
111
|
-
},
|
|
112
|
-
);
|
package/src/config.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Streams server / fixture defaults. Override per call when needed.
|
|
2
|
-
export const DEFAULT_PORT = 4177;
|
|
3
|
-
export const DEFAULT_HOST = "127.0.0.1";
|
|
4
|
-
export const DEFAULT_FIXTURE_DIR = "fixtures";
|
|
5
|
-
export const DEFAULT_DURATION_SECONDS = 5;
|
|
6
|
-
export const DEFAULT_VIDEO_WIDTH = 1280;
|
|
7
|
-
export const DEFAULT_VIDEO_HEIGHT = 720;
|
|
8
|
-
export const DEFAULT_VIDEO_FPS = 25;
|
|
9
|
-
export const DEFAULT_GOP_SECONDS = 1;
|
|
10
|
-
export const DEFAULT_HLS_SEGMENT_SECONDS = 2;
|
|
11
|
-
|
|
12
|
-
export type GenerateOptions = {
|
|
13
|
-
codecs?: ReadonlyArray<CodecId>;
|
|
14
|
-
formats?: ReadonlyArray<FormatId>;
|
|
15
|
-
durationSeconds?: number;
|
|
16
|
-
fixtureDir?: string;
|
|
17
|
-
overwrite?: boolean;
|
|
18
|
-
ffmpegPath?: string;
|
|
19
|
-
quiet?: boolean;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type ServeOptions = {
|
|
23
|
-
port?: number;
|
|
24
|
-
host?: string;
|
|
25
|
-
fixtureDir?: string;
|
|
26
|
-
cert?: string;
|
|
27
|
-
key?: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type CodecId = "h264" | "hevc";
|
|
31
|
-
export type FormatId = "flv" | "hls" | "mp4" | "fmp4" | "ts";
|
|
32
|
-
|
|
33
|
-
export const ALL_CODECS: ReadonlyArray<CodecId> = ["h264", "hevc"];
|
|
34
|
-
export const ALL_FORMATS: ReadonlyArray<FormatId> = ["flv", "hls", "mp4", "fmp4", "ts"];
|
package/src/fixtures/catalog.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// 读 manifest.json 并对外提供按 codec / format 维度的查询。
|
|
2
|
-
// 不依赖 ffmpeg;generate.ts 负责写 manifest,catalog 负责读 + 过滤。
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import { ALL_CODECS, ALL_FORMATS, type CodecId, type FormatId } from "../config.ts";
|
|
6
|
-
|
|
7
|
-
export type ManifestEntry = {
|
|
8
|
-
path: string;
|
|
9
|
-
size: number;
|
|
10
|
-
sha256: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type ManifestItem = {
|
|
14
|
-
codec: CodecId;
|
|
15
|
-
format: FormatId;
|
|
16
|
-
entries: ReadonlyArray<ManifestEntry>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type Manifest = {
|
|
20
|
-
version: 1;
|
|
21
|
-
generatedAt: string;
|
|
22
|
-
durationSeconds: number;
|
|
23
|
-
fixtures: ReadonlyArray<ManifestItem>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type FixtureLookup = {
|
|
27
|
-
files: ReadonlyArray<ManifestEntry>;
|
|
28
|
-
mainFile: ManifestEntry | null;
|
|
29
|
-
exists: boolean;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export function loadManifest(fixtureDir: string): Manifest | null {
|
|
33
|
-
const path = resolve(fixtureDir, "manifest.json");
|
|
34
|
-
if (!existsSync(path)) return null;
|
|
35
|
-
const raw = JSON.parse(readFileSync(path, "utf8")) as Manifest;
|
|
36
|
-
return raw;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function lookupFixture(
|
|
40
|
-
manifest: Manifest | null,
|
|
41
|
-
codec: CodecId,
|
|
42
|
-
format: FormatId,
|
|
43
|
-
): FixtureLookup {
|
|
44
|
-
if (!manifest) return { files: [], mainFile: null, exists: false };
|
|
45
|
-
const item = manifest.fixtures.find((f) => f.codec === codec && f.format === format);
|
|
46
|
-
if (!item) return { files: [], mainFile: null, exists: false };
|
|
47
|
-
// 主文件 = 第一个 entry(generate.ts 中 entries 顺序稳定:主文件优先)。
|
|
48
|
-
const mainFile = item.entries[0] ?? null;
|
|
49
|
-
return { files: item.entries, mainFile, exists: true };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type CatalogSummary = {
|
|
53
|
-
codecs: ReadonlyArray<CodecId>;
|
|
54
|
-
formats: ReadonlyArray<FormatId>;
|
|
55
|
-
fixtureCount: number;
|
|
56
|
-
exists: boolean;
|
|
57
|
-
reason?: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export function summarizeCatalog(fixtureDir: string): CatalogSummary {
|
|
61
|
-
const manifest = loadManifest(fixtureDir);
|
|
62
|
-
if (!manifest) {
|
|
63
|
-
return {
|
|
64
|
-
codecs: ALL_CODECS,
|
|
65
|
-
formats: ALL_FORMATS,
|
|
66
|
-
fixtureCount: 0,
|
|
67
|
-
exists: false,
|
|
68
|
-
reason: `manifest.json not found at ${resolve(fixtureDir, "manifest.json")}`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
let total = 0;
|
|
72
|
-
for (const item of manifest.fixtures) total += item.entries.length;
|
|
73
|
-
return {
|
|
74
|
-
codecs: ALL_CODECS,
|
|
75
|
-
formats: ALL_FORMATS,
|
|
76
|
-
fixtureCount: total,
|
|
77
|
-
exists: true,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 用 manifest 的 sha256 重新对磁盘文件做核对;供 generate.ts 写完磁盘后自检。
|
|
82
|
-
export function verifyEntryOnDisk(
|
|
83
|
-
fixtureDir: string,
|
|
84
|
-
entry: ManifestEntry,
|
|
85
|
-
): { ok: boolean; size: number; reason?: string } {
|
|
86
|
-
const abs = resolve(fixtureDir, entry.path);
|
|
87
|
-
if (!existsSync(abs)) {
|
|
88
|
-
return { ok: false, size: 0, reason: `missing file: ${entry.path}` };
|
|
89
|
-
}
|
|
90
|
-
const size = statSync(abs).size;
|
|
91
|
-
if (size !== entry.size) {
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
size,
|
|
95
|
-
reason: `size mismatch for ${entry.path}: manifest=${entry.size} disk=${size}`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return { ok: true, size };
|
|
99
|
-
}
|
package/src/fixtures/codec.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Per-codec ffmpeg 参数集。Centralized so generate.ts and any future
|
|
2
|
-
// probe code agree on a single source of truth。
|
|
3
|
-
import type { CodecId } from "../config.ts";
|
|
4
|
-
|
|
5
|
-
export type CodecSpec = {
|
|
6
|
-
codec: CodecId;
|
|
7
|
-
encoder: string;
|
|
8
|
-
// 通用视频参数(所有 format 都用)
|
|
9
|
-
videoArgs: ReadonlyArray<string>;
|
|
10
|
-
// 容器 tag(在 MP4 / fMP4 时需要 hvc1,FLV / TS 不需要)
|
|
11
|
-
audioArgs: ReadonlyArray<string>;
|
|
12
|
-
pixelFormat: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const CODEC_SPEC: Record<CodecId, CodecSpec> = {
|
|
16
|
-
h264: {
|
|
17
|
-
codec: "h264",
|
|
18
|
-
encoder: "libx264",
|
|
19
|
-
videoArgs: ["-profile:v", "high", "-preset", "veryfast"],
|
|
20
|
-
audioArgs: ["-c:a", "aac", "-b:a", "128k"],
|
|
21
|
-
pixelFormat: "yuv420p",
|
|
22
|
-
},
|
|
23
|
-
hevc: {
|
|
24
|
-
codec: "hevc",
|
|
25
|
-
encoder: "libx265",
|
|
26
|
-
// 注意:FLV / TS 容器用 enhanced-rtmp 1cvh tag,不能强行写 hvc1;
|
|
27
|
-
// MP4 / fMP4 / HLS 走 hvc1。containerArgs 在 buildPlan 时按 format 决定。
|
|
28
|
-
videoArgs: ["-preset", "veryfast"],
|
|
29
|
-
audioArgs: ["-c:a", "aac", "-b:a", "128k"],
|
|
30
|
-
pixelFormat: "yuv420p",
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export function containerExtraArgs(
|
|
35
|
-
format: "flv" | "hls" | "mp4" | "fmp4" | "ts",
|
|
36
|
-
codec: CodecId,
|
|
37
|
-
): ReadonlyArray<string> {
|
|
38
|
-
if (codec !== "hevc") return [];
|
|
39
|
-
// HEVC + MP4 / fMP4 / HLS:fMP4/HLS 内部 ts 段走 mpegts 复用,不需要 tag;
|
|
40
|
-
// 单纯 MP4 文件需要 -tag:v hvc1 才会被 QuickTime/Safari 识别。
|
|
41
|
-
if (format === "mp4") return ["-tag:v", "hvc1"];
|
|
42
|
-
return [];
|
|
43
|
-
}
|
package/src/fixtures/format.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// Per-format output plan. ffmpeg 参数 + 相对 fixture 根的输出路径。
|
|
2
|
-
// 单一事实源:matrix.ts 与 generate.ts 都从这份声明派生。
|
|
3
|
-
// 文件布局约定:
|
|
4
|
-
// flv/mp4/ts → {format}/{codec}.{format} 单文件,无 codec 子目录
|
|
5
|
-
// hls → hls/{codec}/master.m3u8 + seg-NN.ts
|
|
6
|
-
// fmp4 → fmp4/{codec}/init.mp4 + seg-NN.m4s
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import type { CodecId, FormatId } from "../config.ts";
|
|
9
|
-
|
|
10
|
-
export type FormatLayout = {
|
|
11
|
-
format: FormatId;
|
|
12
|
-
mime: string;
|
|
13
|
-
// 单文件格式:文件名(无父目录)。HLS/fMP4:目录内主文件名(master.m3u8/init.mp4)。
|
|
14
|
-
mainFileName: string;
|
|
15
|
-
// ffmpeg 参数,只描述容器层(不含 -c:v -c:a,见 codec.ts)。
|
|
16
|
-
containerArgs: ReadonlyArray<string>;
|
|
17
|
-
isContainerDirectory: boolean;
|
|
18
|
-
sideFiles: ReadonlyArray<string>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function formatSpecific(format: FormatId): ReadonlyArray<string> {
|
|
22
|
-
if (format === "flv") return ["-f", "flv"];
|
|
23
|
-
if (format === "hls") {
|
|
24
|
-
return [
|
|
25
|
-
"-f",
|
|
26
|
-
"hls",
|
|
27
|
-
"-hls_time",
|
|
28
|
-
"2",
|
|
29
|
-
"-hls_playlist_type",
|
|
30
|
-
"vod",
|
|
31
|
-
"-hls_segment_type",
|
|
32
|
-
"mpegts",
|
|
33
|
-
"-hls_flags",
|
|
34
|
-
"independent_segments",
|
|
35
|
-
];
|
|
36
|
-
}
|
|
37
|
-
if (format === "mp4") return ["-f", "mp4", "-movflags", "+faststart"];
|
|
38
|
-
if (format === "fmp4") {
|
|
39
|
-
return [
|
|
40
|
-
"-f",
|
|
41
|
-
"mp4",
|
|
42
|
-
"-movflags",
|
|
43
|
-
"frag_keyframe+empty_moov+default_base_moof",
|
|
44
|
-
"-frag_duration",
|
|
45
|
-
"2000000",
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
if (format === "ts") return ["-f", "mpegts"];
|
|
49
|
-
throw new Error(`Unknown format: ${String(format)}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function layoutFor(codec: CodecId, format: FormatId): FormatLayout {
|
|
53
|
-
if (format === "hls") {
|
|
54
|
-
return {
|
|
55
|
-
format,
|
|
56
|
-
mime: "application/vnd.apple.mpegurl",
|
|
57
|
-
mainFileName: "master.m3u8",
|
|
58
|
-
containerArgs: formatSpecific(format),
|
|
59
|
-
isContainerDirectory: true,
|
|
60
|
-
sideFiles: ["seg-00000.ts", "seg-00001.ts", "seg-00002.ts"],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
if (format === "fmp4") {
|
|
64
|
-
return {
|
|
65
|
-
format,
|
|
66
|
-
mime: "video/iso.segment",
|
|
67
|
-
mainFileName: "init.mp4",
|
|
68
|
-
containerArgs: formatSpecific(format),
|
|
69
|
-
isContainerDirectory: true,
|
|
70
|
-
sideFiles: ["seg-00000.m4s", "seg-00001.m4s", "seg-00002.m4s"],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (format === "flv") {
|
|
74
|
-
return {
|
|
75
|
-
format,
|
|
76
|
-
mime: "video/x-flv",
|
|
77
|
-
mainFileName: `${codec}.flv`,
|
|
78
|
-
containerArgs: formatSpecific(format),
|
|
79
|
-
isContainerDirectory: false,
|
|
80
|
-
sideFiles: [],
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (format === "mp4") {
|
|
84
|
-
return {
|
|
85
|
-
format,
|
|
86
|
-
mime: "video/mp4",
|
|
87
|
-
mainFileName: `${codec}.mp4`,
|
|
88
|
-
containerArgs: formatSpecific(format),
|
|
89
|
-
isContainerDirectory: false,
|
|
90
|
-
sideFiles: [],
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (format === "ts") {
|
|
94
|
-
return {
|
|
95
|
-
format,
|
|
96
|
-
mime: "video/mp2t",
|
|
97
|
-
mainFileName: `${codec}.ts`,
|
|
98
|
-
containerArgs: formatSpecific(format),
|
|
99
|
-
isContainerDirectory: false,
|
|
100
|
-
sideFiles: [],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
throw new Error(`Unknown format: ${String(format)}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 单文件格式的相对路径: {format}/{codec}.{format}
|
|
107
|
-
export function singleFileRelative(codec: CodecId, format: FormatId): string {
|
|
108
|
-
return join(format, `${codec}.${format}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// HLS / fMP4 子目录的相对路径: {format}/{codec}/...
|
|
112
|
-
export function containerRelativeDir(codec: CodecId, format: FormatId): string {
|
|
113
|
-
return join(format, codec);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function containerRelativePath(codec: CodecId, format: FormatId, name: string): string {
|
|
117
|
-
return join(containerRelativeDir(codec, format), name);
|
|
118
|
-
}
|