@flow-player/test-streams 0.8.0 → 0.10.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.
package/README.md CHANGED
@@ -43,6 +43,13 @@ export default {
43
43
  };
44
44
  ```
45
45
 
46
+ ## 发布形态差异
47
+
48
+ npm 发布产物由 `scripts/build-package.ts` 生成:esbuild 编译出 `dist/*.js`,`files` 仅含 `README.md / LICENSE / dist/`,**不含 `fixtures/` 产物**;仓库内 `packages/test-streams/fixtures/` 也被 `.gitignore` 忽略(生成产物不入库)。而 `test-streams`(serve)bin 的默认 fixture 目录按其自身位置解析为 `<包根>/fixtures`,因此裸跑 `test-streams` serve 时该目录是空的:`/streams/health` 回 503、VOD 路由 404(live 流不依赖 fixture,仍可正常播)。裸跑前二选一:
49
+
50
+ 1. 先跑 `test-streams-generate` 生成完整矩阵 + `manifest.json`(系统需有 ffmpeg;建议用 `--fixture-dir` 指到项目内目录,避免写入 `node_modules`);或
51
+ 2. 用 `--fixture-dir <path>` 显式指向一个已有 fixture 目录。
52
+
46
53
  ## 注意
47
54
 
48
55
  - 内置矩阵、CLI 参数与路径语义与 [仓库文档](../../docs/reference/test-stream.md) 保持一致;**不要在业务代码里硬编码流地址**——demo 侧经 `VITE_FLOW_PLAYER_DEMO_STREAMS_BASE` 切换基址。
@@ -365,7 +365,6 @@ function emptyManifest(duration) {
365
365
 
366
366
  // src/cli/generate.ts
367
367
  var here = dirname(fileURLToPath(import.meta.url));
368
- var repoRoot = resolve2(here, "..", "..", "..");
369
368
  var CODEC_SET = { h264: true, hevc: true };
370
369
  var FORMAT_SET = { flv: true, hls: true, mp4: true, fmp4: true, ts: true };
371
370
  function parseArgs(argv) {
@@ -405,6 +404,9 @@ function parseArgs(argv) {
405
404
  parsed.fixtureDir = resolve2(argv[++i] ?? "");
406
405
  break;
407
406
  }
407
+ case "--ffmpeg-path":
408
+ parsed.ffmpegPath = argv[++i] ?? "";
409
+ break;
408
410
  case "--overwrite":
409
411
  parsed.overwrite = true;
410
412
  break;
@@ -427,6 +429,7 @@ Options:
427
429
  Container formats to generate (default: all 5)
428
430
  --duration <seconds> Per-fixture duration (default: 5)
429
431
  --fixture-dir <path> Output directory (default: packages/test-streams/fixtures)
432
+ --ffmpeg-path <path> Explicit ffmpeg executable (default: auto-detect on PATH)
430
433
  --overwrite Re-run ffmpeg even if output exists
431
434
  --help, -h Show this help
432
435
  `;
@@ -448,7 +451,8 @@ ${USAGE}`);
448
451
  formats: args.formats,
449
452
  durationSeconds: args.durationSeconds,
450
453
  fixtureDir: args.fixtureDir,
451
- overwrite: args.overwrite
454
+ overwrite: args.overwrite,
455
+ ffmpegPath: args.ffmpegPath
452
456
  });
453
457
  if (result.ffmpegMissing) {
454
458
  process.stderr.write(
package/dist/cli/serve.js CHANGED
@@ -25,8 +25,12 @@ import { resolve } from "node:path";
25
25
  function loadManifest(fixtureDir) {
26
26
  const path = resolve(fixtureDir, "manifest.json");
27
27
  if (!existsSync(path)) return null;
28
- const raw = JSON.parse(readFileSync(path, "utf8"));
29
- return raw;
28
+ try {
29
+ return JSON.parse(readFileSync(path, "utf8"));
30
+ } catch (err) {
31
+ const reason = err instanceof Error ? err.message : String(err);
32
+ throw new Error(`failed to parse manifest at ${path}: ${reason}`);
33
+ }
30
34
  }
31
35
  function summarizeCatalog(fixtureDir) {
32
36
  const manifest = loadManifest(fixtureDir);
@@ -722,7 +726,20 @@ ${USAGE}`);
722
726
  cert: args.cert,
723
727
  key: args.key
724
728
  });
725
- const listening = await listen(server);
729
+ let listening;
730
+ try {
731
+ listening = await listen(server);
732
+ } catch (err) {
733
+ if (err.code === "EADDRINUSE") {
734
+ process.stderr.write(
735
+ `streams server \u542F\u52A8\u5931\u8D25\uFF1A\u7AEF\u53E3 ${server.port} \u5DF2\u88AB\u5360\u7528\uFF08EADDRINUSE\uFF09\u3002
736
+ \u8BF7\u505C\u6389\u5360\u7528\u8BE5\u7AEF\u53E3\u7684\u8FDB\u7A0B\uFF0C\u6216\u6362\u4E00\u4E2A\u7AEF\u53E3\u91CD\u8BD5\uFF0C\u4F8B\u5982\uFF1A--port ${Number(server.port) + 1}
737
+ `
738
+ );
739
+ return 1;
740
+ }
741
+ throw err;
742
+ }
726
743
  const protocol = args.cert && args.key ? "https" : "http";
727
744
  process.stdout.write(
728
745
  `streams server listening on ${protocol}://${listening.host}:${listening.port}
package/dist/config.d.ts CHANGED
@@ -1,21 +1,28 @@
1
+ /** streams server 默认监听端口(4177)。 */
1
2
  export declare const DEFAULT_PORT = 4177;
