@flow-player/test-streams 0.6.3 → 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.
- package/dist/cli/extend-vod.js +32 -14
- package/dist/cli/serve.js +36 -3
- package/dist/index.js +36 -3
- package/dist/server/routes.d.ts +1 -1
- package/dist/server/server.js +36 -3
- package/package.json +1 -1
package/dist/cli/extend-vod.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -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;
|
package/dist/server/server.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|