@flow-player/test-streams 0.6.2 → 0.7.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.
@@ -24,7 +24,8 @@ function parseArgs(argv) {
24
24
  gopSeconds: 1,
25
25
  width: 1280,
26
26
  height: 720,
27
- fps: 25
27
+ fps: 25,
28
+ codec: "h264"
28
29
  };
29
30
  let help = false;
30
31
  for (let i = 0; i < argv.length; i += 1) {
@@ -44,6 +45,12 @@ function parseArgs(argv) {
44
45
  opts.height = Number(argv[++i]);
45
46
  } else if (a === "--fps") {
46
47
  opts.fps = Number(argv[++i]);
48
+ } else if (a === "--codec") {
49
+ const codec = argv[++i];
50
+ if (codec !== "h264" && codec !== "hevc") {
51
+ throw new Error(`[extend-vod] unsupported --codec "${codec}" (expected h264|hevc)`);
52
+ }
53
+ opts.codec = codec;
47
54
  }
48
55
  }
49
56
  return { ...opts, help };
@@ -58,6 +65,7 @@ Options:
58
65
  --gop <seconds> GOP interval (default: 1)
59
66
  --width <px> Video width (default: 1280)
60
67
  --height <px> Video height (default: 720)
68
+ --codec <h264|hevc> Video codec (default: h264; hevc \u8F93\u51FA\u5230 vod-hevc/ \u5B50\u76EE\u5F55)
61
69
  `;
62
70
  function hashFile(abs) {
63
71
  return createHash("sha256").update(readFileSync(abs)).digest("hex");
@@ -98,11 +106,12 @@ async function main() {
98
106
  return 1;
99
107
  }
100
108
  const fixtureDir = resolve(args.fixtureDir);
101
- const vodDir = join(fixtureDir, "vod");
109
+ const vodDirName = args.codec === "hevc" ? "vod-hevc" : "vod";
110
+ const vodDir = join(fixtureDir, vodDirName);
102
111
  mkdirSync(vodDir, { recursive: true });
103
112
  const gop = String(args.gopSeconds * args.fps);
104
113
  process.stdout.write(
105
- `[extend-vod] generating ${args.segmentCount} segments x ${args.segmentSeconds}s ${args.width}x${args.height}@${args.fps} mp4 h264+aac, gop=${gop} frames (${args.gopSeconds}s); total VOD=${args.segmentCount * args.segmentSeconds}s
114
+ `[extend-vod] generating ${args.segmentCount} segments x ${args.segmentSeconds}s ${args.width}x${args.height}@${args.fps} mp4 ${args.codec}+aac, gop=${gop} frames (${args.gopSeconds}s); total VOD=${args.segmentCount * args.segmentSeconds}s
106
115
  `
107
116
  );
108
117
  const segments = [];