3
+ /** streams server 默认绑定主机(127.0.0.1,仅本机可达)。 */
2
4
  export declare const DEFAULT_HOST = "127.0.0.1";
5
+ /** fixture 根目录默认名(相对包根解析)。 */
3
6
  export declare const DEFAULT_FIXTURE_DIR = "fixtures";
7
+ /** 每条 VOD fixture 的默认时长(秒)。 */
4
8
  export declare const DEFAULT_DURATION_SECONDS = 5;
5
9
  export declare const DEFAULT_VIDEO_WIDTH = 1280;
6
10
  export declare const DEFAULT_VIDEO_HEIGHT = 720;
7
11
  export declare const DEFAULT_VIDEO_FPS = 25;
8
12
  export declare const DEFAULT_GOP_SECONDS = 1;
9
13
  export declare const DEFAULT_HLS_SEGMENT_SECONDS = 2;
14
+ /** generateFixtures 的选项;全部可选,缺省即完整矩阵 + 默认时长。 */
10
15
  export type GenerateOptions = {
11
16
  codecs?: ReadonlyArray<CodecId>;
12
17
  formats?: ReadonlyArray<FormatId>;
13
18
  durationSeconds?: number;
14
19
  fixtureDir?: string;
15
20
  overwrite?: boolean;
21
+ /** ffmpeg 可执行文件路径;缺省自动探测 PATH 与常见安装位置。 */
16
22
  ffmpegPath?: string;
17
23
  quiet?: boolean;
18
24
  };
