simple-ffmpegjs 0.5.2 → 0.5.3
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 +3 -1
- package/package.json +10 -3
- package/src/core/media_info.js +5 -5
- package/src/core/rotation.js +4 -4
- package/src/core/validation.js +228 -170
- 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/music.js +1 -1
- package/src/simpleffmpeg.js +166 -167
- package/types/index.d.ts +20 -0
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,15 +468,15 @@ 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
|
// Types that require position/end on timeline
|
|
467
478
|
const requiresTimeline = ["video", "audio", "text", "image", "color", "effect"].includes(
|
|
468
|
-
clip.type
|
|
479
|
+
clip.type,
|
|
469
480
|
);
|
|
470
481
|
|
|
471
482
|
if (requiresTimeline) {
|
|
@@ -475,8 +486,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
475
486
|
ValidationCodes.MISSING_REQUIRED,
|
|
476
487
|
`${path}.position`,
|
|
477
488
|
"Position is required for this clip type",
|
|
478
|
-
clip.position
|
|
479
|
-
)
|
|
489
|
+
clip.position,
|
|
490
|
+
),
|
|
480
491
|
);
|
|
481
492
|
} else if (!Number.isFinite(clip.position)) {
|
|
482
493
|
errors.push(
|
|
@@ -484,8 +495,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
484
495
|
ValidationCodes.INVALID_VALUE,
|
|
485
496
|
`${path}.position`,
|
|
486
497
|
"Position must be a finite number (not NaN or Infinity)",
|
|
487
|
-
clip.position
|
|
488
|
-
)
|
|
498
|
+
clip.position,
|
|
499
|
+
),
|
|
489
500
|
);
|
|
490
501
|
} else if (clip.position < 0) {
|
|
491
502
|
errors.push(
|
|
@@ -493,8 +504,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
493
504
|
ValidationCodes.INVALID_RANGE,
|
|
494
505
|
`${path}.position`,
|
|
495
506
|
"Position must be >= 0",
|
|
496
|
-
clip.position
|
|
497
|
-
)
|
|
507
|
+
clip.position,
|
|
508
|
+
),
|
|
498
509
|
);
|
|
499
510
|
}
|
|
500
511
|
|
|
@@ -504,8 +515,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
504
515
|
ValidationCodes.MISSING_REQUIRED,
|
|
505
516
|
`${path}.end`,
|
|
506
517
|
"End time is required for this clip type",
|
|
507
|
-
clip.end
|
|
508
|
-
)
|
|
518
|
+
clip.end,
|
|
519
|
+
),
|
|
509
520
|
);
|
|
510
521
|
} else if (!Number.isFinite(clip.end)) {
|
|
511
522
|
errors.push(
|
|
@@ -513,8 +524,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
513
524
|
ValidationCodes.INVALID_VALUE,
|
|
514
525
|
`${path}.end`,
|
|
515
526
|
"End time must be a finite number (not NaN or Infinity)",
|
|
516
|
-
clip.end
|
|
517
|
-
)
|
|
527
|
+
clip.end,
|
|
528
|
+
),
|
|
518
529
|
);
|
|
519
530
|
} else if (Number.isFinite(clip.position) && clip.end <= clip.position) {
|
|
520
531
|
errors.push(
|
|
@@ -522,8 +533,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
522
533
|
ValidationCodes.INVALID_TIMELINE,
|
|
523
534
|
`${path}.end`,
|
|
524
535
|
`End time (${clip.end}) must be greater than position (${clip.position})`,
|
|
525
|
-
clip.end
|
|
526
|
-
)
|
|
536
|
+
clip.end,
|
|
537
|
+
),
|
|
527
538
|
);
|
|
528
539
|
}
|
|
529
540
|
} else {
|
|
@@ -535,8 +546,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
535
546
|
ValidationCodes.INVALID_VALUE,
|
|
536
547
|
`${path}.position`,
|
|
537
548
|
"Position must be a finite number (not NaN or Infinity)",
|
|
538
|
-
clip.position
|
|
539
|
-
)
|
|
549
|
+
clip.position,
|
|
550
|
+
),
|
|
540
551
|
);
|
|
541
552
|
} else if (clip.position < 0) {
|
|
542
553
|
errors.push(
|
|
@@ -544,8 +555,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
544
555
|
ValidationCodes.INVALID_RANGE,
|
|
545
556
|
`${path}.position`,
|
|
546
557
|
"Position must be >= 0",
|
|
547
|
-
clip.position
|
|
548
|
-
)
|
|
558
|
+
clip.position,
|
|
559
|
+
),
|
|
549
560
|
);
|
|
550
561
|
}
|
|
551
562
|
}
|
|
@@ -556,8 +567,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
556
567
|
ValidationCodes.INVALID_VALUE,
|
|
557
568
|
`${path}.end`,
|
|
558
569
|
"End time must be a finite number (not NaN or Infinity)",
|
|
559
|
-
clip.end
|
|
560
|
-
)
|
|
570
|
+
clip.end,
|
|
571
|
+
),
|
|
561
572
|
);
|
|
562
573
|
} else if (
|
|
563
574
|
typeof clip.position === "number" &&
|
|
@@ -569,8 +580,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
569
580
|
ValidationCodes.INVALID_TIMELINE,
|
|
570
581
|
`${path}.end`,
|
|
571
582
|
`End time (${clip.end}) must be greater than position (${clip.position})`,
|
|
572
|
-
clip.end
|
|
573
|
-
)
|
|
583
|
+
clip.end,
|
|
584
|
+
),
|
|
574
585
|
);
|
|
575
586
|
}
|
|
576
587
|
}
|
|
@@ -585,8 +596,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
585
596
|
ValidationCodes.MISSING_REQUIRED,
|
|
586
597
|
`${path}.url`,
|
|
587
598
|
"URL is required for media clips",
|
|
588
|
-
clip.url
|
|
589
|
-
)
|
|
599
|
+
clip.url,
|
|
600
|
+
),
|
|
590
601
|
);
|
|
591
602
|
} else if (!skipFileChecks) {
|
|
592
603
|
try {
|
|
@@ -596,8 +607,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
596
607
|
ValidationCodes.FILE_NOT_FOUND,
|
|
597
608
|
`${path}.url`,
|
|
598
609
|
`File not found: '${clip.url}'`,
|
|
599
|
-
clip.url
|
|
600
|
-
)
|
|
610
|
+
clip.url,
|
|
611
|
+
),
|
|
601
612
|
);
|
|
602
613
|
}
|
|
603
614
|
} catch (_) {}
|
|
@@ -612,8 +623,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
612
623
|
ValidationCodes.INVALID_VALUE,
|
|
613
624
|
`${path}.cutFrom`,
|
|
614
625
|
"cutFrom must be a finite number (not NaN or Infinity)",
|
|
615
|
-
clip.cutFrom
|
|
616
|
-
)
|
|
626
|
+
clip.cutFrom,
|
|
627
|
+
),
|
|
617
628
|
);
|
|
618
629
|
} else if (clip.cutFrom < 0) {
|
|
619
630
|
errors.push(
|
|
@@ -621,8 +632,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
621
632
|
ValidationCodes.INVALID_RANGE,
|
|
622
633
|
`${path}.cutFrom`,
|
|
623
634
|
"cutFrom must be >= 0",
|
|
624
|
-
clip.cutFrom
|
|
625
|
-
)
|
|
635
|
+
clip.cutFrom,
|
|
636
|
+
),
|
|
626
637
|
);
|
|
627
638
|
}
|
|
628
639
|
}
|
|
@@ -637,8 +648,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
637
648
|
ValidationCodes.INVALID_VALUE,
|
|
638
649
|
`${path}.volume`,
|
|
639
650
|
"Volume must be a finite number (not NaN or Infinity)",
|
|
640
|
-
clip.volume
|
|
641
|
-
)
|
|
651
|
+
clip.volume,
|
|
652
|
+
),
|
|
642
653
|
);
|
|
643
654
|
} else if (clip.volume < 0) {
|
|
644
655
|
errors.push(
|
|
@@ -646,8 +657,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
646
657
|
ValidationCodes.INVALID_RANGE,
|
|
647
658
|
`${path}.volume`,
|
|
648
659
|
"Volume must be >= 0",
|
|
649
|
-
clip.volume
|
|
650
|
-
)
|
|
660
|
+
clip.volume,
|
|
661
|
+
),
|
|
651
662
|
);
|
|
652
663
|
}
|
|
653
664
|
}
|
|
@@ -667,8 +678,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
667
678
|
ValidationCodes.MISSING_REQUIRED,
|
|
668
679
|
`${wordPath}.text`,
|
|
669
680
|
"Word text is required",
|
|
670
|
-
w.text
|
|
671
|
-
)
|
|
681
|
+
w.text,
|
|
682
|
+
),
|
|
672
683
|
);
|
|
673
684
|
}
|
|
674
685
|
|
|
@@ -678,8 +689,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
678
689
|
ValidationCodes.MISSING_REQUIRED,
|
|
679
690
|
`${wordPath}.start`,
|
|
680
691
|
"Word start time is required",
|
|
681
|
-
w.start
|
|
682
|
-
)
|
|
692
|
+
w.start,
|
|
693
|
+
),
|
|
683
694
|
);
|
|
684
695
|
} else if (!Number.isFinite(w.start)) {
|
|
685
696
|
errors.push(
|
|
@@ -687,8 +698,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
687
698
|
ValidationCodes.INVALID_VALUE,
|
|
688
699
|
`${wordPath}.start`,
|
|
689
700
|
"Word start time must be a finite number (not NaN or Infinity)",
|
|
690
|
-
w.start
|
|
691
|
-
)
|
|
701
|
+
w.start,
|
|
702
|
+
),
|
|
692
703
|
);
|
|
693
704
|
}
|
|
694
705
|
|
|
@@ -698,8 +709,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
698
709
|
ValidationCodes.MISSING_REQUIRED,
|
|
699
710
|
`${wordPath}.end`,
|
|
700
711
|
"Word end time is required",
|
|
701
|
-
w.end
|
|
702
|
-
)
|
|
712
|
+
w.end,
|
|
713
|
+
),
|
|
703
714
|
);
|
|
704
715
|
} else if (!Number.isFinite(w.end)) {
|
|
705
716
|
errors.push(
|
|
@@ -707,8 +718,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
707
718
|
ValidationCodes.INVALID_VALUE,
|
|
708
719
|
`${wordPath}.end`,
|
|
709
720
|
"Word end time must be a finite number (not NaN or Infinity)",
|
|
710
|
-
w.end
|
|
711
|
-
)
|
|
721
|
+
w.end,
|
|
722
|
+
),
|
|
712
723
|
);
|
|
713
724
|
}
|
|
714
725
|
|
|
@@ -722,26 +733,32 @@ function validateClip(clip, index, options = {}) {
|
|
|
722
733
|
ValidationCodes.INVALID_WORD_TIMING,
|
|
723
734
|
`${wordPath}.end`,
|
|
724
735
|
`Word end (${w.end}) must be greater than start (${w.start})`,
|
|
725
|
-
w.end
|
|
726
|
-
)
|
|
736
|
+
w.end,
|
|
737
|
+
),
|
|
727
738
|
);
|
|
728
739
|
}
|
|
729
740
|
|
|
730
741
|
// Check if word is within clip bounds
|
|
742
|
+
// Words can use absolute timings [clip.position, clip.end]
|
|
743
|
+
// or relative timings [0, clipDuration]. Accept either.
|
|
731
744
|
if (
|
|
732
745
|
typeof w.start === "number" &&
|
|
733
746
|
typeof w.end === "number" &&
|
|
734
747
|
typeof clip.position === "number" &&
|
|
735
748
|
typeof clip.end === "number"
|
|
736
749
|
) {
|
|
737
|
-
|
|
750
|
+
const clipDuration = clip.end - clip.position;
|
|
751
|
+
const inAbsolute =
|
|
752
|
+
w.start >= clip.position && w.end <= clip.end;
|
|
753
|
+
const inRelative = w.start >= 0 && w.end <= clipDuration;
|
|
754
|
+
if (!inAbsolute && !inRelative) {
|
|
738
755
|
warnings.push(
|
|
739
756
|
createIssue(
|
|
740
757
|
ValidationCodes.OUTSIDE_BOUNDS,
|
|
741
758
|
wordPath,
|
|
742
|
-
`Word timing [${w.start}, ${w.end}] outside clip bounds [${clip.position}, ${clip.end}]`,
|
|
743
|
-
{ start: w.start, end: w.end }
|
|
744
|
-
)
|
|
759
|
+
`Word timing [${w.start}, ${w.end}] outside clip bounds [${clip.position}, ${clip.end}] (duration: ${clipDuration}s)`,
|
|
760
|
+
{ start: w.start, end: w.end },
|
|
761
|
+
),
|
|
745
762
|
);
|
|
746
763
|
}
|
|
747
764
|
}
|
|
@@ -758,8 +775,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
758
775
|
ValidationCodes.INVALID_VALUE,
|
|
759
776
|
`${path}.wordTimestamps[${i}]`,
|
|
760
777
|
"Word timestamps must be numbers",
|
|
761
|
-
ts[i]
|
|
762
|
-
)
|
|
778
|
+
ts[i],
|
|
779
|
+
),
|
|
763
780
|
);
|
|
764
781
|
break;
|
|
765
782
|
}
|
|
@@ -769,8 +786,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
769
786
|
ValidationCodes.INVALID_WORD_TIMING,
|
|
770
787
|
`${path}.wordTimestamps[${i}]`,
|
|
771
788
|
`Timestamps must be non-decreasing (${ts[i - 1]} -> ${ts[i]})`,
|
|
772
|
-
ts[i]
|
|
773
|
-
)
|
|
789
|
+
ts[i],
|
|
790
|
+
),
|
|
774
791
|
);
|
|
775
792
|
break;
|
|
776
793
|
}
|
|
@@ -786,8 +803,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
786
803
|
ValidationCodes.FILE_NOT_FOUND,
|
|
787
804
|
`${path}.fontFile`,
|
|
788
805
|
`Font file not found: '${clip.fontFile}'. Will fall back to fontFamily.`,
|
|
789
|
-
clip.fontFile
|
|
790
|
-
)
|
|
806
|
+
clip.fontFile,
|
|
807
|
+
),
|
|
791
808
|
);
|
|
792
809
|
}
|
|
793
810
|
} catch (_) {}
|
|
@@ -804,8 +821,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
804
821
|
ValidationCodes.INVALID_VALUE,
|
|
805
822
|
`${path}.text`,
|
|
806
823
|
"Multiline text is only supported in karaoke mode. Newlines will be replaced with spaces.",
|
|
807
|
-
clip.text
|
|
808
|
-
)
|
|
824
|
+
clip.text,
|
|
825
|
+
),
|
|
809
826
|
);
|
|
810
827
|
}
|
|
811
828
|
|
|
@@ -817,8 +834,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
817
834
|
ValidationCodes.INVALID_VALUE,
|
|
818
835
|
`${path}.mode`,
|
|
819
836
|
`Invalid mode '${clip.mode}'. Expected: ${validModes.join(", ")}`,
|
|
820
|
-
clip.mode
|
|
821
|
-
)
|
|
837
|
+
clip.mode,
|
|
838
|
+
),
|
|
822
839
|
);
|
|
823
840
|
}
|
|
824
841
|
|
|
@@ -833,8 +850,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
833
850
|
`Invalid highlightStyle '${
|
|
834
851
|
clip.highlightStyle
|
|
835
852
|
}'. Expected: ${validStyles.join(", ")}`,
|
|
836
|
-
clip.highlightStyle
|
|
837
|
-
)
|
|
853
|
+
clip.highlightStyle,
|
|
854
|
+
),
|
|
838
855
|
);
|
|
839
856
|
}
|
|
840
857
|
}
|
|
@@ -863,8 +880,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
863
880
|
`Invalid animation type '${
|
|
864
881
|
clip.animation.type
|
|
865
882
|
}'. Expected: ${validAnimations.join(", ")}`,
|
|
866
|
-
clip.animation.type
|
|
867
|
-
)
|
|
883
|
+
clip.animation.type,
|
|
884
|
+
),
|
|
868
885
|
);
|
|
869
886
|
}
|
|
870
887
|
}
|
|
@@ -885,8 +902,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
885
902
|
ValidationCodes.INVALID_VALUE,
|
|
886
903
|
`${path}.${prop}`,
|
|
887
904
|
`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
|
-
)
|
|
905
|
+
clip[prop],
|
|
906
|
+
),
|
|
890
907
|
);
|
|
891
908
|
}
|
|
892
909
|
}
|
|
@@ -901,8 +918,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
901
918
|
ValidationCodes.MISSING_REQUIRED,
|
|
902
919
|
`${path}.url`,
|
|
903
920
|
"URL is required for subtitle clips",
|
|
904
|
-
clip.url
|
|
905
|
-
)
|
|
921
|
+
clip.url,
|
|
922
|
+
),
|
|
906
923
|
);
|
|
907
924
|
} else {
|
|
908
925
|
// Check file extension
|
|
@@ -916,8 +933,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
916
933
|
`Unsupported subtitle format '.${ext}'. Expected: ${validExts
|
|
917
934
|
.map((e) => "." + e)
|
|
918
935
|
.join(", ")}`,
|
|
919
|
-
clip.url
|
|
920
|
-
)
|
|
936
|
+
clip.url,
|
|
937
|
+
),
|
|
921
938
|
);
|
|
922
939
|
}
|
|
923
940
|
|
|
@@ -930,8 +947,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
930
947
|
ValidationCodes.FILE_NOT_FOUND,
|
|
931
948
|
`${path}.url`,
|
|
932
949
|
`Subtitle file not found: '${clip.url}'`,
|
|
933
|
-
clip.url
|
|
934
|
-
)
|
|
950
|
+
clip.url,
|
|
951
|
+
),
|
|
935
952
|
);
|
|
936
953
|
}
|
|
937
954
|
} catch (_) {}
|
|
@@ -945,8 +962,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
945
962
|
ValidationCodes.INVALID_RANGE,
|
|
946
963
|
`${path}.position`,
|
|
947
964
|
"Subtitle position offset must be >= 0",
|
|
948
|
-
clip.position
|
|
949
|
-
)
|
|
965
|
+
clip.position,
|
|
966
|
+
),
|
|
950
967
|
);
|
|
951
968
|
}
|
|
952
969
|
|
|
@@ -960,8 +977,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
960
977
|
ValidationCodes.INVALID_VALUE,
|
|
961
978
|
`${path}.${prop}`,
|
|
962
979
|
`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
|
-
)
|
|
980
|
+
clip[prop],
|
|
981
|
+
),
|
|
965
982
|
);
|
|
966
983
|
}
|
|
967
984
|
}
|
|
@@ -978,8 +995,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
978
995
|
ValidationCodes.INVALID_VALUE,
|
|
979
996
|
`${path}.imageFit`,
|
|
980
997
|
`Invalid imageFit '${clip.imageFit}'. Expected: ${validImageFit.join(", ")}`,
|
|
981
|
-
clip.imageFit
|
|
982
|
-
)
|
|
998
|
+
clip.imageFit,
|
|
999
|
+
),
|
|
983
1000
|
);
|
|
984
1001
|
}
|
|
985
1002
|
}
|
|
@@ -991,8 +1008,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
991
1008
|
ValidationCodes.INVALID_TYPE,
|
|
992
1009
|
`${path}.blurIntensity`,
|
|
993
1010
|
`blurIntensity must be a finite number`,
|
|
994
|
-
clip.blurIntensity
|
|
995
|
-
)
|
|
1011
|
+
clip.blurIntensity,
|
|
1012
|
+
),
|
|
996
1013
|
);
|
|
997
1014
|
} else if (clip.blurIntensity <= 0) {
|
|
998
1015
|
errors.push(
|
|
@@ -1000,8 +1017,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1000
1017
|
ValidationCodes.INVALID_RANGE,
|
|
1001
1018
|
`${path}.blurIntensity`,
|
|
1002
1019
|
`blurIntensity must be > 0`,
|
|
1003
|
-
clip.blurIntensity
|
|
1004
|
-
)
|
|
1020
|
+
clip.blurIntensity,
|
|
1021
|
+
),
|
|
1005
1022
|
);
|
|
1006
1023
|
}
|
|
1007
1024
|
}
|
|
@@ -1027,10 +1044,10 @@ function validateClip(clip, index, options = {}) {
|
|
|
1027
1044
|
ValidationCodes.INVALID_VALUE,
|
|
1028
1045
|
`${path}.kenBurns`,
|
|
1029
1046
|
`Invalid kenBurns effect '${kbType}'. Expected: ${validKenBurns.join(
|
|
1030
|
-
", "
|
|
1047
|
+
", ",
|
|
1031
1048
|
)}`,
|
|
1032
|
-
kbType
|
|
1033
|
-
)
|
|
1049
|
+
kbType,
|
|
1050
|
+
),
|
|
1034
1051
|
);
|
|
1035
1052
|
}
|
|
1036
1053
|
|
|
@@ -1054,10 +1071,10 @@ function validateClip(clip, index, options = {}) {
|
|
|
1054
1071
|
ValidationCodes.INVALID_VALUE,
|
|
1055
1072
|
`${path}.kenBurns.anchor`,
|
|
1056
1073
|
`Invalid kenBurns anchor '${anchor}'. Expected: ${validAnchors.join(
|
|
1057
|
-
", "
|
|
1074
|
+
", ",
|
|
1058
1075
|
)}`,
|
|
1059
|
-
anchor
|
|
1060
|
-
)
|
|
1076
|
+
anchor,
|
|
1077
|
+
),
|
|
1061
1078
|
);
|
|
1062
1079
|
}
|
|
1063
1080
|
}
|
|
@@ -1070,10 +1087,10 @@ function validateClip(clip, index, options = {}) {
|
|
|
1070
1087
|
ValidationCodes.INVALID_VALUE,
|
|
1071
1088
|
`${path}.kenBurns.easing`,
|
|
1072
1089
|
`Invalid kenBurns easing '${easing}'. Expected: ${validEasing.join(
|
|
1073
|
-
", "
|
|
1090
|
+
", ",
|
|
1074
1091
|
)}`,
|
|
1075
|
-
easing
|
|
1076
|
-
)
|
|
1092
|
+
easing,
|
|
1093
|
+
),
|
|
1077
1094
|
);
|
|
1078
1095
|
}
|
|
1079
1096
|
}
|
|
@@ -1097,8 +1114,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1097
1114
|
ValidationCodes.INVALID_TYPE,
|
|
1098
1115
|
`${path}.kenBurns.${field}`,
|
|
1099
1116
|
`kenBurns.${field} must be a finite number`,
|
|
1100
|
-
value
|
|
1101
|
-
)
|
|
1117
|
+
value,
|
|
1118
|
+
),
|
|
1102
1119
|
);
|
|
1103
1120
|
return;
|
|
1104
1121
|
}
|
|
@@ -1109,8 +1126,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1109
1126
|
ValidationCodes.INVALID_RANGE,
|
|
1110
1127
|
`${path}.kenBurns.${field}`,
|
|
1111
1128
|
`kenBurns.${field} must be > 0`,
|
|
1112
|
-
value
|
|
1113
|
-
)
|
|
1129
|
+
value,
|
|
1130
|
+
),
|
|
1114
1131
|
);
|
|
1115
1132
|
}
|
|
1116
1133
|
|
|
@@ -1119,15 +1136,15 @@ function validateClip(clip, index, options = {}) {
|
|
|
1119
1136
|
field === "startY" ||
|
|
1120
1137
|
field === "endX" ||
|
|
1121
1138
|
field === "endY") &&
|
|
1122
|
-
|
|
1139
|
+
(value < 0 || value > 1)
|
|
1123
1140
|
) {
|
|
1124
1141
|
errors.push(
|
|
1125
1142
|
createIssue(
|
|
1126
1143
|
ValidationCodes.INVALID_RANGE,
|
|
1127
1144
|
`${path}.kenBurns.${field}`,
|
|
1128
1145
|
`kenBurns.${field} must be between 0 and 1`,
|
|
1129
|
-
value
|
|
1130
|
-
)
|
|
1146
|
+
value,
|
|
1147
|
+
),
|
|
1131
1148
|
);
|
|
1132
1149
|
}
|
|
1133
1150
|
});
|
|
@@ -1149,7 +1166,7 @@ function validateClip(clip, index, options = {}) {
|
|
|
1149
1166
|
strictKenBurns
|
|
1150
1167
|
? `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
1168
|
: `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 }
|
|
1169
|
+
{ width: clip.width, height: clip.height },
|
|
1153
1170
|
);
|
|
1154
1171
|
|
|
1155
1172
|
if (strictKenBurns) {
|
|
@@ -1166,8 +1183,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1166
1183
|
ValidationCodes.INVALID_VALUE,
|
|
1167
1184
|
`${path}`,
|
|
1168
1185
|
`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
|
-
)
|
|
1186
|
+
clip.url,
|
|
1187
|
+
),
|
|
1171
1188
|
);
|
|
1172
1189
|
}
|
|
1173
1190
|
}
|
|
@@ -1181,8 +1198,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1181
1198
|
ValidationCodes.MISSING_REQUIRED,
|
|
1182
1199
|
`${path}.color`,
|
|
1183
1200
|
"Color is required for color clips",
|
|
1184
|
-
clip.color
|
|
1185
|
-
)
|
|
1201
|
+
clip.color,
|
|
1202
|
+
),
|
|
1186
1203
|
);
|
|
1187
1204
|
} else if (typeof clip.color === "string") {
|
|
1188
1205
|
if (!isValidFFmpegColor(clip.color)) {
|
|
@@ -1191,8 +1208,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1191
1208
|
ValidationCodes.INVALID_VALUE,
|
|
1192
1209
|
`${path}.color`,
|
|
1193
1210
|
`Invalid color "${clip.color}". Use a named color (e.g. "black", "navy"), hex (#RRGGBB, 0xRRGGBB), or "random".`,
|
|
1194
|
-
clip.color
|
|
1195
|
-
)
|
|
1211
|
+
clip.color,
|
|
1212
|
+
),
|
|
1196
1213
|
);
|
|
1197
1214
|
}
|
|
1198
1215
|
} else if (typeof clip.color === "object" && clip.color !== null) {
|
|
@@ -1203,8 +1220,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1203
1220
|
ValidationCodes.INVALID_VALUE,
|
|
1204
1221
|
`${path}.color.type`,
|
|
1205
1222
|
`Invalid gradient type '${clip.color.type}'. Expected: ${validGradientTypes.join(", ")}`,
|
|
1206
|
-
clip.color.type
|
|
1207
|
-
)
|
|
1223
|
+
clip.color.type,
|
|
1224
|
+
),
|
|
1208
1225
|
);
|
|
1209
1226
|
}
|
|
1210
1227
|
if (!Array.isArray(clip.color.colors) || clip.color.colors.length < 2) {
|
|
@@ -1213,8 +1230,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1213
1230
|
ValidationCodes.INVALID_VALUE,
|
|
1214
1231
|
`${path}.color.colors`,
|
|
1215
1232
|
"Gradient colors must be an array of at least 2 color strings",
|
|
1216
|
-
clip.color.colors
|
|
1217
|
-
)
|
|
1233
|
+
clip.color.colors,
|
|
1234
|
+
),
|
|
1218
1235
|
);
|
|
1219
1236
|
} else {
|
|
1220
1237
|
clip.color.colors.forEach((c, ci) => {
|
|
@@ -1224,8 +1241,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1224
1241
|
ValidationCodes.INVALID_VALUE,
|
|
1225
1242
|
`${path}.color.colors[${ci}]`,
|
|
1226
1243
|
`Invalid gradient color "${c}". Use a named color (e.g. "black", "navy"), hex (#RRGGBB), or "random".`,
|
|
1227
|
-
c
|
|
1228
|
-
)
|
|
1244
|
+
c,
|
|
1245
|
+
),
|
|
1229
1246
|
);
|
|
1230
1247
|
}
|
|
1231
1248
|
});
|
|
@@ -1238,8 +1255,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1238
1255
|
ValidationCodes.INVALID_VALUE,
|
|
1239
1256
|
`${path}.color.direction`,
|
|
1240
1257
|
`Invalid gradient direction '${clip.color.direction}'. Expected: "vertical", "horizontal", or a number (angle in degrees)`,
|
|
1241
|
-
clip.color.direction
|
|
1242
|
-
)
|
|
1258
|
+
clip.color.direction,
|
|
1259
|
+
),
|
|
1243
1260
|
);
|
|
1244
1261
|
}
|
|
1245
1262
|
}
|
|
@@ -1249,8 +1266,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1249
1266
|
ValidationCodes.INVALID_VALUE,
|
|
1250
1267
|
`${path}.color`,
|
|
1251
1268
|
"Color must be a string (flat color) or an object (gradient spec)",
|
|
1252
|
-
clip.color
|
|
1253
|
-
)
|
|
1269
|
+
clip.color,
|
|
1270
|
+
),
|
|
1254
1271
|
);
|
|
1255
1272
|
}
|
|
1256
1273
|
}
|
|
@@ -1268,8 +1285,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1268
1285
|
ValidationCodes.INVALID_VALUE,
|
|
1269
1286
|
`${path}.transition.duration`,
|
|
1270
1287
|
"Transition duration must be a number",
|
|
1271
|
-
clip.transition.duration
|
|
1272
|
-
)
|
|
1288
|
+
clip.transition.duration,
|
|
1289
|
+
),
|
|
1273
1290
|
);
|
|
1274
1291
|
} else if (!Number.isFinite(clip.transition.duration)) {
|
|
1275
1292
|
errors.push(
|
|
@@ -1277,8 +1294,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1277
1294
|
ValidationCodes.INVALID_VALUE,
|
|
1278
1295
|
`${path}.transition.duration`,
|
|
1279
1296
|
"Transition duration must be a finite number (not NaN or Infinity)",
|
|
1280
|
-
clip.transition.duration
|
|
1281
|
-
)
|
|
1297
|
+
clip.transition.duration,
|
|
1298
|
+
),
|
|
1282
1299
|
);
|
|
1283
1300
|
} else if (clip.transition.duration <= 0) {
|
|
1284
1301
|
errors.push(
|
|
@@ -1286,8 +1303,8 @@ function validateClip(clip, index, options = {}) {
|
|
|
1286
1303
|
ValidationCodes.INVALID_VALUE,
|
|
1287
1304
|
`${path}.transition.duration`,
|
|
1288
1305
|
"Transition duration must be a positive number",
|
|
1289
|
-
clip.transition.duration
|
|
1290
|
-
)
|
|
1306
|
+
clip.transition.duration,
|
|
1307
|
+
),
|
|
1291
1308
|
);
|
|
1292
1309
|
}
|
|
1293
1310
|
}
|
|
@@ -1317,7 +1334,7 @@ function validateTimelineGaps(clips) {
|
|
|
1317
1334
|
.filter(({ clip }) => clip.type === "video" || clip.type === "image" || clip.type === "color")
|
|
1318
1335
|
.filter(
|
|
1319
1336
|
({ clip }) =>
|
|
1320
|
-
typeof clip.position === "number" && typeof clip.end === "number"
|
|
1337
|
+
typeof clip.position === "number" && typeof clip.end === "number",
|
|
1321
1338
|
)
|
|
1322
1339
|
.sort((a, b) => a.clip.position - b.clip.position);
|
|
1323
1340
|
|
|
@@ -1330,15 +1347,15 @@ function validateTimelineGaps(clips) {
|
|
|
1330
1347
|
ValidationCodes.TIMELINE_GAP,
|
|
1331
1348
|
"timeline",
|
|
1332
1349
|
`Gap at start of visual timeline [0, ${gap.end.toFixed(
|
|
1333
|
-
3
|
|
1350
|
+
3,
|
|
1334
1351
|
)}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
|
-
)
|
|
1352
|
+
{ start: gap.start, end: gap.end },
|
|
1353
|
+
),
|
|
1337
1354
|
);
|
|
1338
1355
|
} else {
|
|
1339
1356
|
// 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);
|
|
1357
|
+
const before = visual.filter((v) => v.clip.end <= gap.start + 1e-3);
|
|
1358
|
+
const after = visual.filter((v) => v.clip.position >= gap.end - 1e-3);
|
|
1342
1359
|
const prevIdx = before.length > 0 ? before[before.length - 1].index : "?";
|
|
1343
1360
|
const nextIdx = after.length > 0 ? after[0].index : "?";
|
|
1344
1361
|
|
|
@@ -1347,10 +1364,10 @@ function validateTimelineGaps(clips) {
|
|
|
1347
1364
|
ValidationCodes.TIMELINE_GAP,
|
|
1348
1365
|
"timeline",
|
|
1349
1366
|
`Gap in visual timeline [${gap.start.toFixed(3)}s, ${gap.end.toFixed(
|
|
1350
|
-
3
|
|
1367
|
+
3,
|
|
1351
1368
|
)}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
|
-
)
|
|
1369
|
+
{ start: gap.start, end: gap.end },
|
|
1370
|
+
),
|
|
1354
1371
|
);
|
|
1355
1372
|
}
|
|
1356
1373
|
}
|
|
@@ -1377,8 +1394,8 @@ function validateConfig(clips, options = {}) {
|
|
|
1377
1394
|
ValidationCodes.INVALID_TYPE,
|
|
1378
1395
|
"clips",
|
|
1379
1396
|
"Clips must be an array",
|
|
1380
|
-
typeof clips
|
|
1381
|
-
)
|
|
1397
|
+
typeof clips,
|
|
1398
|
+
),
|
|
1382
1399
|
);
|
|
1383
1400
|
return { valid: false, errors: allErrors, warnings: allWarnings };
|
|
1384
1401
|
}
|
|
@@ -1390,8 +1407,8 @@ function validateConfig(clips, options = {}) {
|
|
|
1390
1407
|
ValidationCodes.MISSING_REQUIRED,
|
|
1391
1408
|
"clips",
|
|
1392
1409
|
"At least one clip is required",
|
|
1393
|
-
[]
|
|
1394
|
-
)
|
|
1410
|
+
[],
|
|
1411
|
+
),
|
|
1395
1412
|
);
|
|
1396
1413
|
return { valid: false, errors: allErrors, warnings: allWarnings };
|
|
1397
1414
|
}
|
|
@@ -1408,6 +1425,47 @@ function validateConfig(clips, options = {}) {
|
|
|
1408
1425
|
allErrors.push(...gapResult.errors);
|
|
1409
1426
|
allWarnings.push(...gapResult.warnings);
|
|
1410
1427
|
|
|
1428
|
+
// Warn about non-visual clips positioned beyond the visual timeline
|
|
1429
|
+
const visualClips = clips.filter(
|
|
1430
|
+
(c) => c.type === "video" || c.type === "image" || c.type === "color",
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
if (visualClips.length > 0) {
|
|
1434
|
+
const visualBaseSum = visualClips.reduce(
|
|
1435
|
+
(acc, c) => acc + Math.max(0, (c.end || 0) - (c.position || 0)),
|
|
1436
|
+
0,
|
|
1437
|
+
);
|
|
1438
|
+
const visualTransitionOverlap = visualClips.reduce((acc, c) => {
|
|
1439
|
+
const d =
|
|
1440
|
+
c.transition && typeof c.transition.duration === "number"
|
|
1441
|
+
? c.transition.duration
|
|
1442
|
+
: 0;
|
|
1443
|
+
return acc + d;
|
|
1444
|
+
}, 0);
|
|
1445
|
+
const visualDuration = Math.max(0, visualBaseSum - visualTransitionOverlap);
|
|
1446
|
+
|
|
1447
|
+
if (visualDuration > 0) {
|
|
1448
|
+
const nonVisualTypes = ["text", "audio", "subtitle", "music", "backgroundAudio"];
|
|
1449
|
+
for (let i = 0; i < clips.length; i++) {
|
|
1450
|
+
const clip = clips[i];
|
|
1451
|
+
if (
|
|
1452
|
+
nonVisualTypes.includes(clip.type) &&
|
|
1453
|
+
typeof clip.position === "number" &&
|
|
1454
|
+
clip.position >= visualDuration
|
|
1455
|
+
) {
|
|
1456
|
+
allWarnings.push(
|
|
1457
|
+
createIssue(
|
|
1458
|
+
ValidationCodes.OUTSIDE_BOUNDS,
|
|
1459
|
+
`clips[${i}]`,
|
|
1460
|
+
`${clip.type} clip starts at ${clip.position}s but visual timeline ends at ${visualDuration}s`,
|
|
1461
|
+
{ position: clip.position, visualDuration },
|
|
1462
|
+
),
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1411
1469
|
return {
|
|
1412
1470
|
valid: allErrors.length === 0,
|
|
1413
1471
|
errors: allErrors,
|