@@ -111,6 +120,18 @@ async function main() {
111
120
  const file = join(vodDir, `seg-${String(index).padStart(3, "0")}.mp4`);
112
121
  const startSec = i * args.segmentSeconds;
113
122
  const frequency = 220 + i * 40;
123
+ const videoArgs = args.codec === "hevc" ? ["-c:v", "libx265", "-preset", "veryfast", "-tag:v", "hvc1", "-pix_fmt", "yuv420p"] : [
124
+ "-c:v",
125
+ "libx264",
126
+ "-profile:v",
127
+ "high",
128
+ "-bf",
129
+ "0",
130
+ "-preset",
131
+ "veryfast",
132
+ "-pix_fmt",
133
+ "yuv420p"
134
+ ];
114
135
  const ffmpegArgs = [
115
136
  "-y",
116
137
  "-f",
@@ -123,16 +144,7 @@ async function main() {
123
144
  `sine=frequency=${frequency}:sample_rate=44100`,
124
145
  "-t",
125
146
  String(args.segmentSeconds),
126
- "-c:v",
127
- "libx264",
128
- "-profile:v",
129
- "high",
130
- "-bf",
131
- "0",
132
- "-preset",
133
- "veryfast",
134
- "-pix_fmt",
135
- "yuv420p",
147
+ ...videoArgs,
136
148
  "-g",
137
149
  gop,
138
150
  "-keyint_min",
@@ -156,11 +168,17 @@ async function main() {
156
168
  const code = await runFfmpeg(ffmpegPath, ffmpegArgs);
157
169
  const size = statSync(file).size;
158
170
  const sha256 = hashFile(file);
159
- segments.push({ index, path: `vod/seg-${String(index).padStart(3, "0")}.mp4`, size, sha256 });
171
+ segments.push({
172
+ index,
173
+ path: `${vodDirName}/seg-${String(index).padStart(3, "0")}.mp4`,
174
+ size,
175
+ sha256
176
+ });
160
177
  }
161
178
  const vodList = {
162
179
  version: 1,
163
180
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
181
+ codec: args.codec,
164
182
  segmentSeconds: args.segmentSeconds,
165
183
  segmentCount: args.segmentCount,
166
184
  gopSeconds: args.gopSeconds,
package/dist/cli/serve.js CHANGED
@@ -55,6 +55,10 @@ function applyCors(res) {
55
55
  res.setHeader("Access-Control-Allow-Origin", "*");
56
56
  res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
57
57
  res.setHeader("Access-Control-Allow-Headers", "Range, Content-Type, If-None-Match");
58
+ res.setHeader(
59
+ "Access-Control-Expose-Headers",
60
+ "Content-Range, Accept-Ranges, Content-Length, Content-Type"
61
+ );
58
62
  }
59
63
  function writeJson(res, status, body) {
60
64
  const text = JSON.stringify(body);
@@ -118,6 +122,7 @@ var HLS_FMP4 = { hls: true, fmp4: true };
118
122
  var SINGLE = /^\/streams\/([a-z0-9]+)\/([a-z0-9]+)\/?$/;
119
123
  var PLAYLIST = /^\/streams\/(hls|fmp4)\/([a-z0-9]+)\/(.+)$/;
120
124
  var VOD = /^\/streams\/vod\/([a-zA-Z0-9._-]+)\/?$/;
125
+ var VOD_HEVC = /^\/streams\/vod-hevc\/([a-zA-Z0-9._-]+)\/?$/;
121
126
  function resolveRoute(path) {
122
127
  const single = SINGLE.exec(path);
123
128
  if (single) {
@@ -139,6 +144,11 @@ function resolveRoute(path) {
139
144
  const [, name] = vod;
140
145
  return join("vod", name);
141
146
  }
147
+ const vodHevc = VOD_HEVC.exec(path);
148
+ if (vodHevc) {
149
+ const [, name] = vodHevc;
150
+ return join("vod-hevc", name);
151
+ }
142
152
  return null;
143
153
  }
144
154
  function handleHealth(res, manifest, fixtureDir) {
@@ -159,8 +169,8 @@ function handleList(res, manifest) {
159
169
  }
160
170
  writeJson(res, 200, manifest);
161
171
  }
162
- function handleVodList(res, manifest, fixtureDir) {
163
- const vodListPath = join(fixtureDir, "vod", "vod-list.json");
172
+ function handleVodList(res, manifest, fixtureDir, codec = "h264") {
173
+ const vodListPath = codec === "hevc" ? join(fixtureDir, "vod-hevc", "vod-list.json") : join(fixtureDir, "vod", "vod-list.json");
164
174
  if (existsSync2(vodListPath)) {
165
175
  try {
166
176
  const raw = JSON.parse(readFileSync2(vodListPath, "utf8"));
@@ -179,6 +189,10 @@ function handleVodList(res, manifest, fixtureDir) {
179
189
  } catch {
180
190
  }
181
191
  }
192
+ if (codec === "hevc") {
193
+ writeJson(res, 404, { error: "vod-hevc list not found (run extend-vod --codec hevc first)" });
194
+ return;
195
+ }
182
196
  if (!manifest) {
183
197
  writeJson(res, 503, { error: "manifest not found" });
184
198
  return;
@@ -311,7 +325,22 @@ function handleLiveStream(req, res, format, codec, options = {}) {
311
325
  });
312
326
  }
313
327
  });
314
- proc.stdout.pipe(res);
328
+ let stderrTail = "";
329
+ proc.stderr.setEncoding("utf8");
330
+ proc.stderr.on("data", (chunk) => {
331
+ stderrTail = (stderrTail + chunk).slice(-2e3);
332
+ });
333
+ proc.stdout.pipe(res, { end: false });
334
+ proc.once("close", (code) => {
335
+ if (!res.headersSent && code !== 0) {
336
+ const hint = codec === "hevc" && (format === "flv" || format === "ts") ? " (HEVC in FLV/TS requires ffmpeg >= 6 with enhanced-rtmp support)" : "";
337
+ writeJson(res, 503, {
338
+ error: "ffmpeg exited before producing output",
339
+ reason: `${stderrTail.trim().split("\n").at(-1) ?? "unknown"}${hint}`,
340
+ exitCode: code
341
+ });
342
+ }
343
+ });
315
344
  const cleanup = () => {
316
345
  if (!proc.killed) proc.kill("SIGKILL");
317
346
  };
@@ -440,6 +469,10 @@ function createStreamsServer(options) {
440
469
  handleVodList(res, ctx.manifest, ctx.fixtureDir);
441
470
  return;
442
471
  }
472
+ if (path === "/streams/vod-list-hevc" || path === "/streams/vod-list-hevc/") {
473
+ handleVodList(res, ctx.manifest, ctx.fixtureDir, "hevc");
474
+ return;
475
+ }
443
476
  const resolved = resolveRoute(path);
444
477
  if (resolved) {
445
478
  serveEntry(req, res, fixtureDir, resolved);
package/dist/index.js CHANGED
@@ -257,6 +257,10 @@ function applyCors(res) {
257
257
  res.setHeader("Access-Control-Allow-Origin", "*");
258
258
  res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
259
259
  res.setHeader("Access-Control-Allow-Headers", "Range, Content-Type, If-None-Match");
260
+ res.setHeader(
261
+ "Access-Control-Expose-Headers",
262
+ "Content-Range, Accept-Ranges, Content-Length, Content-Type"
263
+ );
260
264
  }
261
265
  function writeJson(res, status, body) {
262
266
  const text = JSON.stringify(body);
@@ -320,6 +324,7 @@ var HLS_FMP4 = { hls: true, fmp4: true };
320
324
  var SINGLE = /^\/streams\/([a-z0-9]+)\/([a-z0-9]+)\/?$/;
321
325
  var PLAYLIST = /^\/streams\/(hls|fmp4)\/([a-z0-9]+)\/(.+)$/;
322
326
  var VOD = /^\/streams\/vod\/([a-zA-Z0-9._-]+)\/?$/;
327
+ var VOD_HEVC = /^\/streams\/vod-hevc\/([a-zA-Z0-9._-]+)\/?$/;
323
328
  function resolveRoute(path) {
324
329
  const single = SINGLE.exec(path);
325
330
  if (single) {
@@ -341,6 +346,11 @@ function resolveRoute(path) {
341
346
  const [, name] = vod;
342
347
  return join3("vod", name);
343
348
  }
349
+ const vodHevc = VOD_HEVC.exec(path);
350
+ if (vodHevc) {
351
+ const [, name] = vodHevc;
352
+ return join3("vod-hevc", name);
353
+ }
344
354
  return null;
345
355
  }
346
356
  function handleHealth(res, manifest, fixtureDir) {
@@ -361,8 +371,8 @@ function handleList(res, manifest) {
361
371
  }
362
372
  writeJson(res, 200, manifest);
363
373
  }
364
- function handleVodList(res, manifest, fixtureDir) {
365
- const vodListPath = join3(fixtureDir, "vod", "vod-list.json");
374
+ function handleVodList(res, manifest, fixtureDir, codec = "h264") {
375
+ const vodListPath = codec === "hevc" ? join3(fixtureDir, "vod-hevc", "vod-list.json") : join3(fixtureDir, "vod", "vod-list.json");
366
376
  if (existsSync2(vodListPath)) {
367
377
  try {
368
378
  const raw = JSON.parse(readFileSync2(vodListPath, "utf8"));
@@ -381,6 +391,10 @@ function handleVodList(res, manifest, fixtureDir) {
381
391
  } catch {
382
392
  }
383
393
  }
394
+ if (codec === "hevc") {
395
+ writeJson(res, 404, { error: "vod-hevc list not found (run extend-vod --codec hevc first)" });
396
+ return;
397
+ }
384
398
  if (!manifest) {
385
399
  writeJson(res, 503, { error: "manifest not found" });
386
400
  return;
@@ -491,7 +505,22 @@ function handleLiveStream(req, res, format, codec, options = {}) {
491
505
  });
492
506
  }
493
507
  });
494
- proc.stdout.pipe(res);
508
+ let stderrTail = "";
509
+ proc.stderr.setEncoding("utf8");
510
+ proc.stderr.on("data", (chunk) => {
511
+ stderrTail = (stderrTail + chunk).slice(-2e3);
512
+ });
513
+ proc.stdout.pipe(res, { end: false });
514
+ proc.once("close", (code) => {
515
+ if (!res.headersSent && code !== 0) {
516
+ const hint = codec === "hevc" && (format === "flv" || format === "ts") ? " (HEVC in FLV/TS requires ffmpeg >= 6 with enhanced-rtmp support)" : "";
517
+ writeJson(res, 503, {
518
+ error: "ffmpeg exited before producing output",
519
+ reason: `${stderrTail.trim().split("\n").at(-1) ?? "unknown"}${hint}`,
520
+ exitCode: code
521
+ });
522
+ }
523
+ });
495
524
  const cleanup = () => {
496
525
  if (!proc.killed) proc.kill("SIGKILL");
497
526
  };
@@ -620,6 +649,10 @@ function createStreamsServer(options) {
620
649
  handleVodList(res, ctx.manifest, ctx.fixtureDir);
621
650
  return;
622
651
  }
652
+ if (path === "/streams/vod-list-hevc" || path === "/streams/vod-list-hevc/") {
653
+ handleVodList(res, ctx.manifest, ctx.fixtureDir, "hevc");
654
+ return;
655
+ }
623
656
  const resolved = resolveRoute(path);
624
657
  if (resolved) {
625
658
  serveEntry(req, res, fixtureDir, resolved);
@@ -15,4 +15,4 @@ export type VodSegmentDescriptor = {
15
15
  export type VodListResponse = {
16
16
  segments: ReadonlyArray<VodSegmentDescriptor>;
17
17
  };
18
- export declare function handleVodList(res: ServerResponse, manifest: Manifest | null, fixtureDir: string): void;
18
+ export declare function handleVodList(res: ServerResponse, manifest: Manifest | null, fixtureDir: string, codec?: "h264" | "hevc"): void;
@@ -48,6 +48,10 @@ function applyCors(res) {
48
48
  res.setHeader("Access-Control-Allow-Origin", "*");
49
49
  res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
50
50
  res.setHeader("Access-Control-Allow-Headers", "Range, Content-Type, If-None-Match");
51
+ res.setHeader(
52
+ "Access-Control-Expose-Headers",
53
+ "Content-Range, Accept-Ranges, Content-Length, Content-Type"
54
+ );
51
55
  }
52
56
  function writeJson(res, status, body) {
53
57
  const text = JSON.stringify(body);
@@ -111,6 +115,7 @@ var HLS_FMP4 = { hls: true, fmp4: true };
111
115
  var SINGLE = /^\/streams\/([a-z0-9]+)\/([a-z0-9]+)\/?$/;
112
116
  var PLAYLIST = /^\/streams\/(hls|fmp4)\/([a-z0-9]+)\/(.+)$/;
113
117
  var VOD = /^\/streams\/vod\/([a-zA-Z0-9._-]+)\/?$/;
118
+ var VOD_HEVC = /^\/streams\/vod-hevc\/([a-zA-Z0-9._-]+)\/?$/;
114
119
  function resolveRoute(path) {
115
120
  const single = SINGLE.exec(path);
116
121
  if (single) {
@@ -132,6 +137,11 @@ function resolveRoute(path) {
132
137
  const [, name] = vod;
133
138
  return join("vod", name);
134
139
  }
140
+ const vodHevc = VOD_HEVC.exec(path);
141
+ if (vodHevc) {
142
+ const [, name] = vodHevc;
143
+ return join("vod-hevc", name);
144
+ }
135
145
  return null;
136
146
  }
137
147
  function handleHealth(res, manifest, fixtureDir) {
@@ -152,8 +162,8 @@ function handleList(res, manifest) {
152
162
  }
153
163
  writeJson(res, 200, manifest);
154
164
  }
155
- function handleVodList(res, manifest, fixtureDir) {
156
- const vodListPath = join(fixtureDir, "vod", "vod-list.json");
165
+ function handleVodList(res, manifest, fixtureDir, codec = "h264") {
166
+ const vodListPath = codec === "hevc" ? join(fixtureDir, "vod-hevc", "vod-list.json") : join(fixtureDir, "vod", "vod-list.json");
157
167
  if (existsSync2(vodListPath)) {
158
168
  try {
159
169
  const raw = JSON.parse(readFileSync2(vodListPath, "utf8"));
@@ -172,6 +182,10 @@ function handleVodList(res, manifest, fixtureDir) {
172
182
  } catch {
173
183
  }
174
184
  }
185
+ if (codec === "hevc") {
186
+ writeJson(res, 404, { error: "vod-hevc list not found (run extend-vod --codec hevc first)" });
187
+ return;
188
+ }
175
189
  if (!manifest) {
176
190
  writeJson(res, 503, { error: "manifest not found" });
177
191
  return;
@@ -304,7 +318,22 @@ function handleLiveStream(req, res, format, codec, options = {}) {
304
318
  });
305
319
  }
306
320
  });
307
- proc.stdout.pipe(res);
321
+ let stderrTail = "";
322
+ proc.stderr.setEncoding("utf8");
323
+ proc.stderr.on("data", (chunk) => {
324
+ stderrTail = (stderrTail + chunk).slice(-2e3);
325
+ });
326
+ proc.stdout.pipe(res, { end: false });
327
+ proc.once("close", (code) => {
328
+ if (!res.headersSent && code !== 0) {
329
+ const hint = codec === "hevc" && (format === "flv" || format === "ts") ? " (HEVC in FLV/TS requires ffmpeg >= 6 with enhanced-rtmp support)" : "";
330
+ writeJson(res, 503, {
331
+ error: "ffmpeg exited before producing output",
332
+ reason: `${stderrTail.trim().split("\n").at(-1) ?? "unknown"}${hint}`,
333
+ exitCode: code
334
+ });
335
+ }
336
+ });
308
337
  const cleanup = () => {
309
338
  if (!proc.killed) proc.kill("SIGKILL");
310
339
  };
@@ -433,6 +462,10 @@ function createStreamsServer(options) {
433
462
  handleVodList(res, ctx.manifest, ctx.fixtureDir);
434
463
  return;
435
464
  }
465
+ if (path === "/streams/vod-list-hevc" || path === "/streams/vod-list-hevc/") {
466
+ handleVodList(res, ctx.manifest, ctx.fixtureDir, "hevc");
467
+ return;
468
+ }
436
469
  const resolved = resolveRoute(path);
437
470
  if (resolved) {
438
471
  serveEntry(req, res, fixtureDir, resolved);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-player/test-streams",
3
- "version": "0.6.2",
3
+ "version": "0.7.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",