simple-ffmpegjs 0.5.2 → 0.5.4
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 +4 -1
- package/package.json +10 -3
- package/src/core/media_info.js +5 -5
- package/src/core/resolve.js +33 -0
- package/src/core/rotation.js +4 -4
- package/src/core/validation.js +255 -172
- package/src/ffmpeg/audio_builder.js +1 -1
- package/src/ffmpeg/bgm_builder.js +5 -5
- package/src/ffmpeg/command_builder.js +6 -6
- package/src/ffmpeg/effect_builder.js +11 -11
- package/src/ffmpeg/standalone_audio_builder.js +75 -0
- package/src/ffmpeg/strings.js +4 -17
- package/src/ffmpeg/subtitle_builder.js +6 -14
- package/src/ffmpeg/text_passes.js +33 -8
- package/src/ffmpeg/text_renderer.js +17 -17
- package/src/ffmpeg/video_builder.js +6 -4
- package/src/ffmpeg/watermark_builder.js +1 -1
- package/src/lib/utils.js +4 -4
- package/src/loaders.js +8 -8
- package/src/schema/formatter.js +15 -15
- package/src/schema/index.js +4 -4
- package/src/schema/modules/effect.js +5 -4
- package/src/schema/modules/music.js +1 -1
- package/src/schema/modules/text.js +4 -3
- package/src/simpleffmpeg.js +180 -167
- package/types/index.d.mts +33 -39
- package/types/index.d.ts +33 -39
package/src/core/validation.js
CHANGED
|
@@ -169,8 +169,8 @@ function validateMediaUrlExtension(clip, clipPath, errors) {
|
|
|
169
169
|
ValidationCodes.INVALID_FORMAT,
|
|
170
170
|
`${clipPath}.url`,
|
|
171
171
|
`URL extension '${ext || "(none)"}' does not match clip type '${clip.type}'. Expected a ${expectedLabel} file extension, not ${oppositeLabel}.`,
|
|
172
|
-
clip.url
|
|
173
|
-
)
|
|
172
|
+
clip.url,
|
|
173
|
+
),
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
@@ -183,8 +183,8 @@ function validateFiniteNumber(value, path, errors, opts = {}) {
|
|
|
183
183
|
ValidationCodes.INVALID_VALUE,
|
|
184
184
|
path,
|
|
185
185
|
"Must be a finite number",
|
|
186
|
-
value
|
|
187
|
-
)
|
|
186
|
+
value,
|
|
187
|
+
),
|
|
188
188
|
);
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
@@ -196,8 +196,8 @@ function validateFiniteNumber(value, path, errors, opts = {}) {
|
|
|
196
196
|
ValidationCodes.INVALID_RANGE,
|
|
197
197
|
path,
|
|
198
198
|
minInclusive ? `Must be >= ${min}` : `Must be > ${min}`,
|
|
199
|
-
value
|
|
200
|
-
)
|
|
199
|
+
value,
|
|
200
|
+
),
|
|
201
201
|
);
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
@@ -210,8 +210,8 @@ function validateFiniteNumber(value, path, errors, opts = {}) {
|
|
|
210
210
|
ValidationCodes.INVALID_RANGE,
|
|
211
211
|
path,
|
|
212
212
|
maxInclusive ? `Must be <= ${max}` : `Must be < ${max}`,
|
|
213
|
-
value
|
|
214
|
-
)
|
|
213
|
+
value,
|
|
214
|
+
),
|
|
215
215
|
);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
@@ -224,8 +224,8 @@ function validateEffectClip(clip, path, errors) {
|
|
|
224
224
|
ValidationCodes.INVALID_VALUE,
|
|
225
225
|
`${path}.effect`,
|
|
226
226
|
`Invalid effect '${clip.effect}'. Expected: ${EFFECT_TYPES.join(", ")}`,
|
|
227
|
-
clip.effect
|
|
228
|
-
)
|
|
227
|
+
clip.effect,
|
|
228
|
+
),
|
|
229
229
|
);
|
|
230
230
|
}
|
|
231
231
|
|
|
@@ -244,8 +244,8 @@ function validateEffectClip(clip, path, errors) {
|
|
|
244
244
|
ValidationCodes.INVALID_TIMELINE,
|
|
245
245
|
`${path}`,
|
|
246
246
|
`fadeIn + fadeOut (${fadeTotal}) must be <= clip duration (${duration})`,
|
|
247
|
-
{ fadeIn: clip.fadeIn || 0, fadeOut: clip.fadeOut || 0, duration }
|
|
248
|
-
)
|
|
247
|
+
{ fadeIn: clip.fadeIn || 0, fadeOut: clip.fadeOut || 0, duration },
|
|
248
|
+
),
|
|
249
249
|
);
|
|
250
250
|
}
|
|
251
251
|
}
|
|
@@ -260,8 +260,8 @@ function validateEffectClip(clip, path, errors) {
|
|
|
260
260
|
ValidationCodes.MISSING_REQUIRED,
|
|
261
261
|
`${path}.params`,
|
|
262
262
|
"params is required and must be an object for effect clips",
|
|
263
|
-
clip.params
|
|
264
|
-
)
|
|
263
|
+
clip.params,
|
|
264
|
+
),
|
|
265
265
|
);
|
|
266
266
|
return;
|
|
267
267
|
}
|
|
@@ -294,8 +294,8 @@ function validateEffectClip(clip, path, errors) {
|
|
|
294
294
|
ValidationCodes.INVALID_VALUE,
|
|
295
295
|
`${path}.params.temporal`,
|
|
296
296
|
"temporal must be a boolean",
|
|
297
|
-
params.temporal
|
|
298
|
-
)
|
|
297
|
+
params.temporal,
|
|
298
|
+
),
|
|
299
299
|
);
|
|
300
300
|
}
|
|
301
301
|
} else if (clip.effect === "gaussianBlur") {
|
|
@@ -360,15 +360,26 @@ function validateEffectClip(clip, path, errors) {
|
|
|
360
360
|
max: 0.5,
|
|
361
361
|
});
|
|
362
362
|
}
|
|
363
|
-
if (params.color != null
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
if (params.color != null) {
|
|
364
|
+
if (typeof params.color !== "string") {
|
|
365
|
+
errors.push(
|
|
366
|
+
createIssue(
|
|
367
|
+
ValidationCodes.INVALID_VALUE,
|
|
368
|
+
`${path}.params.color`,
|
|
369
|
+
"color must be a string",
|
|
370
|
+
params.color,
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
} else if (!isValidFFmpegColor(params.color)) {
|
|
374
|
+
errors.push(
|
|
375
|
+
createIssue(
|
|
376
|
+
ValidationCodes.INVALID_VALUE,
|
|
377
|
+
`${path}.params.color`,
|
|
378
|
+
`invalid color "${params.color}". Use a named color (e.g. "black"), hex (#RRGGBB), or color@alpha format.`,
|
|
379
|
+
params.color,
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
372
383
|
}
|
|
373
384
|
}
|
|
374
385
|
}
|
|
@@ -402,8 +413,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
402
413
|
ValidationCodes.MISSING_REQUIRED,
|
|
403
414
|
`${path}.type`,
|
|
404
415
|
"Clip type is required",
|
|
405
|
-
undefined
|
|
406
|
-
)
|
|
416
|
+
undefined,
|
|
417
|
+
),
|
|
407
418
|
);
|
|
408
419
|
return { errors, warnings }; // Can't validate further without type
|
|
409
420
|
}
|
|
@@ -414,8 +425,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
414
425
|
ValidationCodes.INVALID_TYPE,
|
|
415
426
|
`${path}.type`,
|
|
416
427
|
`Invalid clip type '${clip.type}'. Expected: ${validTypes.join(", ")}`,
|
|
417
|
-
clip.type
|
|
418
|
-
)
|
|
428
|
+
clip.type,
|
|
429
|
+
),
|
|
419
430
|
);
|
|
420
431
|
return { errors, warnings }; // Can't validate further with invalid type
|
|
421
432
|
}
|
|
@@ -428,8 +439,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
428
439
|
ValidationCodes.INVALID_VALUE,
|
|
429
440
|
`${path}.duration`,
|
|
430
441
|
"Duration must be a number",
|
|
431
|
-
clip.duration
|
|
432
|
-
)
|
|
442
|
+
clip.duration,
|
|
443
|
+
),
|
|
433
444
|
);
|
|
434
445
|
} else if (!Number.isFinite(clip.duration)) {
|
|
435
446
|
errors.push(
|
|
@@ -437,8 +448,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
437
448
|
ValidationCodes.INVALID_VALUE,
|
|
438
449
|
`${path}.duration`,
|
|
439
450
|
"Duration must be a finite number (not NaN or Infinity)",
|
|
440
|
-
clip.duration
|
|
441
|
-
)
|
|
451
|
+
clip.duration,
|
|
452
|
+
),
|
|
442
453
|
);
|
|
443
454
|
} else if (clip.duration <= 0) {
|
|
444
455
|
errors.push(
|
|
@@ -446,8 +457,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
446
457
|
ValidationCodes.INVALID_RANGE,
|
|
447
458
|
`${path}.duration`,
|
|
448
459
|
"Duration must be greater than 0",
|
|
449
|
-
clip.duration
|
|
450
|
-
)
|
|
460
|
+
clip.duration,
|
|
461
|
+
),
|
|
451
462
|
);
|
|
452
463
|
}
|
|
453
464
|
// Conflict check: duration + end both set
|
|
@@ -457,26 +468,51 @@ function validateClip(clip, index, options = {}) {
|
|
|
457
468
|
ValidationCodes.INVALID_VALUE,
|
|
458
469
|
`${path}`,
|
|
459
470
|
"Cannot specify both 'duration' and 'end'. Use one or the other.",
|
|
460
|
-
{ duration: clip.duration, end: clip.end }
|
|
461
|
-
)
|
|
471
|
+
{ duration: clip.duration, end: clip.end },
|
|
472
|
+
),
|
|
462
473
|
);
|
|
463
474
|
}
|
|
464
475
|
}
|
|
465
476
|
|
|
466
|
-
//
|
|
477
|
+
// fullDuration validation
|
|
478
|
+
const fullDurationTypes = ["effect", "text"];
|
|
479
|
+
if (clip.fullDuration != null) {
|
|
480
|
+
if (clip.fullDuration !== true) {
|
|
481
|
+
errors.push(
|
|
482
|
+
createIssue(
|
|
483
|
+
ValidationCodes.INVALID_VALUE,
|
|
484
|
+
`${path}.fullDuration`,
|
|
485
|
+
"fullDuration must be true when specified",
|
|
486
|
+
clip.fullDuration,
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
} else if (!fullDurationTypes.includes(clip.type)) {
|
|
490
|
+
errors.push(
|
|
491
|
+
createIssue(
|
|
492
|
+
ValidationCodes.INVALID_VALUE,
|
|
493
|
+
`${path}.fullDuration`,
|
|
494
|
+
`fullDuration is only supported on ${fullDurationTypes.join(", ")} clips`,
|
|
495
|
+
clip.type,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Types that require position/end on timeline (unless fullDuration is set)
|
|
502
|
+
const hasFullDuration = clip.fullDuration === true && fullDurationTypes.includes(clip.type);
|
|
467
503
|
const requiresTimeline = ["video", "audio", "text", "image", "color", "effect"].includes(
|
|
468
|
-
clip.type
|
|
504
|
+
clip.type,
|
|
469
505
|
);
|
|
470
506
|
|
|
471
|
-
if (requiresTimeline) {
|
|
507
|
+
if (requiresTimeline && !hasFullDuration) {
|
|
472
508
|
if (typeof clip.position !== "number") {
|
|
473
509
|
errors.push(
|
|
474
510
|
createIssue(
|
|
475
511
|
ValidationCodes.MISSING_REQUIRED,
|
|
476
512
|
`${path}.position`,
|
|
477
513
|
"Position is required for this clip type",
|
|
478
|
-
clip.position
|
|
479
|
-
)
|
|
514
|
+
clip.position,
|
|
515
|
+
),
|
|
480
516
|
);
|
|
481
517
|
} else if (!Number.isFinite(clip.position)) {
|
|
482
518
|
errors.push(
|
|
@@ -484,8 +520,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
484
520
|
ValidationCodes.INVALID_VALUE,
|
|
485
521
|
`${path}.position`,
|
|
486
522
|
"Position must be a finite number (not NaN or Infinity)",
|
|
487
|
-
clip.position
|
|
488
|
-
)
|
|
523
|
+
clip.position,
|
|
524
|
+
),
|
|
489
525
|
);
|
|
490
526
|
} else if (clip.position < 0) {
|
|
491
527
|
errors.push(
|
|
@@ -493,8 +529,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
493
529
|
ValidationCodes.INVALID_RANGE,
|
|
494
530
|
`${path}.position`,
|
|
495
531
|
"Position must be >= 0",
|
|
496
|
-
clip.position
|
|
497
|
-
)
|
|
532
|
+
clip.position,
|
|
533
|
+
),
|
|
498
534
|
);
|
|
499
535
|
}
|
|
500
536
|
|
|
@@ -504,8 +540,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
504
540
|
ValidationCodes.MISSING_REQUIRED,
|
|
505
541
|
`${path}.end`,
|
|
506
542
|
"End time is required for this clip type",
|
|
507
|
-
clip.end
|
|
508
|
-
)
|
|
543
|
+
clip.end,
|
|
544
|
+
),
|
|
509
545
|
);
|
|
510
546
|
} else if (!Number.isFinite(clip.end)) {
|
|
511
547
|
errors.push(
|
|
@@ -513,8 +549,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
513
549
|
ValidationCodes.INVALID_VALUE,
|
|
514
550
|
`${path}.end`,
|
|
515
551
|
"End time must be a finite number (not NaN or Infinity)",
|
|
516
|
-
clip.end
|
|
517
|
-
)
|
|
552
|
+
clip.end,
|
|
553
|
+
),
|
|
518
554
|
);
|
|
519
555
|
} else if (Number.isFinite(clip.position) && clip.end <= clip.position) {
|
|
520
556
|
errors.push(
|
|
@@ -522,8 +558,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
522
558
|
ValidationCodes.INVALID_TIMELINE,
|
|
523
559
|
`${path}.end`,
|
|
524
560
|
`End time (${clip.end}) must be greater than position (${clip.position})`,
|
|
525
|
-
clip.end
|
|
526
|
-
)
|
|
561
|
+
clip.end,
|
|
562
|
+
),
|
|
527
563
|
);
|
|
528
564
|
}
|
|
529
565
|
} else {
|
|
@@ -535,8 +571,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
535
571
|
ValidationCodes.INVALID_VALUE,
|
|
536
572
|
`${path}.position`,
|
|
537
573
|
"Position must be a finite number (not NaN or Infinity)",
|
|
538
|
-
clip.position
|
|
539
|
-
)
|
|
574
|
+
clip.position,
|
|
575
|
+
),
|
|
540
576
|
);
|
|
541
577
|
} else if (clip.position < 0) {
|
|
542
578
|
errors.push(
|
|
@@ -544,8 +580,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
544
580
|
ValidationCodes.INVALID_RANGE,
|
|
545
581
|
`${path}.position`,
|
|
546
582
|
"Position must be >= 0",
|
|
547
|
-
clip.position
|
|
548
|
-
)
|
|
583
|
+
clip.position,
|
|
584
|
+
),
|
|
549
585
|
);
|
|
550
586
|
}
|
|
551
587
|
}
|
|
@@ -556,8 +592,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
556
592
|
ValidationCodes.INVALID_VALUE,
|
|
557
593
|
`${path}.end`,
|
|
558
594
|
"End time must be a finite number (not NaN or Infinity)",
|
|
559
|
-
clip.end
|
|
560
|
-
)
|
|
595
|
+
clip.end,
|
|
596
|
+
),
|
|
561
597
|
);
|
|
562
598
|
} else if (
|
|
563
599
|
typeof clip.position === "number" &&
|
|
@@ -569,8 +605,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
569
605
|
ValidationCodes.INVALID_TIMELINE,
|
|
570
606
|
`${path}.end`,
|
|
571
607
|
`End time (${clip.end}) must be greater than position (${clip.position})`,
|
|
572
|
-
clip.end
|
|
573
|
-
)
|
|
608
|
+
clip.end,
|
|
609
|
+
),
|
|
574
610
|
);
|
|
575
611
|
}
|
|
576
612
|
}
|
|
@@ -585,8 +621,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
585
621
|
ValidationCodes.MISSING_REQUIRED,
|
|
586
622
|
`${path}.url`,
|
|
587
623
|
"URL is required for media clips",
|
|
588
|
-
clip.url
|
|
589
|
-
)
|
|
624
|
+
clip.url,
|
|
625
|
+
),
|
|
590
626
|
);
|
|
591
627
|
} else if (!skipFileChecks) {
|
|
592
628
|
try {
|
|
@@ -596,8 +632,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
596
632
|
ValidationCodes.FILE_NOT_FOUND,
|
|
597
633
|
`${path}.url`,
|
|
598
634
|
`File not found: '${clip.url}'`,
|
|
599
|
-
clip.url
|
|
600
|
-
)
|
|
635
|
+
clip.url,
|
|
636
|
+
),
|
|
601
637
|
);
|
|
602
638
|
}
|
|
603
639
|
} catch (_) {}
|
|
@@ -612,8 +648,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
612
648
|
ValidationCodes.INVALID_VALUE,
|
|
613
649
|
`${path}.cutFrom`,
|
|
614
650
|
"cutFrom must be a finite number (not NaN or Infinity)",
|
|
615
|
-
clip.cutFrom
|
|
616
|
-
)
|
|
651
|
+
clip.cutFrom,
|
|
652
|
+
),
|
|
617
653
|
);
|
|
618
654
|
} else if (clip.cutFrom < 0) {
|
|
619
655
|
errors.push(
|
|
@@ -621,8 +657,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
621
657
|
ValidationCodes.INVALID_RANGE,
|
|
622
658
|
`${path}.cutFrom`,
|
|
623
659
|
"cutFrom must be >= 0",
|
|
624
|
-
clip.cutFrom
|
|
625
|
-
)
|
|
660
|
+
clip.cutFrom,
|
|
661
|
+
),
|
|
626
662
|
);
|
|
627
663
|
}
|
|
628
664
|
}
|
|
@@ -637,8 +673,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
637
673
|
ValidationCodes.INVALID_VALUE,
|
|
638
674
|
`${path}.volume`,
|
|
639
675
|
"Volume must be a finite number (not NaN or Infinity)",
|
|
640
|
-
clip.volume
|
|
641
|
-
)
|
|
676
|
+
clip.volume,
|
|
677
|
+
),
|
|
642
678
|
);
|
|
643
679
|
} else if (clip.volume < 0) {
|
|
644
680
|
errors.push(
|
|
@@ -646,8 +682,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
646
682
|
ValidationCodes.INVALID_RANGE,
|
|
647
683
|
`${path}.volume`,
|
|
648
684
|
"Volume must be >= 0",
|
|
649
|
-
clip.volume
|
|
650
|
-
)
|
|
685
|
+
clip.volume,
|
|
686
|
+
),
|
|
651
687
|
);
|
|
652
688
|
}
|
|
653
689
|
}
|
|
@@ -667,8 +703,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
667
703
|
ValidationCodes.MISSING_REQUIRED,
|
|
668
704
|
`${wordPath}.text`,
|
|
669
705
|
"Word text is required",
|
|
670
|
-
w.text
|
|
671
|
-
)
|
|
706
|
+
w.text,
|
|
707
|
+
),
|
|
672
708
|
);
|
|
673
709
|
}
|
|
674
710
|
|
|
@@ -678,8 +714,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
678
714
|
ValidationCodes.MISSING_REQUIRED,
|
|
679
715
|
`${wordPath}.start`,
|
|
680
716
|
"Word start time is required",
|
|
681
|
-
w.start
|
|
682
|
-
)
|
|
717
|
+
w.start,
|
|
718
|
+
),
|
|
683
719
|
);
|
|
684
720
|
} else if (!Number.isFinite(w.start)) {
|
|
685
721
|
errors.push(
|
|
@@ -687,8 +723,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
687
723
|
ValidationCodes.INVALID_VALUE,
|
|
688
724
|
`${wordPath}.start`,
|
|
689
725
|
"Word start time must be a finite number (not NaN or Infinity)",
|
|
690
|
-
w.start
|
|
691
|
-
)
|
|
726
|
+
w.start,
|
|
727
|
+
),
|
|
692
728
|
);
|
|
693
729
|
}
|
|
694
730
|
|
|
@@ -698,8 +734,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
698
734
|
ValidationCodes.MISSING_REQUIRED,
|
|
699
735
|
`${wordPath}.end`,
|
|
700
736
|
"Word end time is required",
|
|
701
|
-
w.end
|
|
702
|
-
)
|
|
737
|
+
w.end,
|
|
738
|
+
),
|
|
703
739
|
);
|
|
704
740
|
} else if (!Number.isFinite(w.end)) {
|
|
705
741
|
errors.push(
|
|
@@ -707,8 +743,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
707
743
|
ValidationCodes.INVALID_VALUE,
|
|
708
744
|
`${wordPath}.end`,
|
|
709
745
|
"Word end time must be a finite number (not NaN or Infinity)",
|
|
710
|
-
w.end
|
|
711
|
-
)
|
|
746
|
+
w.end,
|
|
747
|
+
),
|
|
712
748
|
);
|
|
713
749
|
}
|
|
714
750
|
|
|
@@ -722,26 +758,32 @@ function validateClip(clip, index, options = {}) {
|
|
|
722
758
|
ValidationCodes.INVALID_WORD_TIMING,
|
|
723
759
|
`${wordPath}.end`,
|
|
724
760
|
`Word end (${w.end}) must be greater than start (${w.start})`,
|
|
725
|
-
w.end
|
|
726
|
-
)
|
|
761
|
+
w.end,
|
|
762
|
+
),
|
|
727
763
|
);
|
|
728
764
|
}
|
|
729
765
|
|
|
730
766
|
// Check if word is within clip bounds
|
|
767
|
+
// Words can use absolute timings [clip.position, clip.end]
|
|
768
|
+
// or relative timings [0, clipDuration]. Accept either.
|
|
731
769
|
if (
|
|
732
770
|
typeof w.start === "number" &&
|
|
733
771
|
typeof w.end === "number" &&
|
|
734
772
|
typeof clip.position === "number" &&
|
|
735
773
|
typeof clip.end === "number"
|
|
736
774
|
) {
|
|
737
|
-
|
|
775
|
+
const clipDuration = clip.end - clip.position;
|
|
776
|
+
const inAbsolute =
|
|
777
|
+
w.start >= clip.position && w.end <= clip.end;
|
|
778
|
+
const inRelative = w.start >= 0 && w.end <= clipDuration;
|
|
779
|
+
if (!inAbsolute && !inRelative) {
|
|
738
780
|
warnings.push(
|
|
739
781
|
createIssue(
|
|
740
782
|
ValidationCodes.OUTSIDE_BOUNDS,
|
|
741
783
|
wordPath,
|
|
742
|
-
`Word timing [${w.start}, ${w.end}] outside clip bounds [${clip.position}, ${clip.end}]`,
|
|
743
|
-
{ start: w.start, end: w.end }
|
|
744
|
-
)
|
|
784
|
+
`Word timing [${w.start}, ${w.end}] outside clip bounds [${clip.position}, ${clip.end}] (duration: ${clipDuration}s)`,
|
|
785
|
+
{ start: w.start, end: w.end },
|
|
786
|
+
),
|
|
745
787
|
);
|
|
746
788
|
}
|
|
747
789
|
}
|
|
@@ -758,8 +800,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
758
800
|
ValidationCodes.INVALID_VALUE,
|
|
759
801
|
`${path}.wordTimestamps[${i}]`,
|
|
760
802
|
"Word timestamps must be numbers",
|
|
761
|
-
ts[i]
|
|
762
|
-
)
|
|
803
|
+
ts[i],
|
|
804
|
+
),
|
|
763
805
|
);
|
|
764
806
|
break;
|
|
765
807
|
}
|
|
@@ -769,8 +811,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
769
811
|
ValidationCodes.INVALID_WORD_TIMING,
|
|
770
812
|
`${path}.wordTimestamps[${i}]`,
|
|
771
813
|
`Timestamps must be non-decreasing (${ts[i - 1]} -> ${ts[i]})`,
|
|
772
|
-
ts[i]
|
|
773
|
-
)
|
|
814
|
+
ts[i],
|
|
815
|
+
),
|
|
774
816
|
);
|
|
775
817
|
break;
|
|
776
818
|
}
|
|
@@ -786,8 +828,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
786
828
|
ValidationCodes.FILE_NOT_FOUND,
|
|
787
829
|
`${path}.fontFile`,
|
|
788
830
|
`Font file not found: '${clip.fontFile}'. Will fall back to fontFamily.`,
|
|
789
|
-
clip.fontFile
|
|
790
|
-
)
|
|
831
|
+
clip.fontFile,
|
|
832
|
+
),
|
|
791
833
|
);
|
|
792
834
|
}
|
|
793
835
|
} catch (_) {}
|
|
@@ -804,8 +846,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
804
846
|
ValidationCodes.INVALID_VALUE,
|
|
805
847
|
`${path}.text`,
|
|
806
848
|
"Multiline text is only supported in karaoke mode. Newlines will be replaced with spaces.",
|
|
807
|
-
clip.text
|
|
808
|
-
)
|
|
849
|
+
clip.text,
|
|
850
|
+
),
|
|
809
851
|
);
|
|
810
852
|
}
|
|
811
853
|
|
|
@@ -817,8 +859,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
817
859
|
ValidationCodes.INVALID_VALUE,
|
|
818
860
|
`${path}.mode`,
|
|
819
861
|
`Invalid mode '${clip.mode}'. Expected: ${validModes.join(", ")}`,
|
|
820
|
-
clip.mode
|
|
821
|
-
)
|
|
862
|
+
clip.mode,
|
|
863
|
+
),
|
|
822
864
|
);
|
|
823
865
|
}
|
|
824
866
|
|
|
@@ -833,8 +875,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
833
875
|
`Invalid highlightStyle '${
|
|
834
876
|
clip.highlightStyle
|
|
835
877
|
}'. Expected: ${validStyles.join(", ")}`,
|
|
836
|
-
clip.highlightStyle
|
|
837
|
-
)
|
|
878
|
+
clip.highlightStyle,
|
|
879
|
+
),
|
|
838
880
|
);
|
|
839
881
|
}
|
|
840
882
|
}
|
|
@@ -863,8 +905,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
863
905
|
`Invalid animation type '${
|
|
864
906
|
clip.animation.type
|
|
865
907
|
}'. Expected: ${validAnimations.join(", ")}`,
|
|
866
|
-
clip.animation.type
|
|
867
|
-
)
|
|
908
|
+
clip.animation.type,
|
|
909
|
+
),
|
|
868
910
|
);
|
|
869
911
|
}
|
|
870
912
|
}
|
|
@@ -885,8 +927,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
885
927
|
ValidationCodes.INVALID_VALUE,
|
|
886
928
|
`${path}.${prop}`,
|
|
887
929
|
`Invalid color "${clip[prop]}". Use a named color (e.g. "white", "red"), hex (#RRGGBB), or color@alpha (e.g. "black@0.5").`,
|
|
888
|
-
clip[prop]
|
|
889
|
-
)
|
|
930
|
+
clip[prop],
|
|
931
|
+
),
|
|
890
932
|
);
|
|
891
933
|
}
|
|
892
934
|
}
|
|
@@ -901,8 +943,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
901
943
|
ValidationCodes.MISSING_REQUIRED,
|
|
902
944
|
`${path}.url`,
|
|
903
945
|
"URL is required for subtitle clips",
|
|
904
|
-
clip.url
|
|
905
|
-
)
|
|
946
|
+
clip.url,
|
|
947
|
+
),
|
|
906
948
|
);
|
|
907
949
|
} else {
|
|
908
950
|
// Check file extension
|
|
@@ -916,8 +958,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
916
958
|
`Unsupported subtitle format '.${ext}'. Expected: ${validExts
|
|
917
959
|
.map((e) => "." + e)
|
|
918
960
|
.join(", ")}`,
|
|
919
|
-
clip.url
|
|
920
|
-
)
|
|
961
|
+
clip.url,
|
|
962
|
+
),
|
|
921
963
|
);
|
|
922
964
|
}
|
|
923
965
|
|
|
@@ -930,8 +972,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
930
972
|
ValidationCodes.FILE_NOT_FOUND,
|
|
931
973
|
`${path}.url`,
|
|
932
974
|
`Subtitle file not found: '${clip.url}'`,
|
|
933
|
-
clip.url
|
|
934
|
-
)
|
|
975
|
+
clip.url,
|
|
976
|
+
),
|
|
935
977
|
);
|
|
936
978
|
}
|
|
937
979
|
} catch (_) {}
|
|
@@ -945,8 +987,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
945
987
|
ValidationCodes.INVALID_RANGE,
|
|
946
988
|
`${path}.position`,
|
|
947
989
|
"Subtitle position offset must be >= 0",
|
|
948
|
-
clip.position
|
|
949
|
-
)
|
|
990
|
+
clip.position,
|
|
991
|
+
),
|
|
950
992
|
);
|
|
951
993
|
}
|
|
952
994
|
|
|
@@ -960,8 +1002,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
960
1002
|
ValidationCodes.INVALID_VALUE,
|
|
961
1003
|
`${path}.${prop}`,
|
|
962
1004
|
`Invalid color "${clip[prop]}". Use a named color (e.g. "white", "red"), hex (#RRGGBB), or color@alpha (e.g. "black@0.5").`,
|
|
963
|
-
clip[prop]
|
|
964
|
-
)
|
|
1005
|
+
clip[prop],
|
|
1006
|
+
),
|
|
965
1007
|
);
|
|
966
1008
|
}
|
|
967
1009
|
}
|
|
@@ -978,8 +1020,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
978
1020
|
ValidationCodes.INVALID_VALUE,
|
|
979
1021
|
`${path}.imageFit`,
|
|
980
1022
|
`Invalid imageFit '${clip.imageFit}'. Expected: ${validImageFit.join(", ")}`,
|
|
981
|
-
clip.imageFit
|
|
982
|
-
)
|
|
1023
|
+
clip.imageFit,
|
|
1024
|
+
),
|
|
983
1025
|
);
|
|
984
1026
|
}
|
|
985
1027
|
}
|
|
@@ -991,8 +1033,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
991
1033
|
ValidationCodes.INVALID_TYPE,
|
|
992
1034
|
`${path}.blurIntensity`,
|
|
993
1035
|
`blurIntensity must be a finite number`,
|
|
994
|
-
clip.blurIntensity
|
|
995
|
-
)
|
|
1036
|
+
clip.blurIntensity,
|
|
1037
|
+
),
|
|
996
1038
|
);
|
|
997
1039
|
} else if (clip.blurIntensity <= 0) {
|
|
998
1040
|
errors.push(
|
|
@@ -1000,8 +1042,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1000
1042
|
ValidationCodes.INVALID_RANGE,
|
|
1001
1043
|
`${path}.blurIntensity`,
|
|
1002
1044
|
`blurIntensity must be > 0`,
|
|
1003
|
-
clip.blurIntensity
|
|
1004
|
-
)
|
|
1045
|
+
clip.blurIntensity,
|
|
1046
|
+
),
|
|
1005
1047
|
);
|
|
1006
1048
|
}
|
|
1007
1049
|
}
|
|
@@ -1027,10 +1069,10 @@ function validateClip(clip, index, options = {}) {
|
|
|
1027
1069
|
ValidationCodes.INVALID_VALUE,
|
|
1028
1070
|
`${path}.kenBurns`,
|
|
1029
1071
|
`Invalid kenBurns effect '${kbType}'. Expected: ${validKenBurns.join(
|
|
1030
|
-
", "
|
|
1072
|
+
", ",
|
|
1031
1073
|
)}`,
|
|
1032
|
-
kbType
|
|
1033
|
-
)
|
|
1074
|
+
kbType,
|
|
1075
|
+
),
|
|
1034
1076
|
);
|
|
1035
1077
|
}
|
|
1036
1078
|
|
|
@@ -1054,10 +1096,10 @@ function validateClip(clip, index, options = {}) {
|
|
|
1054
1096
|
ValidationCodes.INVALID_VALUE,
|
|
1055
1097
|
`${path}.kenBurns.anchor`,
|
|
1056
1098
|
`Invalid kenBurns anchor '${anchor}'. Expected: ${validAnchors.join(
|
|
1057
|
-
", "
|
|
1099
|
+
", ",
|
|
1058
1100
|
)}`,
|
|
1059
|
-
anchor
|
|
1060
|
-
)
|
|
1101
|
+
anchor,
|
|
1102
|
+
),
|
|
1061
1103
|
);
|
|
1062
1104
|
}
|
|
1063
1105
|
}
|
|
@@ -1070,10 +1112,10 @@ function validateClip(clip, index, options = {}) {
|
|
|
1070
1112
|
ValidationCodes.INVALID_VALUE,
|
|
1071
1113
|
`${path}.kenBurns.easing`,
|
|
1072
1114
|
`Invalid kenBurns easing '${easing}'. Expected: ${validEasing.join(
|
|
1073
|
-
", "
|
|
1115
|
+
", ",
|
|
1074
1116
|
)}`,
|
|
1075
|
-
easing
|
|
1076
|
-
)
|
|
1117
|
+
easing,
|
|
1118
|
+
),
|
|
1077
1119
|
);
|
|
1078
1120
|
}
|
|
1079
1121
|
}
|
|
@@ -1097,8 +1139,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1097
1139
|
ValidationCodes.INVALID_TYPE,
|
|
1098
1140
|
`${path}.kenBurns.${field}`,
|
|
1099
1141
|
`kenBurns.${field} must be a finite number`,
|
|
1100
|
-
value
|
|
1101
|
-
)
|
|
1142
|
+
value,
|
|
1143
|
+
),
|
|
1102
1144
|
);
|
|
1103
1145
|
return;
|
|
1104
1146
|
}
|
|
@@ -1109,8 +1151,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1109
1151
|
ValidationCodes.INVALID_RANGE,
|
|
1110
1152
|
`${path}.kenBurns.${field}`,
|
|
1111
1153
|
`kenBurns.${field} must be > 0`,
|
|
1112
|
-
value
|
|
1113
|
-
)
|
|
1154
|
+
value,
|
|
1155
|
+
),
|
|
1114
1156
|
);
|
|
1115
1157
|
}
|
|
1116
1158
|
|
|
@@ -1119,15 +1161,15 @@ function validateClip(clip, index, options = {}) {
|
|
|
1119
1161
|
field === "startY" ||
|
|
1120
1162
|
field === "endX" ||
|
|
1121
1163
|
field === "endY") &&
|
|
1122
|
-
|
|
1164
|
+
(value < 0 || value > 1)
|
|
1123
1165
|
) {
|
|
1124
1166
|
errors.push(
|
|
1125
1167
|
createIssue(
|
|
1126
1168
|
ValidationCodes.INVALID_RANGE,
|
|
1127
1169
|
`${path}.kenBurns.${field}`,
|
|
1128
1170
|
`kenBurns.${field} must be between 0 and 1`,
|
|
1129
|
-
value
|
|
1130
|
-
)
|
|
1171
|
+
value,
|
|
1172
|
+
),
|
|
1131
1173
|
);
|
|
1132
1174
|
}
|
|
1133
1175
|
});
|
|
@@ -1149,7 +1191,7 @@ function validateClip(clip, index, options = {}) {
|
|
|
1149
1191
|
strictKenBurns
|
|
1150
1192
|
? `Image dimensions (${clip.width}x${clip.height}) are smaller than project dimensions (${projectWidth}x${projectHeight}). Ken Burns effects require images at least as large as the output.`
|
|
1151
1193
|
: `Image (${clip.width}x${clip.height}) will be upscaled to ${projectWidth}x${projectHeight} for Ken Burns effect. Quality may be reduced.`,
|
|
1152
|
-
{ width: clip.width, height: clip.height }
|
|
1194
|
+
{ width: clip.width, height: clip.height },
|
|
1153
1195
|
);
|
|
1154
1196
|
|
|
1155
1197
|
if (strictKenBurns) {
|
|
@@ -1166,8 +1208,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1166
1208
|
ValidationCodes.INVALID_VALUE,
|
|
1167
1209
|
`${path}`,
|
|
1168
1210
|
`Ken Burns effect on image - ensure source image is at least ${projectWidth}x${projectHeight}px for best quality (smaller images will be upscaled).`,
|
|
1169
|
-
clip.url
|
|
1170
|
-
)
|
|
1211
|
+
clip.url,
|
|
1212
|
+
),
|
|
1171
1213
|
);
|
|
1172
1214
|
}
|
|
1173
1215
|
}
|
|
@@ -1181,8 +1223,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1181
1223
|
ValidationCodes.MISSING_REQUIRED,
|
|
1182
1224
|
`${path}.color`,
|
|
1183
1225
|
"Color is required for color clips",
|
|
1184
|
-
clip.color
|
|
1185
|
-
)
|
|
1226
|
+
clip.color,
|
|
1227
|
+
),
|
|
1186
1228
|
);
|
|
1187
1229
|
} else if (typeof clip.color === "string") {
|
|
1188
1230
|
if (!isValidFFmpegColor(clip.color)) {
|
|
@@ -1191,8 +1233,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1191
1233
|
ValidationCodes.INVALID_VALUE,
|
|
1192
1234
|
`${path}.color`,
|
|
1193
1235
|
`Invalid color "${clip.color}". Use a named color (e.g. "black", "navy"), hex (#RRGGBB, 0xRRGGBB), or "random".`,
|
|
1194
|
-
clip.color
|
|
1195
|
-
)
|
|
1236
|
+
clip.color,
|
|
1237
|
+
),
|
|
1196
1238
|
);
|
|
1197
1239
|
}
|
|
1198
1240
|
} else if (typeof clip.color === "object" && clip.color !== null) {
|
|
@@ -1203,8 +1245,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1203
1245
|
ValidationCodes.INVALID_VALUE,
|
|
1204
1246
|
`${path}.color.type`,
|
|
1205
1247
|
`Invalid gradient type '${clip.color.type}'. Expected: ${validGradientTypes.join(", ")}`,
|
|
1206
|
-
clip.color.type
|
|
1207
|
-
)
|
|
1248
|
+
clip.color.type,
|
|
1249
|
+
),
|
|
1208
1250
|
);
|
|
1209
1251
|
}
|
|
1210
1252
|
if (!Array.isArray(clip.color.colors) || clip.color.colors.length < 2) {
|
|
@@ -1213,8 +1255,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1213
1255
|
ValidationCodes.INVALID_VALUE,
|
|
1214
1256
|
`${path}.color.colors`,
|
|
1215
1257
|
"Gradient colors must be an array of at least 2 color strings",
|
|
1216
|
-
clip.color.colors
|
|
1217
|
-
)
|
|
1258
|
+
clip.color.colors,
|
|
1259
|
+
),
|
|
1218
1260
|
);
|
|
1219
1261
|
} else {
|
|
1220
1262
|
clip.color.colors.forEach((c, ci) => {
|
|
@@ -1224,8 +1266,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1224
1266
|
ValidationCodes.INVALID_VALUE,
|
|
1225
1267
|
`${path}.color.colors[${ci}]`,
|
|
1226
1268
|
`Invalid gradient color "${c}". Use a named color (e.g. "black", "navy"), hex (#RRGGBB), or "random".`,
|
|
1227
|
-
c
|
|
1228
|
-
)
|
|
1269
|
+
c,
|
|
1270
|
+
),
|
|
1229
1271
|
);
|
|
1230
1272
|
}
|
|
1231
1273
|
});
|
|
@@ -1238,8 +1280,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1238
1280
|
ValidationCodes.INVALID_VALUE,
|
|
1239
1281
|
`${path}.color.direction`,
|
|
1240
1282
|
`Invalid gradient direction '${clip.color.direction}'. Expected: "vertical", "horizontal", or a number (angle in degrees)`,
|
|
1241
|
-
clip.color.direction
|
|
1242
|
-
)
|
|
1283
|
+
clip.color.direction,
|
|
1284
|
+
),
|
|
1243
1285
|
);
|
|
1244
1286
|
}
|
|
1245
1287
|
}
|
|
@@ -1249,8 +1291,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1249
1291
|
ValidationCodes.INVALID_VALUE,
|
|
1250
1292
|
`${path}.color`,
|
|
1251
1293
|
"Color must be a string (flat color) or an object (gradient spec)",
|
|
1252
|
-
clip.color
|
|
1253
|
-
)
|
|
1294
|
+
clip.color,
|
|
1295
|
+
),
|
|
1254
1296
|
);
|
|
1255
1297
|
}
|
|
1256
1298
|
}
|
|
@@ -1268,8 +1310,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1268
1310
|
ValidationCodes.INVALID_VALUE,
|
|
1269
1311
|
`${path}.transition.duration`,
|
|
1270
1312
|
"Transition duration must be a number",
|
|
1271
|
-
clip.transition.duration
|
|
1272
|
-
)
|
|
1313
|
+
clip.transition.duration,
|
|
1314
|
+
),
|
|
1273
1315
|
);
|
|
1274
1316
|
} else if (!Number.isFinite(clip.transition.duration)) {
|
|
1275
1317
|
errors.push(
|
|
@@ -1277,8 +1319,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1277
1319
|
ValidationCodes.INVALID_VALUE,
|
|
1278
1320
|
`${path}.transition.duration`,
|
|
1279
1321
|
"Transition duration must be a finite number (not NaN or Infinity)",
|
|
1280
|
-
clip.transition.duration
|
|
1281
|
-
)
|
|
1322
|
+
clip.transition.duration,
|
|
1323
|
+
),
|
|
1282
1324
|
);
|
|
1283
1325
|
} else if (clip.transition.duration <= 0) {
|
|
1284
1326
|
errors.push(
|
|
@@ -1286,8 +1328,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1286
1328
|
ValidationCodes.INVALID_VALUE,
|
|
1287
1329
|
`${path}.transition.duration`,
|
|
1288
1330
|
"Transition duration must be a positive number",
|
|
1289
|
-
clip.transition.duration
|
|
1290
|
-
)
|
|
1331
|
+
clip.transition.duration,
|
|
1332
|
+
),
|
|
1291
1333
|
);
|
|
1292
1334
|
}
|
|
1293
1335
|
}
|
|
@@ -1317,7 +1359,7 @@ function validateTimelineGaps(clips) {
|
|
|
1317
1359
|
.filter(({ clip }) => clip.type === "video" || clip.type === "image" || clip.type === "color")
|
|
1318
1360
|
.filter(
|
|
1319
1361
|
({ clip }) =>
|
|
1320
|
-
typeof clip.position === "number" && typeof clip.end === "number"
|
|
1362
|
+
typeof clip.position === "number" && typeof clip.end === "number",
|
|
1321
1363
|
)
|
|
1322
1364
|
.sort((a, b) => a.clip.position - b.clip.position);
|
|
1323
1365
|
|
|
@@ -1330,15 +1372,15 @@ function validateTimelineGaps(clips) {
|
|
|
1330
1372
|
ValidationCodes.TIMELINE_GAP,
|
|
1331
1373
|
"timeline",
|
|
1332
1374
|
`Gap at start of visual timeline [0, ${gap.end.toFixed(
|
|
1333
|
-
3
|
|
1375
|
+
3,
|
|
1334
1376
|
)}s]. If intentional, fill it with a { type: "color" } clip. Otherwise, start your first clip at position 0.`,
|
|
1335
|
-
{ start: gap.start, end: gap.end }
|
|
1336
|
-
)
|
|
1377
|
+
{ start: gap.start, end: gap.end },
|
|
1378
|
+
),
|
|
1337
1379
|
);
|
|
1338
1380
|
} else {
|
|
1339
1381
|
// Find the surrounding clip indices for a helpful message
|
|
1340
|
-
const before = visual.filter(v => v.clip.end <= gap.start + 1e-3);
|
|
1341
|
-
const after = visual.filter(v => v.clip.position >= gap.end - 1e-3);
|
|
1382
|
+
const before = visual.filter((v) => v.clip.end <= gap.start + 1e-3);
|
|
1383
|
+
const after = visual.filter((v) => v.clip.position >= gap.end - 1e-3);
|
|
1342
1384
|
const prevIdx = before.length > 0 ? before[before.length - 1].index : "?";
|
|
1343
1385
|
const nextIdx = after.length > 0 ? after[0].index : "?";
|
|
1344
1386
|
|
|
@@ -1347,10 +1389,10 @@ function validateTimelineGaps(clips) {
|
|
|
1347
1389
|
ValidationCodes.TIMELINE_GAP,
|
|
1348
1390
|
"timeline",
|
|
1349
1391
|
`Gap in visual timeline [${gap.start.toFixed(3)}s, ${gap.end.toFixed(
|
|
1350
|
-
3
|
|
1392
|
+
3,
|
|
1351
1393
|
)}s] between clips[${prevIdx}] and clips[${nextIdx}]. If intentional, fill it with a { type: "color" } clip. Otherwise, adjust clip positions to remove the gap.`,
|
|
1352
|
-
{ start: gap.start, end: gap.end }
|
|
1353
|
-
)
|
|
1394
|
+
{ start: gap.start, end: gap.end },
|
|
1395
|
+
),
|
|
1354
1396
|
);
|
|
1355
1397
|
}
|
|
1356
1398
|
}
|
|
@@ -1377,8 +1419,8 @@ function validateConfig(clips, options = {}) {
|
|
|
1377
1419
|
ValidationCodes.INVALID_TYPE,
|
|
1378
1420
|
"clips",
|
|
1379
1421
|
"Clips must be an array",
|
|
1380
|
-
typeof clips
|
|
1381
|
-
)
|
|
1422
|
+
typeof clips,
|
|
1423
|
+
),
|
|
1382
1424
|
);
|
|
1383
1425
|
return { valid: false, errors: allErrors, warnings: allWarnings };
|
|
1384
1426
|
}
|
|
@@ -1390,8 +1432,8 @@ function validateConfig(clips, options = {}) {
|
|
|
1390
1432
|
ValidationCodes.MISSING_REQUIRED,
|
|
1391
1433
|
"clips",
|
|
1392
1434
|
"At least one clip is required",
|
|
1393
|
-
[]
|
|
1394
|
-
)
|
|
1435
|
+
[],
|
|
1436
|
+
),
|
|
1395
1437
|
);
|
|
1396
1438
|
return { valid: false, errors: allErrors, warnings: allWarnings };
|
|
1397
1439
|
}
|
|
@@ -1408,6 +1450,47 @@ function validateConfig(clips, options = {}) {
|
|
|
1408
1450
|
allErrors.push(...gapResult.errors);
|
|
1409
1451
|
allWarnings.push(...gapResult.warnings);
|
|
1410
1452
|
|
|
1453
|
+
// Warn about non-visual clips positioned beyond the visual timeline
|
|
1454
|
+
const visualClips = clips.filter(
|
|
1455
|
+
(c) => c.type === "video" || c.type === "image" || c.type === "color",
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
if (visualClips.length > 0) {
|
|
1459
|
+
const visualBaseSum = visualClips.reduce(
|
|
1460
|
+
(acc, c) => acc + Math.max(0, (c.end || 0) - (c.position || 0)),
|
|
1461
|
+
0,
|
|
1462
|
+
);
|
|
1463
|
+
const visualTransitionOverlap = visualClips.reduce((acc, c) => {
|
|
1464
|
+
const d =
|
|
1465
|
+
c.transition && typeof c.transition.duration === "number"
|
|
1466
|
+
? c.transition.duration
|
|
1467
|
+
: 0;
|
|
1468
|
+
return acc + d;
|
|
1469
|
+
}, 0);
|
|
1470
|
+
const visualDuration = Math.max(0, visualBaseSum - visualTransitionOverlap);
|
|
1471
|
+
|
|
1472
|
+
if (visualDuration > 0) {
|
|
1473
|
+
const nonVisualTypes = ["text", "audio", "subtitle", "music", "backgroundAudio"];
|
|
1474
|
+
for (let i = 0; i < clips.length; i++) {
|
|
1475
|
+
const clip = clips[i];
|
|
1476
|
+
if (
|
|
1477
|
+
nonVisualTypes.includes(clip.type) &&
|
|
1478
|
+
typeof clip.position === "number" &&
|
|
1479
|
+
clip.position >= visualDuration
|
|
1480
|
+
) {
|
|
1481
|
+
allWarnings.push(
|
|
1482
|
+
createIssue(
|
|
1483
|
+
ValidationCodes.OUTSIDE_BOUNDS,
|
|
1484
|
+
`clips[${i}]`,
|
|
1485
|
+
`${clip.type} clip starts at ${clip.position}s but visual timeline ends at ${visualDuration}s`,
|
|
1486
|
+
{ position: clip.position, visualDuration },
|
|
1487
|
+
),
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1411
1494
|
return {
|
|
1412
1495
|
valid: allErrors.length === 0,
|
|
1413
1496
|
errors: allErrors,
|