@flow-player/test-streams 0.6.0

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.
@@ -0,0 +1,99 @@
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
+ }
@@ -0,0 +1,43 @@
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
+ }
@@ -0,0 +1,118 @@
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
+ }
@@ -0,0 +1,209 @@
1
+ // ffmpeg 驱动的 fixture 生成器。按 codec × format 矩阵顺序执行 ffmpeg,
2
+ // 把每个 plan 的输出写到 fixtureDir,写完后输出 manifest.json。
3
+ // 设计:不引入 npm 依赖,直接 spawn ffmpeg 子进程。
4
+ import { createHash } from "node:crypto";
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ statSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { spawn } from "node:child_process";
15
+ import { join, resolve } from "node:path";
16
+ import {
17
+ ALL_CODECS,
18
+ ALL_FORMATS,
19
+ DEFAULT_DURATION_SECONDS,
20
+ DEFAULT_FIXTURE_DIR,
21
+ type GenerateOptions,
22
+ } from "../config.ts";
23
+ import { type FixturePlan, expandMatrix, VOD_SAMPLE_RELATIVE } from "./matrix.ts";
24
+ import { type Manifest, type ManifestEntry, type ManifestItem } from "./catalog.ts";
25
+
26
+ export type GenerateResult = {
27
+ fixtureDir: string;
28
+ manifest: Manifest;
29
+ generated: number;
30
+ skipped: number;
31
+ ffmpegPath: string;
32
+ ffmpegMissing: boolean;
33
+ };
34
+
35
+ const FFMPEG_PROBE_ARGS = ["-version"] as const;
36
+
37
+ export async function generateFixtures(opts: GenerateOptions = {}): Promise<GenerateResult> {
38
+ const ffmpegPath = opts.ffmpegPath ?? (await findFfmpeg());
39
+ if (!ffmpegPath) {
40
+ return {
41
+ fixtureDir: resolve(opts.fixtureDir ?? DEFAULT_FIXTURE_DIR),
42
+ manifest: emptyManifest(opts.durationSeconds ?? DEFAULT_DURATION_SECONDS),
43
+ generated: 0,
44
+ skipped: 0,
45
+ ffmpegPath: "ffmpeg",
46
+ ffmpegMissing: true,
47
+ };
48
+ }
49
+ const fixtureDir = resolve(opts.fixtureDir ?? DEFAULT_FIXTURE_DIR);
50
+ mkdirSync(fixtureDir, { recursive: true });
51
+ const duration = opts.durationSeconds ?? DEFAULT_DURATION_SECONDS;
52
+ const plans = expandMatrix({
53
+ codecs: opts.codecs ?? ALL_CODECS,
54
+ formats: opts.formats ?? ALL_FORMATS,
55
+ durationSeconds: duration,
56
+ });
57
+ const manifestItems: ManifestItem[] = [];
58
+ let generated = 0;
59
+ let skipped = 0;
60
+ for (const plan of plans) {
61
+ const item = await generateOne(
62
+ plan,
63
+ fixtureDir,
64
+ ffmpegPath,
65
+ opts.quiet ?? false,
66
+ opts.overwrite ?? false,
67
+ );
68
+ if (item) {
69
+ manifestItems.push(item);
70
+ generated += 1;
71
+ } else {
72
+ skipped += 1;
73
+ }
74
+ }
75
+ copyVodSamples(fixtureDir);
76
+ const manifest: Manifest = {
77
+ version: 1,
78
+ generatedAt: new Date().toISOString(),
79
+ durationSeconds: duration,
80
+ fixtures: manifestItems,
81
+ };
82
+ writeFileSync(join(fixtureDir, "manifest.json"), JSON.stringify(manifest, null, 2));
83
+ return { fixtureDir, manifest, generated, skipped, ffmpegPath, ffmpegMissing: false };
84
+ }
85
+
86
+ async function generateOne(
87
+ plan: FixturePlan,
88
+ fixtureDir: string,
89
+ ffmpegPath: string,
90
+ quiet: boolean,
91
+ overwrite: boolean,
92
+ ): Promise<ManifestItem | null> {
93
+ const outDir = join(fixtureDir, plan.containerDir);
94
+ mkdirSync(outDir, { recursive: true });
95
+ const outFile = join(fixtureDir, plan.mainFile);
96
+ if (!overwrite && existsSync(outFile)) {
97
+ return collectExisting(plan, fixtureDir);
98
+ }
99
+ if (plan.layout.isContainerDirectory) {
100
+ for (const f of readdirSync(outDir)) {
101
+ if (f === plan.layout.mainFileName) continue;
102
+ rmIfExists(join(outDir, f));
103
+ }
104
+ }
105
+ const args = plan.ffmpegOutputArgs.concat([outFile]);
106
+ if (!quiet) {
107
+ process.stdout.write(`[generate] ${plan.codec}/${plan.format} -> ${plan.mainFile}\n`);
108
+ }
109
+ const code = await runFfmpeg(ffmpegPath, args, quiet);
110
+ if (code !== 0) {
111
+ throw new Error(`ffmpeg exited with code ${code} for ${plan.codec}/${plan.format}`);
112
+ }
113
+ return collectExisting(plan, fixtureDir);
114
+ }
115
+
116
+ function collectExisting(plan: FixturePlan, fixtureDir: string): ManifestItem {
117
+ const entries: ManifestEntry[] = [];
118
+ for (const rel of plan.files) {
119
+ const abs = join(fixtureDir, rel);
120
+ if (!existsSync(abs)) continue;
121
+ entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
122
+ }
123
+ if (plan.layout.isContainerDirectory) {
124
+ // HLS / fMP4 容器目录内 ffmpeg 会按命名规则追加 segment(实际文件名不一定
125
+ // 等于 plan.files 预定义的 seg-00000.ts / seg-00000.m4s,可能形如 master0.ts)。
126
+ const absDir = join(fixtureDir, plan.containerDir);
127
+ if (existsSync(absDir)) {
128
+ for (const name of readdirSync(absDir).sort()) {
129
+ if (name === plan.layout.mainFileName) continue;
130
+ const rel = join(plan.containerDir, name);
131
+ if (entries.find((e) => e.path === rel)) continue;
132
+ const abs = join(absDir, name);
133
+ if (!statSync(abs).isFile()) continue;
134
+ entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
135
+ }
136
+ }
137
+ }
138
+ return { codec: plan.codec, format: plan.format, entries };
139
+ }
140
+
141
+ function copyVodSamples(fixtureDir: string): void {
142
+ const src = join(fixtureDir, VOD_SAMPLE_RELATIVE);
143
+ if (!existsSync(src)) return;
144
+ const vodDir = join(fixtureDir, "vod");
145
+ mkdirSync(vodDir, { recursive: true });
146
+ for (const name of ["sample-1.mp4", "sample-2.mp4"]) {
147
+ const dst = join(vodDir, name);
148
+ writeFileSync(dst, readFileSync(src));
149
+ }
150
+ }
151
+
152
+ function rmIfExists(abs: string): void {
153
+ if (!existsSync(abs)) return;
154
+ try {
155
+ unlinkSync(abs);
156
+ } catch {
157
+ // ignore
158
+ }
159
+ }
160
+
161
+ function runFfmpeg(
162
+ ffmpegPath: string,
163
+ args: ReadonlyArray<string>,
164
+ quiet: boolean,
165
+ ): Promise<number> {
166
+ const { promise, resolve, reject } = Promise.withResolvers<number>();
167
+ const proc = spawn(ffmpegPath, args, {
168
+ stdio: quiet ? ["ignore", "ignore", "ignore"] : "inherit",
169
+ });
170
+ proc.once("error", reject);
171
+ proc.once("exit", (code) => resolve(code ?? 1));
172
+ return promise;
173
+ }
174
+
175
+ async function findFfmpeg(): Promise<string | null> {
176
+ const candidates = [
177
+ "ffmpeg",
178
+ "/opt/homebrew/bin/ffmpeg",
179
+ "/usr/local/bin/ffmpeg",
180
+ "/usr/bin/ffmpeg",
181
+ ];
182
+ for (const candidate of candidates) {
183
+ if (await canRun(candidate)) return candidate;
184
+ }
185
+ return null;
186
+ }
187
+
188
+ async function canRun(path: string): Promise<boolean> {
189
+ const { promise, resolve } = Promise.withResolvers<boolean>();
190
+ const proc = spawn(path, FFMPEG_PROBE_ARGS as unknown as string[], { stdio: "ignore" });
191
+ proc.once("error", () => resolve(false));
192
+ proc.once("exit", (code) => resolve(code === 0));
193
+ return promise;
194
+ }
195
+
196
+ function hashFile(abs: string): string {
197
+ const hash = createHash("sha256");
198
+ hash.update(readFileSync(abs));
199
+ return hash.digest("hex");
200
+ }
201
+
202
+ function emptyManifest(duration: number): Manifest {
203
+ return {
204
+ version: 1,
205
+ generatedAt: new Date(0).toISOString(),
206
+ durationSeconds: duration,
207
+ fixtures: [],
208
+ };
209
+ }
@@ -0,0 +1,94 @@
1
+ // 完整的 codec × format 矩阵 + 单条 fixture plan。
2
+ // 仅依赖 codec.ts / format.ts / config.ts;不引入 ffmpeg,以便单测可脱机运行。
3
+ import { join } from "node:path";
4
+ import { ALL_CODECS, ALL_FORMATS, type CodecId, type FormatId } from "../config.ts";
5
+ import { CODEC_SPEC, containerExtraArgs } from "./codec.ts";
6
+ import {
7
+ containerRelativeDir,
8
+ containerRelativePath,
9
+ layoutFor,
10
+ singleFileRelative,
11
+ type FormatLayout,
12
+ } from "./format.ts";
13
+
14
+ export type FixturePlan = {
15
+ codec: CodecId;
16
+ format: FormatId;
17
+ layout: FormatLayout;
18
+ containerDir: string;
19
+ mainFile: string;
20
+ files: ReadonlyArray<string>;
21
+ ffmpegOutputArgs: ReadonlyArray<string>;
22
+ };
23
+
24
+ export function buildPlan(codec: CodecId, format: FormatId, durationSeconds: number): FixturePlan {
25
+ const layout = layoutFor(codec, format);
26
+ const spec = CODEC_SPEC[codec];
27
+ const gop = String(durationSeconds * 25);
28
+ const videoInput = `testsrc2=size=1280x720:rate=25,format=yuv420p`;
29
+ const audioInput = `sine=frequency=1000:sample_rate=44100`;
30
+
31
+ // 关键 GOP 锁:25 fps × duration,保证每个 fixture 至少一个 IDR。
32
+ const ffmpegOutputArgs = [
33
+ "-y",
34
+ "-f",
35
+ "lavfi",
36
+ "-i",
37
+ videoInput,
38
+ "-f",
39
+ "lavfi",
40
+ "-i",
41
+ audioInput,
42
+ "-t",
43
+ String(durationSeconds),
44
+ "-c:v",
45
+ spec.encoder,
46
+ ...spec.videoArgs,
47
+ "-pix_fmt",
48
+ spec.pixelFormat,
49
+ "-g",
50
+ gop,
51
+ "-keyint_min",
52
+ gop,
53
+ "-sc_threshold",
54
+ "0",
55
+ ...containerExtraArgs(format, codec),
56
+ ...spec.audioArgs,
57
+ ...layout.containerArgs,
58
+ ];
59
+
60
+ if (layout.isContainerDirectory) {
61
+ const containerDir = containerRelativeDir(codec, format);
62
+ const mainFile = containerRelativePath(codec, format, layout.mainFileName);
63
+ const files = [
64
+ mainFile,
65
+ ...layout.sideFiles.map((s) => containerRelativePath(codec, format, s)),
66
+ ];
67
+ return { codec, format, layout, containerDir, mainFile, files, ffmpegOutputArgs };
68
+ }
69
+ // 单文件格式
70
+ const mainFile = singleFileRelative(codec, format);
71
+ const containerDir = join(mainFile.split("/").slice(0, -1).join("/"));
72
+ return { codec, format, layout, containerDir, mainFile, files: [mainFile], ffmpegOutputArgs };
73
+ }
74
+
75
+ export function expandMatrix(opts?: {
76
+ codecs?: ReadonlyArray<CodecId>;
77
+ formats?: ReadonlyArray<FormatId>;
78
+ durationSeconds?: number;
79
+ }): ReadonlyArray<FixturePlan> {
80
+ const codecs = opts?.codecs ?? ALL_CODECS;
81
+ const formats = opts?.formats ?? ALL_FORMATS;
82
+ const duration = opts?.durationSeconds ?? 5;
83
+ const plans: FixturePlan[] = [];
84
+ for (const codec of codecs) {
85
+ for (const format of formats) {
86
+ plans.push(buildPlan(codec, format, duration));
87
+ }
88
+ }
89
+ return plans;
90
+ }
91
+
92
+ // VOD 段默认从 mp4/h264.mp4 派生(同 fixture)。generate.ts 在生成结束后
93
+ // 把真实 fileSize 写进 vodList.example.json;此处只描述派生约定。
94
+ export const VOD_SAMPLE_RELATIVE = "mp4/h264.mp4";
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ // 公共 API 入口。generate / serve / catalog / server 各模块在此 re-export。
2
+ export {
3
+ buildPlan,
4
+ expandMatrix,
5
+ VOD_SAMPLE_RELATIVE,
6
+ type FixturePlan,
7
+ } from "./fixtures/matrix.ts";
8
+ export {
9
+ loadManifest,
10
+ lookupFixture,
11
+ summarizeCatalog,
12
+ verifyEntryOnDisk,
13
+ type CatalogSummary,
14
+ type FixtureLookup,
15
+ type Manifest,
16
+ type ManifestEntry,
17
+ type ManifestItem,
18
+ } from "./fixtures/catalog.ts";
19
+ export { CODEC_SPEC, type CodecSpec } from "./fixtures/codec.ts";
20
+ export {
21
+ layoutFor,
22
+ containerRelativeDir,
23
+ containerRelativePath,
24
+ type FormatLayout,
25
+ } from "./fixtures/format.ts";
26
+ export {
27
+ ALL_CODECS,
28
+ ALL_FORMATS,
29
+ DEFAULT_DURATION_SECONDS,
30
+ DEFAULT_FIXTURE_DIR,
31
+ DEFAULT_HLS_SEGMENT_SECONDS,
32
+ DEFAULT_HOST,
33
+ DEFAULT_PORT,
34
+ type CodecId,
35
+ type FormatId,
36
+ type GenerateOptions,
37
+ type ServeOptions,
38
+ } from "./config.ts";
39
+ export { createStreamsServer, type StreamsServerHandle } from "./server/server.ts";
40
+ export { generateFixtures, type GenerateResult } from "./fixtures/generate.ts";
41
+ export { handleLiveStream, matchLivePath, type LiveStreamOptions } from "./server/live.ts";