25
+ /** createStreamsServer / serve CLI 的选项;fixtureDir 必填(server 侧)。 */
19
26
  export type ServeOptions = {
20
27
  port?: number;
21
28
  host?: string;
@@ -23,7 +30,11 @@ export type ServeOptions = {
23
30
  cert?: string;
24
31
  key?: string;
25
32
  };
33
+ /** 支持的视频编码 id。 */
26
34
  export type CodecId = "h264" | "hevc";
35
+ /** 支持的容器格式 id。 */
27
36
  export type FormatId = "flv" | "hls" | "mp4" | "fmp4" | "ts";
37
+ /** 全量编码矩阵(h264、hevc)。 */
28
38
  export declare const ALL_CODECS: ReadonlyArray<CodecId>;
39
+ /** 全量容器矩阵(flv、hls、mp4、fmp4、ts)。 */
29
40
  export declare const ALL_FORMATS: ReadonlyArray<FormatId>;
@@ -1,35 +1,53 @@
1
1
  import { type CodecId, type FormatId } from "../config.js";
2
+ /** manifest 内单条文件记录:相对 fixtureDir 的路径 + 字节数 + sha256。 */
2
3
  export type ManifestEntry = {
3
4
  path: string;
4
5
  size: number;
5
6
  sha256: string;
6
7
  };
8
+ /** 同一 codec × format 组合下的全部文件;generate.ts 保证主文件在 entries[0]。 */
7
9
  export type ManifestItem = {
8
10
  codec: CodecId;
9
11
  format: FormatId;
10
12
  entries: ReadonlyArray<ManifestEntry>;
11
13
  };
14
+ /** manifest.json 顶层结构:generate.ts 负责写,catalog 负责读 + 过滤。 */
12
15
  export type Manifest = {
13
16
  version: 1;
14
17
  generatedAt: string;
15
18
  durationSeconds: number;
16
19
  fixtures: ReadonlyArray<ManifestItem>;
17
20
  };
21
+ /** lookupFixture 的查询结果:该组合的文件清单与主文件。 */
18
22
  export type FixtureLookup = {
19
23
  files: ReadonlyArray<ManifestEntry>;
24
+ /** 该组合的主文件(entries[0]);无 manifest / 无该组合时为 null。 */
20
25
  mainFile: ManifestEntry | null;
21
26
  exists: boolean;
22
27
  };
28
+ /**
29
+ * 读取 `fixtureDir/manifest.json`;文件不存在时返回 null。
30
+ * 文件存在但 JSON 损坏时抛出新的 Error(消息含 manifest 绝对路径与底层解析
31
+ * 错误),不透传原始 SyntaxError——createStreamsServer 构造期即可定位坏 manifest。
32
+ */
23
33
  export declare function loadManifest(fixtureDir: string): Manifest | null;
34
+ /** 按 codec × format 查 manifest;无 manifest 或无该组合时 exists=false、files 为空数组。 */
24
35
  export declare function lookupFixture(manifest: Manifest | null, codec: CodecId, format: FormatId): FixtureLookup;
36
+ /** summarizeCatalog 的汇总结果:支持的 codec/format 全集 + fixture 文件总数。 */
25
37
  export type CatalogSummary = {
26
38
  codecs: ReadonlyArray<CodecId>;
27
39
  formats: ReadonlyArray<FormatId>;
28
40
  fixtureCount: number;
29
41
  exists: boolean;
42
+ /** manifest 缺失时的诊断信息(指明期望的 manifest.json 路径)。 */
30
43
  reason?: string;
31
44
  };
45
+ /** 汇总 fixture 目录状态:manifest 缺失时 exists=false 且 reason 指明路径;否则统计全部 entries 总数。 */
32
46
  export declare function summarizeCatalog(fixtureDir: string): CatalogSummary;
47
+ /**
48
+ * 用 manifest 记录核对磁盘文件的存在性与 size(sha256 留给调用方按需重算);
49
+ * 不一致时 ok=false 并给出 reason,供 generate.ts 写完磁盘后自检。
50
+ */
33
51
  export declare function verifyEntryOnDisk(fixtureDir: string, entry: ManifestEntry): {
34
52
  ok: boolean;
35
53
  size: number;
@@ -10,8 +10,12 @@ var ALL_FORMATS = ["flv", "hls", "mp4", "fmp4", "ts"];
10
10
  function loadManifest(fixtureDir) {
11
11
  const path = resolve(fixtureDir, "manifest.json");
12
12
  if (!existsSync(path)) return null;
13
- const raw = JSON.parse(readFileSync(path, "utf8"));
14
- return raw;
13
+ try {
14
+ return JSON.parse(readFileSync(path, "utf8"));
15
+ } catch (err) {
16
+ const reason = err instanceof Error ? err.message : String(err);
17
+ throw new Error(`failed to parse manifest at ${path}: ${reason}`);
18
+ }
15
19
  }
16
20
  function lookupFixture(manifest, codec, format) {
17
21
  if (!manifest) return { files: [], mainFile: null, exists: false };
@@ -6,5 +6,7 @@ export type CodecSpec = {
6
6
  audioArgs: ReadonlyArray<string>;
7
7
  pixelFormat: string;
8
8
  };
9
+ /** 按 codec 集中的 ffmpeg 编码参数集;generate.ts 与 live.ts 共用的单一事实源。 */
9
10
  export declare const CODEC_SPEC: Record<CodecId, CodecSpec>;
11
+ /** 按 format/codec 追加容器层参数(目前仅 HEVC + MP4 需要 `-tag:v hvc1` 供 QuickTime/Safari 识别)。 */
10
12
  export declare function containerExtraArgs(format: "flv" | "hls" | "mp4" | "fmp4" | "ts", codec: CodecId): ReadonlyArray<string>;
@@ -7,7 +7,11 @@ export type FormatLayout = {
7
7
  isContainerDirectory: boolean;
8
8
  sideFiles: ReadonlyArray<string>;
9
9
  };
10
+ /** 查询某 codec × format 的输出布局(主文件名、容器参数、是否目录型),单文件与容器目录两种形态。 */
10
11
  export declare function layoutFor(codec: CodecId, format: FormatId): FormatLayout;
12
+ /** 单文件格式(flv/mp4/ts)的相对路径:`{format}/{codec}.{format}`。 */
11
13
  export declare function singleFileRelative(codec: CodecId, format: FormatId): string;
14
+ /** HLS / fMP4 容器子目录的相对路径:`{format}/{codec}/`。 */
12
15
  export declare function containerRelativeDir(codec: CodecId, format: FormatId): string;
16
+ /** 拼容器目录内文件的相对路径:`{format}/{codec}/{name}`。 */
13
17
  export declare function containerRelativePath(codec: CodecId, format: FormatId, name: string): string;
@@ -1,11 +1,26 @@
1
1
  import { type GenerateOptions } from "../config.js";
2
2
  import { type Manifest } from "./catalog.js";
3
+ /** generateFixtures 的结果:落盘目录、写好的 manifest 与本次统计。 */
3
4
  export type GenerateResult = {
5
+ /** fixture 根目录(绝对路径),manifest.json 写在其下。 */
4
6
  fixtureDir: string;
7
+ /** 本次写盘的 manifest:每个 codec × format 组合一条,entries 带 size/sha256。 */
5
8
  manifest: Manifest;
9
+ /** 本次实际跑 ffmpeg 生成(或收集已有输出元信息)的组合数。 */
6
10
  generated: number;
11
+ /** 输出已存在且未开 overwrite 而跳过的组合数。 */
7
12
  skipped: number;
13
+ /** 实际使用的 ffmpeg 可执行路径。 */
8
14
  ffmpegPath: string;
15
+ /** 系统找不到可运行的 ffmpeg 时为 true;此时不写盘、manifest 为空壳。 */
9
16
  ffmpegMissing: boolean;
10
17
  };
18
+ /**
19
+ * 按 codec(外层)× format(内层)的矩阵顺序逐项 spawn ffmpeg 生成短时
20
+ * fixture,输出写入 fixtureDir;全部完成后把带 size/sha256 的 manifest.json
21
+ * 写到 `fixtureDir/manifest.json`。输出已存在且未开 overwrite 的组合跳过生成,
22
+ * 但仍从磁盘收集条目进 manifest;hls/fmp4 容器目录内 ffmpeg 实际产出的段文件
23
+ * 一并收编。开头还会从 mp4/h264.mp4 复制出 vod/sample-*.mp4 样例。
24
+ * 找不到 ffmpeg 时不创建目录、不写盘,返回 ffmpegMissing=true 的空结果。
25
+ */
11
26
  export declare function generateFixtures(opts?: GenerateOptions): Promise<GenerateResult>;
@@ -1,18 +1,29 @@
1
1
  import { type CodecId, type FormatId } from "../config.js";
2
2
  import { type FormatLayout } from "./format.js";
3
+ /** 单条 fixture 的生成计划:目标路径布局 + 一次 ffmpeg 调用所需的全部参数。 */
3
4
  export type FixturePlan = {
4
5
  codec: CodecId;
5
6
  format: FormatId;
6
7
  layout: FormatLayout;
8
+ /** 该组合的输出目录(相对 fixtureDir;单文件格式为其父目录)。 */
7
9
  containerDir: string;
10
+ /** 主文件相对路径(flv/mp4/ts 为单文件;hls 为 master.m3u8、fmp4 为 init.mp4)。 */
8
11
  mainFile: string;
12
+ /** 全部预期输出文件的相对路径(主文件在前)。 */
9
13
  files: ReadonlyArray<string>;
14
+ /** 传给 ffmpeg 的输出侧参数(不含最后的输出文件名)。 */
10
15
  ffmpegOutputArgs: ReadonlyArray<string>;
11
16
  };
17
+ /** 为一个 codec × format 组合构建生成计划;GOP 锁定为 durationSeconds × 25,保证至少一个 IDR。 */
12
18
  export declare function buildPlan(codec: CodecId, format: FormatId, durationSeconds: number): FixturePlan;
19
+ /** 展开完整 codec × format 矩阵为有序 plan 列表(codec 外层、format 内层),供 generateFixtures 顺序执行。 */
13
20
  export declare function expandMatrix(opts?: {
14
21
  codecs?: ReadonlyArray<CodecId>;
15
22
  formats?: ReadonlyArray<FormatId>;
16
23
  durationSeconds?: number;
17
24
  }): ReadonlyArray<FixturePlan>;
25
+ /**
26
+ * VOD 样例段的派生源:mp4/h264.mp4。generate.ts 在生成结束后把它复制为
27
+ * vod/sample-1.mp4、vod/sample-2.mp4,供 /streams/vod-list 的两段回退响应使用。
28
+ */
18
29
  export declare const VOD_SAMPLE_RELATIVE = "mp4/h264.mp4";
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export { ALL_CODECS, ALL_FORMATS, DEFAULT_DURATION_SECONDS, DEFAULT_FIXTURE_DIR,
6
6
  export { createStreamsServer, type StreamsServerHandle } from "./server/server.js";
7
7
  export { generateFixtures, type GenerateResult } from "./fixtures/generate.js";
8
8
  export { handleLiveStream, matchLivePath, type LiveStreamOptions } from "./server/live.js";
9
+ export { liveStreamUrl, vodStreamUrl } from "./urls.js";
package/dist/index.js CHANGED
@@ -197,8 +197,12 @@ import { resolve } from "node:path";
197
197
  function loadManifest(fixtureDir) {
198
198
  const path = resolve(fixtureDir, "manifest.json");
199
199
  if (!existsSync(path)) return null;
200
- const raw = JSON.parse(readFileSync(path, "utf8"));
201
- return raw;
200
+ try {
201
+ return JSON.parse(readFileSync(path, "utf8"));
202
+ } catch (err) {
203
+ const reason = err instanceof Error ? err.message : String(err);
204
+ throw new Error(`failed to parse manifest at ${path}: ${reason}`);
205
+ }
202
206
  }
203
207
  function lookupFixture(manifest, codec, format) {
204
208
  if (!manifest) return { files: [], mainFile: null, exists: false };
@@ -982,6 +986,20 @@ function emptyManifest(duration) {
982
986
  fixtures: []
983
987
  };
984
988
  }
989
+
990
+ // src/urls.ts
991
+ function joinBase(base, path) {
992
+ return `${base.replace(/\/+$/, "")}${path}`;
993
+ }
994
+ function vodStreamUrl(base, format, codec) {
995
+ const root = joinBase(base, `/streams/${format}/${codec}`);
996
+ if (format === "hls") return `${root}/master.m3u8`;
997
+ if (format === "fmp4") return `${root}/init.mp4`;
998
+ return root;
999
+ }
1000
+ function liveStreamUrl(base, format, codec) {
1001
+ return joinBase(base, `/streams/live/${format}/${codec}`);
1002
+ }
985
1003
  export {
986
1004
  ALL_CODECS,
987
1005
  ALL_FORMATS,
@@ -1000,9 +1018,11 @@ export {
1000
1018
  generateFixtures,
1001
1019
  handleLiveStream,
1002
1020
  layoutFor,
1021
+ liveStreamUrl,
1003
1022
  loadManifest,
1004
1023
  lookupFixture,
1005
1024
  matchLivePath,
1006
1025
  summarizeCatalog,
1007
- verifyEntryOnDisk
1026
+ verifyEntryOnDisk,
1027
+ vodStreamUrl
1008
1028
  };
@@ -1,14 +1,36 @@
1
1
  import { type IncomingMessage, type ServerResponse } from "node:http";
2
2
  import { type CodecId, type FormatId } from "../config.js";
3
+ /** 解析 `/streams/live/<format>/<codec>` 路径;format/codec 不在支持集内时返回 null。 */
3
4
  export declare function matchLivePath(path: string): {
4
5
  format: FormatId;
5
6
  codec: CodecId;
6
7
  } | null;
8
+ /** handleLiveStream 的可调参数;缺省值即本地验收基线(1280x720 @ 25fps、1500k)。 */
7
9
  export type LiveStreamOptions = {
10
+ /** ffmpeg 可执行文件路径;缺省用 PATH 上的 `ffmpeg`。 */
8
11
  ffmpegPath?: string;
12
+ /** 视频宽度,默认 1280。 */
9
13
  width?: number;
14
+ /** 视频高度,默认 720。 */
10
15
  height?: number;
16
+ /** 帧率,默认 25。 */
11
17
  fps?: number;
18
+ /** 视频码率(ffmpeg `-b:v` 取值),默认 "1500k"。 */
12
19
  bitrate?: string;
13
20
  };
21
+ /**
22
+ * 处理 `/streams/live/<format>/<codec>` 请求:每个请求按需 spawn 一个
23
+ * `ffmpeg -stream_loop -1 -re` 子进程(lavfi testsrc2 + sine 音源),把 stdout
24
+ * 管道接到 HTTP response,流永不 EOF,模拟 NVR 无限 live。
25
+ *
26
+ * 进程生命周期与诊断语义:
27
+ * - 每个客户端请求独享一个 ffmpeg 进程;客户端断开(req/res 任一 close)即对该
28
+ * 进程发 SIGKILL 清理,不残留后台进程。
29
+ * - spawn 同步抛错或 ffmpeg 进程启动失败:回 503 JSON(error/reason 字段)。
30
+ * - stderr 尾部 2000 字节持续收集;若 ffmpeg 在产出首字节前以非 0 码退出,
31
+ * 用 stderr 最后一行(外加 HEVC in FLV/TS 需 ffmpeg ≥ 6 enhanced-rtmp 的提示)
32
+ * 回 503,而不是 200 + 0 字节静默挂起。
33
+ * - 响应头等到 ffmpeg stdout 首字节到达才发出,避免初始化慢时客户端空等假头。
34
+ * - 非 GET/HEAD 回 405;hls live 尚未支持回 501;HEAD 只回响应头、不 spawn ffmpeg。
35
+ */
14
36
  export declare function handleLiveStream(req: IncomingMessage, res: ServerResponse, format: FormatId, codec: CodecId, options?: LiveStreamOptions): void;
@@ -1,18 +1,47 @@
1
1
  import { type Server } from "node:http";
2
2
  import { type Manifest } from "../fixtures/catalog.js";
3
+ /** createStreamsServer 返回的服务句柄:包装底层 http(s).Server 与 fixture 目录上下文。 */
3
4
  export type StreamsServerHandle = {
5
+ /** 底层 Node HTTP(S) server 实例,可供调用方挂接额外事件。 */
4
6
  server: Server;
7
+ /** 监听端口;listen() 成功后会回写为内核实际绑定的端口(传 0 自动分配时尤为有用)。 */
5
8
  port: number;
9
+ /** 绑定主机(默认 DEFAULT_HOST,即 127.0.0.1)。 */
6
10
  host: string;
11
+ /** 停止接受新连接并等待现有连接排空;关闭出错时 reject。 */
7
12
  close: () => Promise<void>;
13
+ /** 重新从磁盘加载 fixtureDir/manifest.json 并更新内部缓存(生成器跑完后刷新用);manifest 缺失时返回 null。 */
8
14
  refreshManifest: () => Manifest | null;
9
15
  };
16
+ /** createStreamsServer 的配置项。 */
10
17
  export type StreamsServerOptions = {
18
+ /** 监听端口,默认 DEFAULT_PORT(4177)。 */
11
19
  port?: number;
20
+ /** 绑定主机,默认 DEFAULT_HOST(127.0.0.1)。 */
12
21
  host?: string;
22
+ /** fixture 根目录(内含生成器产物 manifest.json),必填。 */
13
23
  fixtureDir: string;
24
+ /** TLS 证书路径;与 key 同时提供时启用 HTTPS。 */
14
25
  cert?: string;
26
+ /** TLS 私钥路径;与 cert 同时提供时启用 HTTPS。 */
15
27
  key?: string;
16
28
  };
29
+ /**
30
+ * 创建本地测试流 server(只构造不监听,监听需再调 {@link listen})。
31
+ *
32
+ * 构造期同步读取 `fixtureDir/manifest.json`:缺失时以 null manifest 启动
33
+ * (/streams/health 回 503、VOD 路由 404),生成器跑完后可调
34
+ * handle.refreshManifest() 刷新。流路径约定:
35
+ * - `/streams/live/<format>/<codec>`:按需 spawn ffmpeg 的无限循环 live 流;
36
+ * - `/streams/<format>/<codec>`:5s 预生成 VOD fixture(hls/fmp4 为容器目录布局,
37
+ * 须带主文件子路径 `/streams/hls/<codec>/master.m3u8`、`/streams/fmp4/<codec>/init.mp4`)。
38
+ *
39
+ * manifest.json 损坏(JSON 解析失败)会直接抛错,由调用方在构造期感知。
40
+ */
17
41
  export declare function createStreamsServer(options: StreamsServerOptions): StreamsServerHandle;
42
+ /**
43
+ * 让 handle 开始监听其 port/host;成功时 resolve 同一 handle,且 handle.port
44
+ * 已回写为内核实际绑定的端口。监听出错(典型如端口被占用的 EADDRINUSE)时
45
+ * 返回的 promise reject,提示方式由调用方决定(CLI 侧有友好化输出)。
46
+ */
18
47
  export declare function listen(server: StreamsServerHandle): Promise<StreamsServerHandle>;
@@ -18,8 +18,12 @@ import { resolve } from "node:path";
18
18
  function loadManifest(fixtureDir) {
19
19
  const path = resolve(fixtureDir, "manifest.json");
20
20
  if (!existsSync(path)) return null;
21
- const raw = JSON.parse(readFileSync(path, "utf8"));
22
- return raw;
21
+ try {
22
+ return JSON.parse(readFileSync(path, "utf8"));
23
+ } catch (err) {
24
+ const reason = err instanceof Error ? err.message : String(err);
25
+ throw new Error(`failed to parse manifest at ${path}: ${reason}`);
26
+ }
23
27
  }
24
28
  function summarizeCatalog(fixtureDir) {
25
29
  const manifest = loadManifest(fixtureDir);
package/dist/urls.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { type CodecId, type FormatId } from "./config.js";
2
+ /**
3
+ * 拼 5s 预生成 VOD fixture 的流 URL:`<base>/streams/<format>/<codec>`;
4
+ * base 尾部斜杠可有可无(内部归一化)。hls/fmp4 为容器目录布局,服务端要求
5
+ * 主文件子路径,故分别拼接 `/master.m3u8`、`/init.mp4`;flv/mp4/ts 保持单文件路径。
6
+ */
7
+ export declare function vodStreamUrl(base: string, format: FormatId, codec: CodecId): string;
8
+ /**
9
+ * 拼无限循环 live 流的流 URL:`<base>/streams/live/<format>/<codec>`;
10
+ * base 尾部斜杠可有可无(内部归一化)。注意 hls live 服务端尚未支持(501)。
11
+ */
12
+ export declare function liveStreamUrl(base: string, format: FormatId, codec: CodecId): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-player/test-streams",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
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",