@flow-player/test-streams 0.14.2 → 0.15.1
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 +63 -0
- package/dist/cli/generate.js +469 -15
- package/dist/cli/serve.js +14 -1
- package/dist/config.d.ts +2 -0
- package/dist/fixtures/catalog.d.ts +18 -0
- package/dist/fixtures/catalog.js +1 -0
- package/dist/fixtures/generate.d.ts +6 -3
- package/dist/fixtures/generate.js +462 -14
- package/dist/fixtures/window.d.ts +110 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +493 -22
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/server.js +14 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CodecId, type FormatId } from "../config.js";
|
|
2
|
+
import type { WindowEncodeParams } from "./window.js";
|
|
2
3
|
/** manifest 内单条文件记录:相对 fixtureDir 的路径 + 字节数 + sha256。 */
|
|
3
4
|
export type ManifestEntry = {
|
|
4
5
|
path: string;
|
|
@@ -11,12 +12,29 @@ export type ManifestItem = {
|
|
|
11
12
|
format: FormatId;
|
|
12
13
|
entries: ReadonlyArray<ManifestEntry>;
|
|
13
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* P3-H 烧录窗 fixture 的 manifest 条目:单 MP4 文件 + 编码参数。
|
|
17
|
+
* entries 复用 ManifestEntry 契约(size/sha256),encode 记录 GOP/B 帧/
|
|
18
|
+
* 首 PTS/稀疏等窗口测试关心的编码参数。
|
|
19
|
+
*/
|
|
20
|
+
export type WindowManifestItem = {
|
|
21
|
+
/** 烧录窗 id(window/<id>.mp4 的文件名主体)。 */
|
|
22
|
+
id: string;
|
|
23
|
+
/** 视频编码。 */
|
|
24
|
+
codec: CodecId;
|
|
25
|
+
/** 单文件条目(仅主文件)。 */
|
|
26
|
+
entries: ReadonlyArray<ManifestEntry>;
|
|
27
|
+
/** 编码参数:GOP/B 帧/首 PTS/稀疏标记等。 */
|
|
28
|
+
encode: WindowEncodeParams;
|
|
29
|
+
};
|
|
14
30
|
/** manifest.json 顶层结构:generate.ts 负责写,catalog 负责读 + 过滤。 */
|
|
15
31
|
export type Manifest = {
|
|
16
32
|
version: 1;
|
|
17
33
|
generatedAt: string;
|
|
18
34
|
durationSeconds: number;
|
|
19
35
|
fixtures: ReadonlyArray<ManifestItem>;
|
|
36
|
+
/** P3-H 烧录窗序列(可选,老 manifest 缺省为空);只追加不改存量 fixtures。 */
|
|
37
|
+
windowFixtures?: ReadonlyArray<WindowManifestItem>;
|
|
20
38
|
};
|
|
21
39
|
/** lookupFixture 的查询结果:该组合的文件清单与主文件。 */
|
|
22
40
|
export type FixtureLookup = {
|
package/dist/fixtures/catalog.js
CHANGED
|
@@ -37,6 +37,7 @@ function summarizeCatalog(fixtureDir) {
|
|
|
37
37
|
}
|
|
38
38
|
let total = 0;
|
|
39
39
|
for (const item of manifest.fixtures) total += item.entries.length;
|
|
40
|
+
for (const item of manifest.windowFixtures ?? []) total += item.entries.length;
|
|
40
41
|
return {
|
|
41
42
|
codecs: ALL_CODECS,
|
|
42
43
|
formats: ALL_FORMATS,
|
|
@@ -4,11 +4,11 @@ import { type Manifest } from "./catalog.js";
|
|
|
4
4
|
export type GenerateResult = {
|
|
5
5
|
/** fixture 根目录(绝对路径),manifest.json 写在其下。 */
|
|
6
6
|
fixtureDir: string;
|
|
7
|
-
/** 本次写盘的 manifest:每个 codec × format 组合一条,entries 带 size/sha256
|
|
7
|
+
/** 本次写盘的 manifest:每个 codec × format 组合一条,entries 带 size/sha256;另附 windowFixtures 烧录窗序列。 */
|
|
8
8
|
manifest: Manifest;
|
|
9
|
-
/** 本次实际跑 ffmpeg
|
|
9
|
+
/** 本次实际跑 ffmpeg 生成(或收集已有输出元信息)的组合数(含烧录窗)。 */
|
|
10
10
|
generated: number;
|
|
11
|
-
/** 输出已存在且未开 overwrite
|
|
11
|
+
/** 输出已存在且未开 overwrite 而跳过的组合数(含烧录窗)。 */
|
|
12
12
|
skipped: number;
|
|
13
13
|
/** 实际使用的 ffmpeg 可执行路径。 */
|
|
14
14
|
ffmpegPath: string;
|
|
@@ -21,6 +21,9 @@ export type GenerateResult = {
|
|
|
21
21
|
* 写到 `fixtureDir/manifest.json`。输出已存在且未开 overwrite 的组合跳过生成,
|
|
22
22
|
* 但仍从磁盘收集条目进 manifest;hls/fmp4 容器目录内 ffmpeg 实际产出的段文件
|
|
23
23
|
* 一并收编。开头还会从 mp4/h264.mp4 复制出 vod/sample-*.mp4 样例。
|
|
24
|
+
* 矩阵之后按 WINDOW_SPECS 顺序生成 P3-H 烧录窗 MP4 序列(window/<id>.mp4,
|
|
25
|
+
* 帧号与媒体时间经 drawtext 烧录),条目进 manifest.windowFixtures;
|
|
26
|
+
* includeWindow=false 可跳过烧录窗(快速反馈用)。
|
|
24
27
|
* 找不到 ffmpeg 时不创建目录、不写盘,返回 ffmpegMissing=true 的空结果。
|
|
25
28
|
*/
|
|
26
29
|
export declare function generateFixtures(opts?: GenerateOptions): Promise<GenerateResult>;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
writeFileSync
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
|
-
import { join as
|
|
13
|
+
import { join as join4, resolve } from "node:path";
|
|
14
14
|
|
|
15
15
|
// src/config.ts
|
|
16
16
|
var DEFAULT_FIXTURE_DIR = "fixtures";
|
|
@@ -202,6 +202,372 @@ function expandMatrix(opts) {
|
|
|
202
202
|
}
|
|
203
203
|
var VOD_SAMPLE_RELATIVE = "mp4/h264.mp4";
|
|
204
204
|
|
|
205
|
+
// src/fixtures/window.ts
|
|
206
|
+
import { join as join3 } from "node:path";
|
|
207
|
+
var DEFAULT_DRAWTEXT_FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
|
|
208
|
+
var DRAWTEXT_FONT_CANDIDATES = [
|
|
209
|
+
"/System/Library/Fonts/Supplemental/Verdana.ttf",
|
|
210
|
+
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
|
211
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
212
|
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
|
213
|
+
];
|
|
214
|
+
var WINDOW_CONTAINER_DIR = "window";
|
|
215
|
+
var WINDOW_SPECS = [
|
|
216
|
+
{
|
|
217
|
+
id: "h264-shortgop-bframes",
|
|
218
|
+
codec: "h264",
|
|
219
|
+
width: 1280,
|
|
220
|
+
height: 720,
|
|
221
|
+
fps: 25,
|
|
222
|
+
durationSeconds: 6,
|
|
223
|
+
gopFrames: 25,
|
|
224
|
+
gopMode: "short",
|
|
225
|
+
bframes: 3,
|
|
226
|
+
startPtsSeconds: 5,
|
|
227
|
+
sparse: false,
|
|
228
|
+
moovTrailing: true,
|
|
229
|
+
delayedTail: true,
|
|
230
|
+
usageP3: "\u77ED GOP \u6709 B \u5E27\u57FA\u7EBF\uFF1A\u7A97\u53E3\u9010 GOP \u524D\u8FDB\u65F6\u7684\u89E3\u7801\u8D77\u70B9\u5BF9\u9F50",
|
|
231
|
+
usageP5: "\u591A\u8DEF\u5E76\u53D1\u4E0B\u7684\u5E38\u89C4\u89E3\u7801\u8D1F\u8F7D\u57FA\u7EBF"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "h264-shortgop-nobframes",
|
|
235
|
+
codec: "h264",
|
|
236
|
+
width: 1280,
|
|
237
|
+
height: 720,
|
|
238
|
+
fps: 25,
|
|
239
|
+
durationSeconds: 6,
|
|
240
|
+
gopFrames: 25,
|
|
241
|
+
gopMode: "short",
|
|
242
|
+
bframes: 0,
|
|
243
|
+
startPtsSeconds: 5,
|
|
244
|
+
sparse: false,
|
|
245
|
+
moovTrailing: true,
|
|
246
|
+
delayedTail: false,
|
|
247
|
+
usageP3: "\u65E0 B \u5E27\u5BF9\u7167\uFF1A\u5E27\u8F93\u51FA\u65E0\u91CD\u6392\u5EF6\u8FDF\uFF0C\u6821\u9A8C PTS \u5355\u8C03\u6027",
|
|
248
|
+
usageP5: "\u4F4E\u5EF6\u8FDF\u94FE\u8DEF\u7684\u5BB9\u91CF\u5BF9\u7167\u7EC4"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "hevc-shortgop-bframes",
|
|
252
|
+
codec: "hevc",
|
|
253
|
+
width: 1280,
|
|
254
|
+
height: 720,
|
|
255
|
+
fps: 25,
|
|
256
|
+
durationSeconds: 6,
|
|
257
|
+
gopFrames: 25,
|
|
258
|
+
gopMode: "short",
|
|
259
|
+
bframes: 3,
|
|
260
|
+
startPtsSeconds: 5,
|
|
261
|
+
sparse: false,
|
|
262
|
+
moovTrailing: true,
|
|
263
|
+
delayedTail: true,
|
|
264
|
+
usageP3: "HEVC \u77ED GOP \u6709 B \u5E27\u57FA\u7EBF\uFF1Ahvc1 \u5C01\u88C5\u4E0B\u7A97\u53E3\u63A8\u8FDB",
|
|
265
|
+
usageP5: "HEVC \u8F6F\u89E3 / \u786C\u89E3\u5BB9\u91CF\u57FA\u7EBF"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "hevc-shortgop-nobframes",
|
|
269
|
+
codec: "hevc",
|
|
270
|
+
width: 1280,
|
|
271
|
+
height: 720,
|
|
272
|
+
fps: 25,
|
|
273
|
+
durationSeconds: 6,
|
|
274
|
+
gopFrames: 25,
|
|
275
|
+
gopMode: "short",
|
|
276
|
+
bframes: 0,
|
|
277
|
+
startPtsSeconds: 5,
|
|
278
|
+
sparse: false,
|
|
279
|
+
moovTrailing: true,
|
|
280
|
+
delayedTail: false,
|
|
281
|
+
usageP3: "HEVC \u65E0 B \u5E27\u5BF9\u7167\uFF1A\u91CD\u6392\u5173\u95ED\u65F6\u7684\u7A97\u53E3\u884C\u4E3A",
|
|
282
|
+
usageP5: "HEVC \u4F4E\u5EF6\u8FDF\u5BB9\u91CF\u5BF9\u7167\u7EC4"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "h264-longgop-bframes",
|
|
286
|
+
codec: "h264",
|
|
287
|
+
width: 1280,
|
|
288
|
+
height: 720,
|
|
289
|
+
fps: 25,
|
|
290
|
+
durationSeconds: 10,
|
|
291
|
+
gopFrames: 125,
|
|
292
|
+
gopMode: "long",
|
|
293
|
+
bframes: 3,
|
|
294
|
+
startPtsSeconds: 10,
|
|
295
|
+
sparse: false,
|
|
296
|
+
moovTrailing: true,
|
|
297
|
+
delayedTail: true,
|
|
298
|
+
usageP3: "\u957F GOP\uFF085s\uFF09\u8DE8\u7A97 seek\uFF1A\u7A97\u53E3\u843D\u5728 GOP \u4E2D\u90E8\u65F6\u7684\u56DE\u8DF3\u8DDD\u79BB",
|
|
299
|
+
usageP5: "\u957F GOP \u4F4E\u7801\u7387\u6863\u7684\u89E3\u7801\u5BB9\u91CF"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "hevc-longgop-bframes",
|
|
303
|
+
codec: "hevc",
|
|
304
|
+
width: 1280,
|
|
305
|
+
height: 720,
|
|
306
|
+
fps: 25,
|
|
307
|
+
durationSeconds: 10,
|
|
308
|
+
gopFrames: 125,
|
|
309
|
+
gopMode: "long",
|
|
310
|
+
bframes: 3,
|
|
311
|
+
startPtsSeconds: 10,
|
|
312
|
+
sparse: false,
|
|
313
|
+
moovTrailing: true,
|
|
314
|
+
delayedTail: true,
|
|
315
|
+
usageP3: "HEVC \u957F GOP \u8DE8\u7A97 seek \u5BF9\u7167",
|
|
316
|
+
usageP5: "HEVC \u957F GOP \u5BB9\u91CF\u6863"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: "h264-singlegop",
|
|
320
|
+
codec: "h264",
|
|
321
|
+
width: 1280,
|
|
322
|
+
height: 720,
|
|
323
|
+
fps: 25,
|
|
324
|
+
durationSeconds: 8,
|
|
325
|
+
gopFrames: 200,
|
|
326
|
+
gopMode: "single",
|
|
327
|
+
bframes: 3,
|
|
328
|
+
startPtsSeconds: 20,
|
|
329
|
+
sparse: false,
|
|
330
|
+
moovTrailing: true,
|
|
331
|
+
delayedTail: true,
|
|
332
|
+
usageP3: "\u5355 GOP \u6781\u9650\uFF1A\u7A97\u53E3\u4EFB\u610F\u70B9 seek \u5FC5\u987B\u56DE\u8DF3\u5230\u6BB5\u9996 IDR",
|
|
333
|
+
usageP5: "\u5355 GOP \u6781\u4F4E\u7801\u7387\u6863\u5BB9\u91CF"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: "hevc-singlegop",
|
|
337
|
+
codec: "hevc",
|
|
338
|
+
width: 1280,
|
|
339
|
+
height: 720,
|
|
340
|
+
fps: 25,
|
|
341
|
+
durationSeconds: 8,
|
|
342
|
+
gopFrames: 200,
|
|
343
|
+
gopMode: "single",
|
|
344
|
+
bframes: 3,
|
|
345
|
+
startPtsSeconds: 20,
|
|
346
|
+
sparse: false,
|
|
347
|
+
moovTrailing: true,
|
|
348
|
+
delayedTail: true,
|
|
349
|
+
usageP3: "HEVC \u5355 GOP \u6781\u9650\u5BF9\u7167",
|
|
350
|
+
usageP5: "HEVC \u5355 GOP \u5BB9\u91CF\u6863"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: "h264-sparse-1fps",
|
|
354
|
+
codec: "h264",
|
|
355
|
+
width: 1280,
|
|
356
|
+
height: 720,
|
|
357
|
+
fps: 1,
|
|
358
|
+
durationSeconds: 8,
|
|
359
|
+
gopFrames: 4,
|
|
360
|
+
gopMode: "short",
|
|
361
|
+
bframes: 0,
|
|
362
|
+
startPtsSeconds: 30,
|
|
363
|
+
sparse: true,
|
|
364
|
+
moovTrailing: true,
|
|
365
|
+
delayedTail: false,
|
|
366
|
+
usageP3: "1fps \u7A00\u758F\u6D41\uFF1A\u7A97\u53E3\u6309\u79D2\u6B65\u8FDB\u65F6\u7684\u5E27\u53F7 / \u5A92\u4F53\u65F6\u95F4\u5BF9\u5E94",
|
|
367
|
+
usageP5: "\u7A00\u758F\u6D41\u6781\u4F4E\u5E27\u7387\u5BB9\u91CF\uFF08\u7F29\u7565\u56FE\u5F0F\u62C9\u6D41\uFF09"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: "hevc-sparse-1fps",
|
|
371
|
+
codec: "hevc",
|
|
372
|
+
width: 1280,
|
|
373
|
+
height: 720,
|
|
374
|
+
fps: 1,
|
|
375
|
+
durationSeconds: 8,
|
|
376
|
+
gopFrames: 4,
|
|
377
|
+
gopMode: "short",
|
|
378
|
+
bframes: 0,
|
|
379
|
+
startPtsSeconds: 30,
|
|
380
|
+
sparse: true,
|
|
381
|
+
moovTrailing: true,
|
|
382
|
+
delayedTail: false,
|
|
383
|
+
usageP3: "HEVC 1fps \u7A00\u758F\u5BF9\u7167",
|
|
384
|
+
usageP5: "HEVC \u7A00\u758F\u6D41\u5BB9\u91CF\u6863"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "h264-gap-a",
|
|
388
|
+
codec: "h264",
|
|
389
|
+
width: 1280,
|
|
390
|
+
height: 720,
|
|
391
|
+
fps: 25,
|
|
392
|
+
durationSeconds: 6,
|
|
393
|
+
gopFrames: 25,
|
|
394
|
+
gopMode: "short",
|
|
395
|
+
bframes: 0,
|
|
396
|
+
startPtsSeconds: 100,
|
|
397
|
+
sparse: false,
|
|
398
|
+
moovTrailing: true,
|
|
399
|
+
delayedTail: false,
|
|
400
|
+
pairKind: "gap",
|
|
401
|
+
pairRole: "a",
|
|
402
|
+
usageP3: "gap \u53CC\u6BB5\u9996\u6BB5\uFF1A\u4E0E gap-b \u4E4B\u95F4\u7559 2s \u7A7A\u6D1E\uFF0C\u6D4B\u7A97\u53E3\u7A7A\u6D1E\u5C55\u793A",
|
|
403
|
+
usageP5: "\u65AD\u6D41\u7A7A\u6D1E\u573A\u666F\u7684\u5BB9\u91CF\u5360\u6BD4\u6A21\u578B\u8F93\u5165"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: "h264-gap-b",
|
|
407
|
+
codec: "h264",
|
|
408
|
+
width: 1280,
|
|
409
|
+
height: 720,
|
|
410
|
+
fps: 25,
|
|
411
|
+
durationSeconds: 6,
|
|
412
|
+
gopFrames: 25,
|
|
413
|
+
gopMode: "short",
|
|
414
|
+
bframes: 0,
|
|
415
|
+
startPtsSeconds: 108,
|
|
416
|
+
sparse: false,
|
|
417
|
+
moovTrailing: true,
|
|
418
|
+
delayedTail: false,
|
|
419
|
+
pairKind: "gap",
|
|
420
|
+
pairRole: "b",
|
|
421
|
+
usageP3: "gap \u53CC\u6BB5\u6B21\u6BB5\uFF1A\u9996 PTS \u665A\u4E8E\u9996\u6BB5\u5C3E 2s\uFF0C\u7A97\u53E3\u5E94\u5448\u73B0\u7A7A\u6D1E",
|
|
422
|
+
usageP5: "\u540C gap-a \u914D\u5BF9\u4F7F\u7528"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: "h264-overlap-a",
|
|
426
|
+
codec: "h264",
|
|
427
|
+
width: 1280,
|
|
428
|
+
height: 720,
|
|
429
|
+
fps: 25,
|
|
430
|
+
durationSeconds: 6,
|
|
431
|
+
gopFrames: 25,
|
|
432
|
+
gopMode: "short",
|
|
433
|
+
bframes: 0,
|
|
434
|
+
startPtsSeconds: 200,
|
|
435
|
+
sparse: false,
|
|
436
|
+
moovTrailing: true,
|
|
437
|
+
delayedTail: false,
|
|
438
|
+
pairKind: "overlap",
|
|
439
|
+
pairRole: "a",
|
|
440
|
+
usageP3: "overlap \u53CC\u6BB5\u9996\u6BB5\uFF1A\u4E0E overlap-b \u91CD\u53E0 2s\uFF0C\u6D4B\u7A97\u53E3\u53BB\u91CD",
|
|
441
|
+
usageP5: "\u91CD\u53E0\u5F55\u50CF\u573A\u666F\u7684\u5BB9\u91CF\u5360\u6BD4\u6A21\u578B\u8F93\u5165"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
id: "h264-overlap-b",
|
|
445
|
+
codec: "h264",
|
|
446
|
+
width: 1280,
|
|
447
|
+
height: 720,
|
|
448
|
+
fps: 25,
|
|
449
|
+
durationSeconds: 6,
|
|
450
|
+
gopFrames: 25,
|
|
451
|
+
gopMode: "short",
|
|
452
|
+
bframes: 0,
|
|
453
|
+
startPtsSeconds: 204,
|
|
454
|
+
sparse: false,
|
|
455
|
+
moovTrailing: true,
|
|
456
|
+
delayedTail: false,
|
|
457
|
+
pairKind: "overlap",
|
|
458
|
+
pairRole: "b",
|
|
459
|
+
usageP3: "overlap \u53CC\u6BB5\u6B21\u6BB5\uFF1A\u9996 PTS \u65E9\u4E8E\u9996\u6BB5\u5C3E 2s\uFF0C\u7A97\u53E3\u5E94\u53BB\u91CD\u5408\u5E76",
|
|
460
|
+
usageP5: "\u540C overlap-a \u914D\u5BF9\u4F7F\u7528"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: "h264-size-720p",
|
|
464
|
+
codec: "h264",
|
|
465
|
+
width: 1280,
|
|
466
|
+
height: 720,
|
|
467
|
+
fps: 25,
|
|
468
|
+
durationSeconds: 6,
|
|
469
|
+
gopFrames: 25,
|
|
470
|
+
gopMode: "short",
|
|
471
|
+
bframes: 0,
|
|
472
|
+
startPtsSeconds: 300,
|
|
473
|
+
sparse: false,
|
|
474
|
+
moovTrailing: true,
|
|
475
|
+
delayedTail: false,
|
|
476
|
+
pairKind: "sizeSwitch",
|
|
477
|
+
pairRole: "a",
|
|
478
|
+
usageP3: "\u5C3A\u5BF8\u5207\u6362\u9996\u6BB5\uFF08720p high\uFF09\uFF1A\u4E0E 360p \u6BB5\u540C\u8D77\u70B9\uFF0C\u6D4B\u7A97\u53E3\u5185\u5206\u8FA8\u7387\u5207\u6362",
|
|
479
|
+
usageP5: "\u6E05\u6670\u5EA6\u5207\u6362\u65F6\u7684\u77AC\u65F6\u89E3\u7801\u5CF0\u503C"
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "h264-size-360p",
|
|
483
|
+
codec: "h264",
|
|
484
|
+
width: 640,
|
|
485
|
+
height: 360,
|
|
486
|
+
fps: 25,
|
|
487
|
+
durationSeconds: 6,
|
|
488
|
+
gopFrames: 25,
|
|
489
|
+
gopMode: "short",
|
|
490
|
+
bframes: 0,
|
|
491
|
+
startPtsSeconds: 300,
|
|
492
|
+
sparse: false,
|
|
493
|
+
moovTrailing: true,
|
|
494
|
+
delayedTail: false,
|
|
495
|
+
pairKind: "sizeSwitch",
|
|
496
|
+
pairRole: "b",
|
|
497
|
+
usageP3: "\u5C3A\u5BF8\u5207\u6362\u6B21\u6BB5\uFF08360p baseline\uFF09\uFF1A\u7F16\u7801\u914D\u7F6E\u4E0E\u5206\u8FA8\u7387\u540C\u65F6\u53D8\u5316",
|
|
498
|
+
usageP5: "\u540C size-720p \u914D\u5BF9\u4F7F\u7528"
|
|
499
|
+
}
|
|
500
|
+
];
|
|
501
|
+
function drawtextFilter(fontfile) {
|
|
502
|
+
return `drawtext=fontfile=${fontfile}:text='n=%{frame_num} t=%{pts\\:hms}':fontsize=28:fontcolor=white:box=1:boxcolor=black@0.6:x=16:y=16`;
|
|
503
|
+
}
|
|
504
|
+
function buildWindowPlan(spec, opts) {
|
|
505
|
+
const useDrawtext = opts?.useDrawtext ?? true;
|
|
506
|
+
const fontfile = opts?.fontfile ?? DEFAULT_DRAWTEXT_FONT;
|
|
507
|
+
const specCodec = CODEC_SPEC[spec.codec];
|
|
508
|
+
const videoInput = `testsrc2=size=${spec.width}x${spec.height}:rate=${spec.fps}`;
|
|
509
|
+
const audioInput = `sine=frequency=1000:sample_rate=44100`;
|
|
510
|
+
const videoFilter = useDrawtext ? `format=yuv420p,${drawtextFilter(fontfile)}` : "format=yuv420p";
|
|
511
|
+
const outputFps = spec.sparse ? 1 : spec.fps;
|
|
512
|
+
const sizeSwitchBaseline = spec.pairKind === "sizeSwitch" && spec.pairRole === "b" ? ["-profile:v", "baseline"] : specCodec.videoArgs;
|
|
513
|
+
const ffmpegOutputArgs = [
|
|
514
|
+
"-y",
|
|
515
|
+
"-f",
|
|
516
|
+
"lavfi",
|
|
517
|
+
"-i",
|
|
518
|
+
videoInput,
|
|
519
|
+
"-f",
|
|
520
|
+
"lavfi",
|
|
521
|
+
"-i",
|
|
522
|
+
audioInput,
|
|
523
|
+
"-t",
|
|
524
|
+
String(spec.durationSeconds),
|
|
525
|
+
"-vf",
|
|
526
|
+
videoFilter,
|
|
527
|
+
"-r",
|
|
528
|
+
String(outputFps),
|
|
529
|
+
"-c:v",
|
|
530
|
+
specCodec.encoder,
|
|
531
|
+
...sizeSwitchBaseline,
|
|
532
|
+
"-pix_fmt",
|
|
533
|
+
specCodec.pixelFormat,
|
|
534
|
+
"-bf",
|
|
535
|
+
String(spec.bframes),
|
|
536
|
+
"-g",
|
|
537
|
+
String(spec.gopFrames),
|
|
538
|
+
"-keyint_min",
|
|
539
|
+
String(spec.gopFrames),
|
|
540
|
+
"-sc_threshold",
|
|
541
|
+
"0",
|
|
542
|
+
...containerExtraArgs("mp4", spec.codec),
|
|
543
|
+
"-c:a",
|
|
544
|
+
"aac",
|
|
545
|
+
"-b:a",
|
|
546
|
+
"128k",
|
|
547
|
+
"-output_ts_offset",
|
|
548
|
+
String(spec.startPtsSeconds),
|
|
549
|
+
"-f",
|
|
550
|
+
"mp4",
|
|
551
|
+
"-movflags",
|
|
552
|
+
"-faststart"
|
|
553
|
+
];
|
|
554
|
+
const mainFile = join3(WINDOW_CONTAINER_DIR, `${spec.id}.mp4`);
|
|
555
|
+
const encode = {
|
|
556
|
+
codec: spec.codec,
|
|
557
|
+
gopFrames: spec.gopFrames,
|
|
558
|
+
gopMode: spec.gopMode,
|
|
559
|
+
bframes: spec.bframes,
|
|
560
|
+
startPtsSeconds: spec.startPtsSeconds,
|
|
561
|
+
sparse: spec.sparse,
|
|
562
|
+
moov: "trailing",
|
|
563
|
+
delayedTail: spec.delayedTail,
|
|
564
|
+
width: spec.width,
|
|
565
|
+
height: spec.height,
|
|
566
|
+
fps: outputFps
|
|
567
|
+
};
|
|
568
|
+
return { id: spec.id, spec, mainFile, files: [mainFile], ffmpegOutputArgs, encode };
|
|
569
|
+
}
|
|
570
|
+
|
|
205
571
|
// src/fixtures/generate.ts
|
|
206
572
|
var FFMPEG_PROBE_ARGS = ["-version"];
|
|
207
573
|
async function generateFixtures(opts = {}) {
|
|
@@ -243,26 +609,39 @@ async function generateFixtures(opts = {}) {
|
|
|
243
609
|
}
|
|
244
610
|
}
|
|
245
611
|
copyVodSamples(fixtureDir);
|
|
612
|
+
let windowFixtures = [];
|
|
613
|
+
if (opts.includeWindow !== false) {
|
|
614
|
+
const windowResult = await generateWindowSet(
|
|
615
|
+
fixtureDir,
|
|
616
|
+
ffmpegPath,
|
|
617
|
+
opts.quiet ?? false,
|
|
618
|
+
opts.overwrite ?? false
|
|
619
|
+
);
|
|
620
|
+
windowFixtures = windowResult.items;
|
|
621
|
+
generated += windowResult.generated;
|
|
622
|
+
skipped += windowResult.skipped;
|
|
623
|
+
}
|
|
246
624
|
const manifest = {
|
|
247
625
|
version: 1,
|
|
248
626
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
249
627
|
durationSeconds: duration,
|
|
250
|
-
fixtures: manifestItems
|
|
628
|
+
fixtures: manifestItems,
|
|
629
|
+
windowFixtures
|
|
251
630
|
};
|
|
252
|
-
writeFileSync(
|
|
631
|
+
writeFileSync(join4(fixtureDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
253
632
|
return { fixtureDir, manifest, generated, skipped, ffmpegPath, ffmpegMissing: false };
|
|
254
633
|
}
|
|
255
634
|
async function generateOne(plan, fixtureDir, ffmpegPath, quiet, overwrite) {
|
|
256
|
-
const outDir =
|
|
635
|
+
const outDir = join4(fixtureDir, plan.containerDir);
|
|
257
636
|
mkdirSync(outDir, { recursive: true });
|
|
258
|
-
const outFile =
|
|
637
|
+
const outFile = join4(fixtureDir, plan.mainFile);
|
|
259
638
|
if (!overwrite && existsSync(outFile)) {
|
|
260
639
|
return collectExisting(plan, fixtureDir);
|
|
261
640
|
}
|
|
262
641
|
if (plan.layout.isContainerDirectory) {
|
|
263
642
|
for (const f of readdirSync(outDir)) {
|
|
264
643
|
if (f === plan.layout.mainFileName) continue;
|
|
265
|
-
rmIfExists(
|
|
644
|
+
rmIfExists(join4(outDir, f));
|
|
266
645
|
}
|
|
267
646
|
}
|
|
268
647
|
const args = plan.ffmpegOutputArgs.concat([outFile]);
|
|
@@ -276,21 +655,89 @@ async function generateOne(plan, fixtureDir, ffmpegPath, quiet, overwrite) {
|
|
|
276
655
|
}
|
|
277
656
|
return collectExisting(plan, fixtureDir);
|
|
278
657
|
}
|
|
658
|
+
async function generateWindowSet(fixtureDir, ffmpegPath, quiet, overwrite) {
|
|
659
|
+
const outDir = join4(fixtureDir, WINDOW_CONTAINER_DIR);
|
|
660
|
+
mkdirSync(outDir, { recursive: true });
|
|
661
|
+
const useDrawtext = await supportsDrawtext(ffmpegPath);
|
|
662
|
+
const fontfile = useDrawtext ? pickFontfile() : void 0;
|
|
663
|
+
if (!useDrawtext && !quiet) {
|
|
664
|
+
process.stdout.write(
|
|
665
|
+
"[generate] drawtext \u6EE4\u955C\u7F3A\u5931\uFF0C\u70E7\u5F55\u7A97\u964D\u7EA7\u4E3A\u88F8 testsrc2\uFF08\u65E0\u70E7\u5F55\u6587\u5B57\uFF0C\u7F16\u7801\u53C2\u6570\u4E0D\u53D8\uFF1B\u5B8C\u6574\u547D\u4EE4\u89C1 README P3-H \u4E00\u8282\uFF09\n"
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const items = [];
|
|
669
|
+
let generated = 0;
|
|
670
|
+
let skipped = 0;
|
|
671
|
+
for (const spec of WINDOW_SPECS) {
|
|
672
|
+
const plan = buildWindowPlan(spec, { useDrawtext, fontfile });
|
|
673
|
+
const outFile = join4(fixtureDir, plan.mainFile);
|
|
674
|
+
if (!overwrite && existsSync(outFile)) {
|
|
675
|
+
items.push(collectWindowExisting(plan, fixtureDir));
|
|
676
|
+
skipped += 1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (!quiet) {
|
|
680
|
+
process.stdout.write(`[generate] window/${plan.id} -> ${plan.mainFile}
|
|
681
|
+
`);
|
|
682
|
+
}
|
|
683
|
+
const code = await runFfmpeg(ffmpegPath, plan.ffmpegOutputArgs.concat([outFile]), quiet);
|
|
684
|
+
if (code !== 0) {
|
|
685
|
+
throw new Error(`ffmpeg exited with code ${code} for window/${plan.id}`);
|
|
686
|
+
}
|
|
687
|
+
items.push(collectWindowExisting(plan, fixtureDir));
|
|
688
|
+
generated += 1;
|
|
689
|
+
}
|
|
690
|
+
return { items, generated, skipped };
|
|
691
|
+
}
|
|
692
|
+
function collectWindowExisting(plan, fixtureDir) {
|
|
693
|
+
const entries = [];
|
|
694
|
+
for (const rel of plan.files) {
|
|
695
|
+
const abs = join4(fixtureDir, rel);
|
|
696
|
+
if (!existsSync(abs)) continue;
|
|
697
|
+
entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
|
|
698
|
+
}
|
|
699
|
+
return { id: plan.id, codec: plan.spec.codec, entries, encode: plan.encode };
|
|
700
|
+
}
|
|
701
|
+
async function supportsDrawtext(ffmpegPath) {
|
|
702
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
703
|
+
const proc = spawn(ffmpegPath, ["-hide_banner", "-filters"], {
|
|
704
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
705
|
+
});
|
|
706
|
+
let out = "";
|
|
707
|
+
proc.stdout?.on("data", (chunk) => {
|
|
708
|
+
out += chunk.toString("utf8");
|
|
709
|
+
});
|
|
710
|
+
proc.once("error", () => resolve2(false));
|
|
711
|
+
proc.once("exit", (code) => {
|
|
712
|
+
if (code !== 0) {
|
|
713
|
+
resolve2(false);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
resolve2(/^\s*\S{2,3}\s+drawtext\s/m.test(out));
|
|
717
|
+
});
|
|
718
|
+
return promise;
|
|
719
|
+
}
|
|
720
|
+
function pickFontfile() {
|
|
721
|
+
for (const candidate of DRAWTEXT_FONT_CANDIDATES) {
|
|
722
|
+
if (existsSync(candidate)) return candidate;
|
|
723
|
+
}
|
|
724
|
+
return DRAWTEXT_FONT_CANDIDATES[0] ?? "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
|
|
725
|
+
}
|
|
279
726
|
function collectExisting(plan, fixtureDir) {
|
|
280
727
|
const entries = [];
|
|
281
728
|
for (const rel of plan.files) {
|
|
282
|
-
const abs =
|
|
729
|
+
const abs = join4(fixtureDir, rel);
|
|
283
730
|
if (!existsSync(abs)) continue;
|
|
284
731
|
entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
|
|
285
732
|
}
|
|
286
733
|
if (plan.layout.isContainerDirectory) {
|
|
287
|
-
const absDir =
|
|
734
|
+
const absDir = join4(fixtureDir, plan.containerDir);
|
|
288
735
|
if (existsSync(absDir)) {
|
|
289
736
|
for (const name of readdirSync(absDir).sort()) {
|
|
290
737
|
if (name === plan.layout.mainFileName) continue;
|
|
291
|
-
const rel =
|
|
738
|
+
const rel = join4(plan.containerDir, name);
|
|
292
739
|
if (entries.find((e) => e.path === rel)) continue;
|
|
293
|
-
const abs =
|
|
740
|
+
const abs = join4(absDir, name);
|
|
294
741
|
if (!statSync(abs).isFile()) continue;
|
|
295
742
|
entries.push({ path: rel, size: statSync(abs).size, sha256: hashFile(abs) });
|
|
296
743
|
}
|
|
@@ -299,12 +746,12 @@ function collectExisting(plan, fixtureDir) {
|
|
|
299
746
|
return { codec: plan.codec, format: plan.format, entries };
|
|
300
747
|
}
|
|
301
748
|
function copyVodSamples(fixtureDir) {
|
|
302
|
-
const src =
|
|
749
|
+
const src = join4(fixtureDir, VOD_SAMPLE_RELATIVE);
|
|
303
750
|
if (!existsSync(src)) return;
|
|
304
|
-
const vodDir =
|
|
751
|
+
const vodDir = join4(fixtureDir, "vod");
|
|
305
752
|
mkdirSync(vodDir, { recursive: true });
|
|
306
753
|
for (const name of ["sample-1.mp4", "sample-2.mp4"]) {
|
|
307
|
-
const dst =
|
|
754
|
+
const dst = join4(vodDir, name);
|
|
308
755
|
writeFileSync(dst, readFileSync(src));
|
|
309
756
|
}
|
|
310
757
|
}
|
|
@@ -353,7 +800,8 @@ function emptyManifest(duration) {
|
|
|
353
800
|
version: 1,
|
|
354
801
|
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
355
802
|
durationSeconds: duration,
|
|
356
|
-
fixtures: []
|
|
803
|
+
fixtures: [],
|
|
804
|
+
windowFixtures: []
|
|
357
805
|
};
|
|
358
806
|
}
|
|
359
807
|
export {
|