simple-ffmpegjs 0.4.0 → 0.4.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 +196 -190
- package/package.json +1 -1
- package/src/core/validation.js +57 -13
- package/src/ffmpeg/bgm_builder.js +20 -4
- package/src/ffmpeg/effect_builder.js +103 -21
- package/src/loaders.js +1 -2
- package/src/schema/modules/effect.js +56 -9
- package/src/simpleffmpeg.js +2 -0
- package/types/index.d.mts +38 -4
- package/types/index.d.ts +44 -5
package/README.md
CHANGED
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
- [API Reference](#api-reference)
|
|
25
25
|
- [Constructor](#constructor)
|
|
26
26
|
- [Methods](#methods)
|
|
27
|
-
- [
|
|
27
|
+
- [Auto-Sequencing & Duration Shorthand](#auto-sequencing--duration-shorthand)
|
|
28
|
+
- [Clip Types](#clip-types) — Video, Image, Color, Effect, Text, Subtitle, Audio, Background Music
|
|
28
29
|
- [Platform Presets](#platform-presets)
|
|
29
30
|
- [Watermarks](#watermarks)
|
|
30
31
|
- [Progress Information](#progress-information)
|
|
31
32
|
- [Logging](#logging)
|
|
32
33
|
- [Error Handling](#error-handling)
|
|
33
34
|
- [Cancellation](#cancellation)
|
|
34
|
-
- [Color Clips](#color-clips)
|
|
35
35
|
- [Examples](#examples)
|
|
36
36
|
- [Clips & Transitions](#clips--transitions)
|
|
37
37
|
- [Text & Animations](#text--animations)
|
|
@@ -66,19 +66,26 @@ _Click to watch a "Wonders of the World" video created with simple-ffmpeg — co
|
|
|
66
66
|
|
|
67
67
|
## Features
|
|
68
68
|
|
|
69
|
+
**Video & Images**
|
|
69
70
|
- **Video Concatenation** — Join multiple clips with optional xfade transitions
|
|
71
|
+
- **Image Support** — Ken Burns effects (zoom, pan) for static images
|
|
72
|
+
- **Color Clips** — Flat colors and gradients (linear, radial) as first-class timeline clips with full transition support
|
|
73
|
+
|
|
74
|
+
**Audio**
|
|
70
75
|
- **Audio Mixing** — Layer audio tracks, voiceovers, and background music
|
|
76
|
+
|
|
77
|
+
**Overlays & Effects**
|
|
71
78
|
- **Text Overlays** — Static, word-by-word, and cumulative text with animations
|
|
72
79
|
- **Text Animations** — Typewriter, scale-in, pulse, fade effects
|
|
73
80
|
- **Karaoke Mode** — Word-by-word highlighting with customizable colors
|
|
74
81
|
- **Subtitle Import** — Load SRT, VTT, ASS/SSA subtitle files
|
|
75
82
|
- **Watermarks** — Text or image overlays with positioning and timing control
|
|
83
|
+
- **Effect Clips** — Timed overlay effects (vignette, film grain, blur, color adjust, sepia, black & white, sharpen, chromatic aberration, letterbox) with fade-in/out envelopes
|
|
84
|
+
|
|
85
|
+
**Developer Experience**
|
|
76
86
|
- **Platform Presets** — Quick configuration for TikTok, YouTube, Instagram, etc.
|
|
77
|
-
- **Image Support** — Ken Burns effects (zoom, pan) for static images
|
|
78
87
|
- **Progress Tracking** — Real-time export progress callbacks
|
|
79
88
|
- **Cancellation** — AbortController support for stopping exports
|
|
80
|
-
- **Color Clips** — Flat colors and gradients (linear, radial) as first-class timeline clips with full transition support
|
|
81
|
-
- **Effect Clips** — Timed overlay effects (vignette, film grain, gaussian blur, color adjustment) with fade-in/out envelopes
|
|
82
89
|
- **Auto-Batching** — Automatically splits complex filter graphs to avoid OS command limits
|
|
83
90
|
- **Schema Export** — Generate a structured description of the clip format for documentation, code generation, or AI context
|
|
84
91
|
- **Pre-Validation** — Validate clip configurations before processing with structured, machine-readable error codes
|
|
@@ -249,7 +256,7 @@ Available modules:
|
|
|
249
256
|
| `audio` | Standalone audio clips |
|
|
250
257
|
| `image` | Image clips, Ken Burns effects |
|
|
251
258
|
| `color` | Color clips — flat colors, linear/radial gradients |
|
|
252
|
-
| `effect` | Overlay adjustment effects — vignette, grain, blur,
|
|
259
|
+
| `effect` | Overlay adjustment effects — vignette, grain, blur, color adjust, sepia, B&W, sharpen, chromatic aberration, letterbox |
|
|
253
260
|
| `text` | Text overlays — all modes, animations, positioning, styling |
|
|
254
261
|
| `subtitle` | Subtitle file import (SRT, VTT, ASS, SSA) |
|
|
255
262
|
| `music` | Background music / background audio, looping |
|
|
@@ -288,9 +295,27 @@ new SIMPLEFFMPEG(options?: {
|
|
|
288
295
|
fps?: number; // Frame rate (default: 30)
|
|
289
296
|
validationMode?: 'warn' | 'strict'; // Validation behavior (default: 'warn')
|
|
290
297
|
preset?: string; // Platform preset (e.g., 'tiktok', 'youtube', 'instagram-post')
|
|
298
|
+
fontFile?: string; // Default font file for all text clips (individual clips can override)
|
|
291
299
|
})
|
|
292
300
|
```
|
|
293
301
|
|
|
302
|
+
When `fontFile` is set at the project level, every text clip (including karaoke) inherits it automatically. You can still override it on any individual clip:
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
const project = new SIMPLEFFMPEG({
|
|
306
|
+
preset: "tiktok",
|
|
307
|
+
fontFile: "./fonts/Montserrat-Bold.ttf", // applies to all text clips
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await project.load([
|
|
311
|
+
{ type: "video", url: "intro.mp4", position: 0, end: 10 },
|
|
312
|
+
// Uses the global font
|
|
313
|
+
{ type: "text", text: "Hello!", position: 1, end: 4, fontSize: 72 },
|
|
314
|
+
// Overrides with a different font
|
|
315
|
+
{ type: "text", text: "Special", position: 5, end: 8, fontFile: "./fonts/Italic.otf" },
|
|
316
|
+
]);
|
|
317
|
+
```
|
|
318
|
+
|
|
294
319
|
### Methods
|
|
295
320
|
|
|
296
321
|
#### `project.load(clips)`
|
|
@@ -320,43 +345,6 @@ SIMPLEFFMPEG.getDuration(clips); // 14.5
|
|
|
320
345
|
|
|
321
346
|
Useful for computing text overlay timings or background music end times before calling `load()`.
|
|
322
347
|
|
|
323
|
-
**Duration and Auto-Sequencing:**
|
|
324
|
-
|
|
325
|
-
For video, image, and audio clips, you can use shorthand to avoid specifying explicit `position` and `end` values:
|
|
326
|
-
|
|
327
|
-
- **`duration`** — Use instead of `end`. The library computes `end = position + duration`. You cannot specify both `duration` and `end` on the same clip.
|
|
328
|
-
- **Omit `position`** — The clip is placed immediately after the previous clip on its track. Video and image clips share the visual track; audio clips have their own track. The first clip defaults to `position: 0`.
|
|
329
|
-
|
|
330
|
-
These can be combined:
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
// Before: manual position/end for every clip
|
|
334
|
-
await project.load([
|
|
335
|
-
{ type: "video", url: "./a.mp4", position: 0, end: 5 },
|
|
336
|
-
{ type: "video", url: "./b.mp4", position: 5, end: 10 },
|
|
337
|
-
{ type: "video", url: "./c.mp4", position: 10, end: 18, cutFrom: 3 },
|
|
338
|
-
]);
|
|
339
|
-
|
|
340
|
-
// After: auto-sequencing + duration
|
|
341
|
-
await project.load([
|
|
342
|
-
{ type: "video", url: "./a.mp4", duration: 5 },
|
|
343
|
-
{ type: "video", url: "./b.mp4", duration: 5 },
|
|
344
|
-
{ type: "video", url: "./c.mp4", duration: 8, cutFrom: 3 },
|
|
345
|
-
]);
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
You can mix explicit and implicit positioning freely. Clips with explicit `position` are placed there; subsequent auto-sequenced clips follow from the last clip's end:
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
await project.load([
|
|
352
|
-
{ type: "video", url: "./a.mp4", duration: 5 }, // position: 0, end: 5
|
|
353
|
-
{ type: "video", url: "./b.mp4", position: 10, end: 15 }, // explicit gap
|
|
354
|
-
{ type: "video", url: "./c.mp4", duration: 5 }, // position: 15, end: 20
|
|
355
|
-
]);
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
Text clips always require an explicit `position` (they're overlays on specific moments). Background music and subtitle clips already have optional `position`/`end` with their own defaults.
|
|
359
|
-
|
|
360
348
|
#### `SIMPLEFFMPEG.probe(filePath)`
|
|
361
349
|
|
|
362
350
|
Probe a media file and return comprehensive metadata using ffprobe. Works with video, audio, and image files.
|
|
@@ -482,6 +470,43 @@ await project.preview(options?: ExportOptions): Promise<{
|
|
|
482
470
|
}>
|
|
483
471
|
```
|
|
484
472
|
|
|
473
|
+
### Auto-Sequencing & Duration Shorthand
|
|
474
|
+
|
|
475
|
+
For video, image, and audio clips, you can use shorthand to avoid specifying explicit `position` and `end` values:
|
|
476
|
+
|
|
477
|
+
- **`duration`** — Use instead of `end`. The library computes `end = position + duration`. You cannot specify both `duration` and `end` on the same clip.
|
|
478
|
+
- **Omit `position`** — The clip is placed immediately after the previous clip on its track. Video and image clips share the visual track; audio clips have their own track. The first clip defaults to `position: 0`.
|
|
479
|
+
|
|
480
|
+
These can be combined:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
// Before: manual position/end for every clip
|
|
484
|
+
await project.load([
|
|
485
|
+
{ type: "video", url: "./a.mp4", position: 0, end: 5 },
|
|
486
|
+
{ type: "video", url: "./b.mp4", position: 5, end: 10 },
|
|
487
|
+
{ type: "video", url: "./c.mp4", position: 10, end: 18, cutFrom: 3 },
|
|
488
|
+
]);
|
|
489
|
+
|
|
490
|
+
// After: auto-sequencing + duration
|
|
491
|
+
await project.load([
|
|
492
|
+
{ type: "video", url: "./a.mp4", duration: 5 },
|
|
493
|
+
{ type: "video", url: "./b.mp4", duration: 5 },
|
|
494
|
+
{ type: "video", url: "./c.mp4", duration: 8, cutFrom: 3 },
|
|
495
|
+
]);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
You can mix explicit and implicit positioning freely. Clips with explicit `position` are placed there; subsequent auto-sequenced clips follow from the last clip's end:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
await project.load([
|
|
502
|
+
{ type: "video", url: "./a.mp4", duration: 5 }, // position: 0, end: 5
|
|
503
|
+
{ type: "video", url: "./b.mp4", position: 10, end: 15 }, // explicit gap
|
|
504
|
+
{ type: "video", url: "./c.mp4", duration: 5 }, // position: 15, end: 20
|
|
505
|
+
]);
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Text clips always require an explicit `position` (they're overlays on specific moments). Background music and subtitle clips already have optional `position`/`end` with their own defaults.
|
|
509
|
+
|
|
485
510
|
### Clip Types
|
|
486
511
|
|
|
487
512
|
#### Video Clip
|
|
@@ -504,47 +529,6 @@ await project.preview(options?: ExportOptions): Promise<{
|
|
|
504
529
|
|
|
505
530
|
All [xfade transitions](https://trac.ffmpeg.org/wiki/Xfade) are supported.
|
|
506
531
|
|
|
507
|
-
#### Audio Clip
|
|
508
|
-
|
|
509
|
-
```ts
|
|
510
|
-
{
|
|
511
|
-
type: "audio";
|
|
512
|
-
url: string;
|
|
513
|
-
position?: number; // Omit to auto-sequence after previous audio clip
|
|
514
|
-
end?: number; // Use end OR duration, not both
|
|
515
|
-
duration?: number; // Duration in seconds (alternative to end)
|
|
516
|
-
cutFrom?: number;
|
|
517
|
-
volume?: number;
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
#### Background Music
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
{
|
|
525
|
-
type: "music"; // or "backgroundAudio"
|
|
526
|
-
url: string;
|
|
527
|
-
position?: number; // default: 0
|
|
528
|
-
end?: number; // default: project duration
|
|
529
|
-
cutFrom?: number;
|
|
530
|
-
volume?: number; // default: 0.2
|
|
531
|
-
loop?: boolean; // Loop audio to fill video duration
|
|
532
|
-
}
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
Background music is mixed after transitions, so video crossfades won't affect its volume.
|
|
536
|
-
|
|
537
|
-
**Looping Music:**
|
|
538
|
-
|
|
539
|
-
If your music track is shorter than your video, enable looping:
|
|
540
|
-
|
|
541
|
-
```ts
|
|
542
|
-
await project.load([
|
|
543
|
-
{ type: "video", url: "./video.mp4", position: 0, end: 120 },
|
|
544
|
-
{ type: "music", url: "./30s-track.mp3", volume: 0.3, loop: true },
|
|
545
|
-
]);
|
|
546
|
-
```
|
|
547
|
-
|
|
548
532
|
#### Image Clip
|
|
549
533
|
|
|
550
534
|
```ts
|
|
@@ -575,6 +559,8 @@ await project.load([
|
|
|
575
559
|
|
|
576
560
|
#### Color Clip
|
|
577
561
|
|
|
562
|
+
Color clips add flat colors or gradients as first-class visual elements. They support transitions, text overlays, and all the same timeline features as video and image clips. Use them for intros, outros, title cards, or anywhere you need a background.
|
|
563
|
+
|
|
578
564
|
```ts
|
|
579
565
|
{
|
|
580
566
|
type: "color";
|
|
@@ -593,6 +579,73 @@ await project.load([
|
|
|
593
579
|
}
|
|
594
580
|
```
|
|
595
581
|
|
|
582
|
+
`color` accepts any valid FFmpeg color name or hex code:
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
{ type: "color", color: "navy", position: 0, end: 3 }
|
|
586
|
+
{ type: "color", color: "#1a1a2e", position: 0, end: 3 }
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Gradients:**
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
// Linear gradient (vertical by default)
|
|
593
|
+
{
|
|
594
|
+
type: "color",
|
|
595
|
+
color: { type: "linear-gradient", colors: ["#0a0a2e", "#4a148c"] },
|
|
596
|
+
position: 0,
|
|
597
|
+
end: 4,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Horizontal linear gradient
|
|
601
|
+
{
|
|
602
|
+
type: "color",
|
|
603
|
+
color: { type: "linear-gradient", colors: ["#e74c3c", "#f1c40f", "#2ecc71"], direction: "horizontal" },
|
|
604
|
+
position: 0,
|
|
605
|
+
end: 4,
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Radial gradient
|
|
609
|
+
{
|
|
610
|
+
type: "color",
|
|
611
|
+
color: { type: "radial-gradient", colors: ["#ff8c00", "#1a0000"] },
|
|
612
|
+
position: 0,
|
|
613
|
+
end: 3,
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**With transitions:**
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
await project.load([
|
|
621
|
+
{ type: "color", color: "black", position: 0, end: 3 },
|
|
622
|
+
{
|
|
623
|
+
type: "video",
|
|
624
|
+
url: "./main.mp4",
|
|
625
|
+
position: 3,
|
|
626
|
+
end: 8,
|
|
627
|
+
transition: { type: "fade", duration: 0.5 },
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
type: "color",
|
|
631
|
+
color: { type: "radial-gradient", colors: ["#2c3e50", "#000000"] },
|
|
632
|
+
position: 8,
|
|
633
|
+
end: 11,
|
|
634
|
+
transition: { type: "fade", duration: 0.5 },
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: "The End",
|
|
639
|
+
position: 8.5,
|
|
640
|
+
end: 10.5,
|
|
641
|
+
fontSize: 64,
|
|
642
|
+
fontColor: "white",
|
|
643
|
+
},
|
|
644
|
+
]);
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
> **Note:** Timeline gaps (periods with no visual content) always produce a validation error. If a gap is intentional, fill it with a `type: "color"` clip or adjust your clip positions to close the gap.
|
|
648
|
+
|
|
596
649
|
#### Effect Clip
|
|
597
650
|
|
|
598
651
|
Effects are overlay adjustment layers. They apply to the already-composed video
|
|
@@ -601,34 +654,30 @@ for a time window, and can ramp in/out smoothly (instead of appearing instantly)
|
|
|
601
654
|
```ts
|
|
602
655
|
{
|
|
603
656
|
type: "effect";
|
|
604
|
-
effect:
|
|
657
|
+
effect: EffectName; // See table below
|
|
605
658
|
position: number; // Required timeline start (seconds)
|
|
606
659
|
end?: number; // Use end OR duration, not both
|
|
607
660
|
duration?: number; // Duration in seconds (alternative to end)
|
|
608
661
|
fadeIn?: number; // Optional smooth ramp-in (seconds)
|
|
609
662
|
fadeOut?: number; // Optional smooth ramp-out (seconds)
|
|
610
|
-
|
|
611
|
-
params: {
|
|
612
|
-
amount?: number; // Blend amount 0..1 (default: 1)
|
|
613
|
-
|
|
614
|
-
// vignette
|
|
615
|
-
angle?: number; // radians
|
|
616
|
-
|
|
617
|
-
// filmGrain
|
|
618
|
-
temporal?: boolean; // default: true
|
|
619
|
-
|
|
620
|
-
// gaussianBlur
|
|
621
|
-
sigma?: number;
|
|
622
|
-
|
|
623
|
-
// colorAdjust
|
|
624
|
-
brightness?: number; // -1..1
|
|
625
|
-
contrast?: number; // 0..3
|
|
626
|
-
saturation?: number; // 0..3
|
|
627
|
-
gamma?: number; // 0.1..10
|
|
628
|
-
};
|
|
663
|
+
params: EffectParams; // Effect-specific parameters (see table below)
|
|
629
664
|
}
|
|
630
665
|
```
|
|
631
666
|
|
|
667
|
+
All effects accept `params.amount` (0-1, default 1) to control the blend intensity. Additional per-effect parameters:
|
|
668
|
+
|
|
669
|
+
| Effect | Description | Extra Params |
|
|
670
|
+
|---|---|---|
|
|
671
|
+
| `vignette` | Darkened edges | `angle`: radians (default: PI/5) |
|
|
672
|
+
| `filmGrain` | Noise overlay | `strength`: noise intensity 0-1 (default: 0.35), `temporal`: boolean (default: true) |
|
|
673
|
+
| `gaussianBlur` | Gaussian blur | `sigma`: blur radius (default derived from amount) |
|
|
674
|
+
| `colorAdjust` | Color grading | `brightness`: -1..1, `contrast`: 0..3, `saturation`: 0..3, `gamma`: 0.1..10 |
|
|
675
|
+
| `sepia` | Warm vintage tone | — |
|
|
676
|
+
| `blackAndWhite` | Desaturate to grayscale | `contrast`: boost 0-3 (default: 1) |
|
|
677
|
+
| `sharpen` | Sharpen detail | `strength`: unsharp amount 0-3 (default: 1) |
|
|
678
|
+
| `chromaticAberration` | RGB channel split | `shift`: pixel offset 0-20 (default: 4) |
|
|
679
|
+
| `letterbox` | Cinematic bars | `size`: bar height as fraction of frame 0-0.5 (default: 0.12), `color`: string (default: "black") |
|
|
680
|
+
|
|
632
681
|
#### Text Clip
|
|
633
682
|
|
|
634
683
|
```ts
|
|
@@ -698,6 +747,47 @@ Import external subtitle files (SRT, VTT, ASS/SSA):
|
|
|
698
747
|
}
|
|
699
748
|
```
|
|
700
749
|
|
|
750
|
+
#### Audio Clip
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
{
|
|
754
|
+
type: "audio";
|
|
755
|
+
url: string;
|
|
756
|
+
position?: number; // Omit to auto-sequence after previous audio clip
|
|
757
|
+
end?: number; // Use end OR duration, not both
|
|
758
|
+
duration?: number; // Duration in seconds (alternative to end)
|
|
759
|
+
cutFrom?: number;
|
|
760
|
+
volume?: number;
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
#### Background Music
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
{
|
|
768
|
+
type: "music"; // or "backgroundAudio"
|
|
769
|
+
url: string;
|
|
770
|
+
position?: number; // default: 0
|
|
771
|
+
end?: number; // default: project duration
|
|
772
|
+
cutFrom?: number;
|
|
773
|
+
volume?: number; // default: 0.2
|
|
774
|
+
loop?: boolean; // Loop audio to fill video duration
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
Background music is mixed after transitions, so video crossfades won't affect its volume.
|
|
779
|
+
|
|
780
|
+
**Looping Music:**
|
|
781
|
+
|
|
782
|
+
If your music track is shorter than your video, enable looping:
|
|
783
|
+
|
|
784
|
+
```ts
|
|
785
|
+
await project.load([
|
|
786
|
+
{ type: "video", url: "./video.mp4", position: 0, end: 120 },
|
|
787
|
+
{ type: "music", url: "./30s-track.mp3", volume: 0.3, loop: true },
|
|
788
|
+
]);
|
|
789
|
+
```
|
|
790
|
+
|
|
701
791
|
### Platform Presets
|
|
702
792
|
|
|
703
793
|
Use platform presets to quickly configure optimal dimensions for social media:
|
|
@@ -910,90 +1000,6 @@ try {
|
|
|
910
1000
|
}
|
|
911
1001
|
```
|
|
912
1002
|
|
|
913
|
-
### Color Clips
|
|
914
|
-
|
|
915
|
-
Color clips let you add flat colors or gradients as first-class visual elements in your timeline. They support transitions, text overlays, and all the same timeline features as video and image clips. Use them for intros, outros, title cards, or anywhere you need a background:
|
|
916
|
-
|
|
917
|
-
**Flat color:**
|
|
918
|
-
|
|
919
|
-
```ts
|
|
920
|
-
await project.load([
|
|
921
|
-
// Black intro screen for 2 seconds
|
|
922
|
-
{ type: "color", color: "black", position: 0, end: 2 },
|
|
923
|
-
// Video starts at 2s
|
|
924
|
-
{ type: "video", url: "./clip.mp4", position: 2, end: 7 },
|
|
925
|
-
]);
|
|
926
|
-
```
|
|
927
|
-
|
|
928
|
-
`color` accepts any valid FFmpeg color name or hex code:
|
|
929
|
-
|
|
930
|
-
```ts
|
|
931
|
-
{ type: "color", color: "navy", position: 0, end: 3 }
|
|
932
|
-
{ type: "color", color: "#1a1a2e", position: 0, end: 3 }
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
**Gradients:**
|
|
936
|
-
|
|
937
|
-
```ts
|
|
938
|
-
// Linear gradient (vertical by default)
|
|
939
|
-
{
|
|
940
|
-
type: "color",
|
|
941
|
-
color: { type: "linear-gradient", colors: ["#0a0a2e", "#4a148c"] },
|
|
942
|
-
position: 0,
|
|
943
|
-
end: 4,
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// Horizontal linear gradient
|
|
947
|
-
{
|
|
948
|
-
type: "color",
|
|
949
|
-
color: { type: "linear-gradient", colors: ["#e74c3c", "#f1c40f", "#2ecc71"], direction: "horizontal" },
|
|
950
|
-
position: 0,
|
|
951
|
-
end: 4,
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Radial gradient
|
|
955
|
-
{
|
|
956
|
-
type: "color",
|
|
957
|
-
color: { type: "radial-gradient", colors: ["#ff8c00", "#1a0000"] },
|
|
958
|
-
position: 0,
|
|
959
|
-
end: 3,
|
|
960
|
-
}
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
**With transitions:**
|
|
964
|
-
|
|
965
|
-
Color clips support the same `transition` property as video and image clips:
|
|
966
|
-
|
|
967
|
-
```ts
|
|
968
|
-
await project.load([
|
|
969
|
-
{ type: "color", color: "black", position: 0, end: 3 },
|
|
970
|
-
{
|
|
971
|
-
type: "video",
|
|
972
|
-
url: "./main.mp4",
|
|
973
|
-
position: 3,
|
|
974
|
-
end: 8,
|
|
975
|
-
transition: { type: "fade", duration: 0.5 },
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
type: "color",
|
|
979
|
-
color: { type: "radial-gradient", colors: ["#2c3e50", "#000000"] },
|
|
980
|
-
position: 8,
|
|
981
|
-
end: 11,
|
|
982
|
-
transition: { type: "fade", duration: 0.5 },
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
type: "text",
|
|
986
|
-
text: "The End",
|
|
987
|
-
position: 8.5,
|
|
988
|
-
end: 10.5,
|
|
989
|
-
fontSize: 64,
|
|
990
|
-
fontColor: "white",
|
|
991
|
-
},
|
|
992
|
-
]);
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
> **Note:** Timeline gaps (periods with no visual content) always produce a validation error. If a gap is intentional, fill it with a `type: "color"` clip or adjust your clip positions to close the gap.
|
|
996
|
-
|
|
997
1003
|
## Examples
|
|
998
1004
|
|
|
999
1005
|
### Clips & Transitions
|
|
@@ -1055,7 +1061,7 @@ await project.load([
|
|
|
1055
1061
|
]);
|
|
1056
1062
|
```
|
|
1057
1063
|
|
|
1058
|
-
When `position` is omitted, clips are placed sequentially —
|
|
1064
|
+
When `position` is omitted, clips are placed sequentially — see [Auto-Sequencing & Duration Shorthand](#auto-sequencing--duration-shorthand) for details.
|
|
1059
1065
|
|
|
1060
1066
|
> **Note:** Ken Burns effects work best with images at least as large as your output resolution. Smaller images are automatically upscaled (with a validation warning). Use `strictKenBurns: true` in validation options to enforce size requirements instead.
|
|
1061
1067
|
> If you pass `width`/`height`, they override probed dimensions (useful for remote or generated images).
|
|
@@ -1533,7 +1539,7 @@ Available demo scripts (can also be run individually):
|
|
|
1533
1539
|
| Script | What it tests |
|
|
1534
1540
|
| ------------------------------- | -------------------------------------------------------------------------------------- |
|
|
1535
1541
|
| `demo-color-clips.js` | Flat colors, linear/radial gradients, transitions, full composition with color clips |
|
|
1536
|
-
| `demo-effects
|
|
1542
|
+
| `demo-effects.js` | Timed overlay effects (all 9 effects) with smooth fade ramps |
|
|
1537
1543
|
| `demo-transitions.js` | Fade, wipe, slide, dissolve, fadeblack/white, short/long durations, image transitions |
|
|
1538
1544
|
| `demo-text-and-animations.js` | Positioning, fade, pop, pop-bounce, typewriter, scale-in, pulse, styling, word-replace |
|
|
1539
1545
|
| `demo-ken-burns.js` | All 6 presets, smart anchors, custom diagonal, slideshow with transitions |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-ffmpegjs",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Declarative video composition for Node.js — define clips, transitions, text, and audio as simple objects, and let FFmpeg handle the rest.",
|
|
5
5
|
"author": "Brayden Blackwell <braydenblackwell21@gmail.com> (https://github.com/Fats403)",
|
|
6
6
|
"license": "MIT",
|
package/src/core/validation.js
CHANGED
|
@@ -106,8 +106,17 @@ function createIssue(code, path, message, received = undefined) {
|
|
|
106
106
|
return issue;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const EFFECT_TYPES = [
|
|
110
|
-
|
|
109
|
+
const EFFECT_TYPES = [
|
|
110
|
+
"vignette",
|
|
111
|
+
"filmGrain",
|
|
112
|
+
"gaussianBlur",
|
|
113
|
+
"colorAdjust",
|
|
114
|
+
"sepia",
|
|
115
|
+
"blackAndWhite",
|
|
116
|
+
"sharpen",
|
|
117
|
+
"chromaticAberration",
|
|
118
|
+
"letterbox",
|
|
119
|
+
];
|
|
111
120
|
|
|
112
121
|
function validateFiniteNumber(value, path, errors, opts = {}) {
|
|
113
122
|
const { min = null, max = null, minInclusive = true, maxInclusive = true } = opts;
|
|
@@ -169,17 +178,6 @@ function validateEffectClip(clip, path, errors) {
|
|
|
169
178
|
if (clip.fadeOut != null) {
|
|
170
179
|
validateFiniteNumber(clip.fadeOut, `${path}.fadeOut`, errors, { min: 0 });
|
|
171
180
|
}
|
|
172
|
-
if (clip.easing != null && !EFFECT_EASING.includes(clip.easing)) {
|
|
173
|
-
errors.push(
|
|
174
|
-
createIssue(
|
|
175
|
-
ValidationCodes.INVALID_VALUE,
|
|
176
|
-
`${path}.easing`,
|
|
177
|
-
`Invalid easing '${clip.easing}'. Expected: ${EFFECT_EASING.join(", ")}`,
|
|
178
|
-
clip.easing
|
|
179
|
-
)
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
181
|
if (typeof clip.position === "number" && typeof clip.end === "number") {
|
|
184
182
|
const duration = clip.end - clip.position;
|
|
185
183
|
const fadeTotal = (clip.fadeIn || 0) + (clip.fadeOut || 0);
|
|
@@ -227,6 +225,12 @@ function validateEffectClip(clip, path, errors) {
|
|
|
227
225
|
});
|
|
228
226
|
}
|
|
229
227
|
} else if (clip.effect === "filmGrain") {
|
|
228
|
+
if (params.strength != null) {
|
|
229
|
+
validateFiniteNumber(params.strength, `${path}.params.strength`, errors, {
|
|
230
|
+
min: 0,
|
|
231
|
+
max: 1,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
230
234
|
if (params.temporal != null && typeof params.temporal !== "boolean") {
|
|
231
235
|
errors.push(
|
|
232
236
|
createIssue(
|
|
@@ -269,6 +273,46 @@ function validateEffectClip(clip, path, errors) {
|
|
|
269
273
|
max: 10,
|
|
270
274
|
});
|
|
271
275
|
}
|
|
276
|
+
} else if (clip.effect === "sepia") {
|
|
277
|
+
// sepia only uses amount (base param) — no extra params to validate
|
|
278
|
+
} else if (clip.effect === "blackAndWhite") {
|
|
279
|
+
if (params.contrast != null) {
|
|
280
|
+
validateFiniteNumber(params.contrast, `${path}.params.contrast`, errors, {
|
|
281
|
+
min: 0,
|
|
282
|
+
max: 3,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
} else if (clip.effect === "sharpen") {
|
|
286
|
+
if (params.strength != null) {
|
|
287
|
+
validateFiniteNumber(params.strength, `${path}.params.strength`, errors, {
|
|
288
|
+
min: 0,
|
|
289
|
+
max: 3,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
} else if (clip.effect === "chromaticAberration") {
|
|
293
|
+
if (params.shift != null) {
|
|
294
|
+
validateFiniteNumber(params.shift, `${path}.params.shift`, errors, {
|
|
295
|
+
min: 0,
|
|
296
|
+
max: 20,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
} else if (clip.effect === "letterbox") {
|
|
300
|
+
if (params.size != null) {
|
|
301
|
+
validateFiniteNumber(params.size, `${path}.params.size`, errors, {
|
|
302
|
+
min: 0,
|
|
303
|
+
max: 0.5,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (params.color != null && typeof params.color !== "string") {
|
|
307
|
+
errors.push(
|
|
308
|
+
createIssue(
|
|
309
|
+
ValidationCodes.INVALID_VALUE,
|
|
310
|
+
`${path}.params.color`,
|
|
311
|
+
"color must be a string",
|
|
312
|
+
params.color
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
272
316
|
}
|
|
273
317
|
}
|
|
274
318
|
|
|
@@ -43,15 +43,31 @@ function buildBackgroundMusicMix(
|
|
|
43
43
|
const adelay = effectivePosition * 1000;
|
|
44
44
|
const trimEnd = effectiveCutFrom + (effectiveEnd - effectivePosition);
|
|
45
45
|
const outLabel = `[bg${i}]`;
|
|
46
|
-
filter += `[${inputIndex}:a]volume=${effectiveVolume},atrim=start=${effectiveCutFrom}:end=${trimEnd},adelay=${adelay}|${adelay}
|
|
46
|
+
filter += `[${inputIndex}:a]volume=${effectiveVolume},atrim=start=${effectiveCutFrom}:end=${trimEnd},asetpts=PTS-STARTPTS,adelay=${adelay}|${adelay}${outLabel};`;
|
|
47
47
|
bgLabels.push(outLabel);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
if (bgLabels.length > 0) {
|
|
51
51
|
if (existingAudioLabel) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// Generate a silence anchor from time 0 so that amix starts producing
|
|
53
|
+
// output immediately. Without this, amix waits for ALL inputs to have
|
|
54
|
+
// frames before outputting — if the existing audio (e.g. delayed video
|
|
55
|
+
// audio) starts later on the timeline, background music is silenced
|
|
56
|
+
// until that point.
|
|
57
|
+
const anchorDur = Math.max(projectDuration, visualEnd || 0, 0.1);
|
|
58
|
+
const padLabel = "[_bgmpad]";
|
|
59
|
+
filter += `anullsrc=cl=stereo,atrim=end=${anchorDur}${padLabel};`;
|
|
60
|
+
|
|
61
|
+
// Use normalize=0 with explicit weights so the silence anchor
|
|
62
|
+
// contributes no audio energy while preserving the same volume
|
|
63
|
+
// balance as a direct amix of the real inputs.
|
|
64
|
+
const realCount = bgLabels.length + 1; // bgm tracks + existing audio
|
|
65
|
+
const w = (1 / realCount).toFixed(6);
|
|
66
|
+
const weights = ["0", ...Array(realCount).fill(w)].join(" ");
|
|
67
|
+
|
|
68
|
+
filter += `${padLabel}${existingAudioLabel}${bgLabels.join("")}amix=inputs=${
|
|
69
|
+
bgLabels.length + 2
|
|
70
|
+
}:duration=longest:weights='${weights}':normalize=0[finalaudio];`;
|
|
55
71
|
return { filter, finalAudioLabel: "[finalaudio]", hasAudio: true };
|
|
56
72
|
}
|
|
57
73
|
filter += `${bgLabels.join("")}amix=inputs=${
|
|
@@ -24,11 +24,14 @@ function buildProcessedEffectFilter(effectClip, inputLabel, outputLabel) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (effectClip.effect === "filmGrain") {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// `strength` controls noise intensity (0-1, default 0.35).
|
|
28
|
+
// `amount` is used purely for overlay blend alpha.
|
|
29
|
+
const strength = clamp(
|
|
30
|
+
typeof params.strength === "number" ? params.strength : 0.35,
|
|
29
31
|
0,
|
|
30
|
-
|
|
32
|
+
1
|
|
31
33
|
);
|
|
34
|
+
const grainStrength = strength * 100;
|
|
32
35
|
const flags = params.temporal === false ? "u" : "t+u";
|
|
33
36
|
return {
|
|
34
37
|
filter: `${inputLabel}noise=alls=${formatNumber(
|
|
@@ -53,24 +56,103 @@ function buildProcessedEffectFilter(effectClip, inputLabel, outputLabel) {
|
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
if (effectClip.effect === "colorAdjust") {
|
|
60
|
+
const brightness =
|
|
61
|
+
typeof params.brightness === "number" ? params.brightness : 0;
|
|
62
|
+
const contrast =
|
|
63
|
+
typeof params.contrast === "number" ? params.contrast : 1;
|
|
64
|
+
const saturation =
|
|
65
|
+
typeof params.saturation === "number" ? params.saturation : 1;
|
|
66
|
+
const gamma = typeof params.gamma === "number" ? params.gamma : 1;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
filter:
|
|
70
|
+
`${inputLabel}eq=` +
|
|
71
|
+
`brightness=${formatNumber(brightness, 4)}:` +
|
|
72
|
+
`contrast=${formatNumber(contrast, 4)}:` +
|
|
73
|
+
`saturation=${formatNumber(saturation, 4)}:` +
|
|
74
|
+
`gamma=${formatNumber(gamma, 4)}` +
|
|
75
|
+
`${outputLabel};`,
|
|
76
|
+
amount,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (effectClip.effect === "sepia") {
|
|
81
|
+
// Classic sepia tone via color channel mixer matrix
|
|
82
|
+
return {
|
|
83
|
+
filter:
|
|
84
|
+
`${inputLabel}colorchannelmixer=` +
|
|
85
|
+
`.393:.769:.189:0:` +
|
|
86
|
+
`.349:.686:.168:0:` +
|
|
87
|
+
`.272:.534:.131:0` +
|
|
88
|
+
`${outputLabel};`,
|
|
89
|
+
amount,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (effectClip.effect === "blackAndWhite") {
|
|
94
|
+
const contrast =
|
|
95
|
+
typeof params.contrast === "number" ? params.contrast : 1;
|
|
96
|
+
let chain = `${inputLabel}hue=s=0`;
|
|
97
|
+
if (contrast !== 1) {
|
|
98
|
+
chain += `,eq=contrast=${formatNumber(contrast, 4)}`;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
filter: `${chain}${outputLabel};`,
|
|
102
|
+
amount,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (effectClip.effect === "sharpen") {
|
|
107
|
+
// strength controls the unsharp amount (0-3, default 1.0), 5x5 luma matrix
|
|
108
|
+
const strength = clamp(
|
|
109
|
+
typeof params.strength === "number" ? params.strength : 1.0,
|
|
110
|
+
0,
|
|
111
|
+
3
|
|
112
|
+
);
|
|
113
|
+
return {
|
|
114
|
+
filter: `${inputLabel}unsharp=5:5:${formatNumber(strength, 4)}${outputLabel};`,
|
|
115
|
+
amount,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (effectClip.effect === "chromaticAberration") {
|
|
120
|
+
// shift is horizontal pixel offset for red/blue channels (default 4, range 0-20)
|
|
121
|
+
const shift = clamp(
|
|
122
|
+
typeof params.shift === "number" ? params.shift : 4,
|
|
123
|
+
0,
|
|
124
|
+
20
|
|
125
|
+
);
|
|
126
|
+
const shiftInt = Math.round(shift);
|
|
127
|
+
return {
|
|
128
|
+
filter: `${inputLabel}rgbashift=rh=${shiftInt}:bh=${-shiftInt}${outputLabel};`,
|
|
129
|
+
amount,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (effectClip.effect === "letterbox") {
|
|
134
|
+
// size is bar height as fraction of frame height (default 0.12, range 0-0.5)
|
|
135
|
+
const size = clamp(
|
|
136
|
+
typeof params.size === "number" ? params.size : 0.12,
|
|
137
|
+
0,
|
|
138
|
+
0.5
|
|
139
|
+
);
|
|
140
|
+
const color = typeof params.color === "string" ? params.color : "black";
|
|
141
|
+
const barExpr = `round(ih*${formatNumber(size, 4)})`;
|
|
142
|
+
return {
|
|
143
|
+
filter:
|
|
144
|
+
`${inputLabel}` +
|
|
145
|
+
`drawbox=y=0:w=iw:h='${barExpr}':color=${color}:t=fill,` +
|
|
146
|
+
`drawbox=y='ih-${barExpr}':w=iw:h='${barExpr}':color=${color}:t=fill` +
|
|
147
|
+
`${outputLabel};`,
|
|
148
|
+
amount,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Unknown effect — guard against silent fallthrough
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Unknown effect type '${effectClip.effect}' in buildProcessedEffectFilter`
|
|
155
|
+
);
|
|
74
156
|
}
|
|
75
157
|
|
|
76
158
|
function buildEffectFilters(effectClips, inputLabel) {
|
package/src/loaders.js
CHANGED
|
@@ -154,7 +154,7 @@ async function loadBackgroundAudio(project, clipObj) {
|
|
|
154
154
|
function loadText(project, clipObj) {
|
|
155
155
|
const clip = {
|
|
156
156
|
...clipObj,
|
|
157
|
-
fontFile: clipObj.fontFile || null,
|
|
157
|
+
fontFile: clipObj.fontFile || project.options.fontFile || null,
|
|
158
158
|
fontFamily: clipObj.fontFamily || C.DEFAULT_FONT_FAMILY,
|
|
159
159
|
fontSize: clipObj.fontSize || C.DEFAULT_FONT_SIZE,
|
|
160
160
|
fontColor: clipObj.fontColor || C.DEFAULT_FONT_COLOR,
|
|
@@ -180,7 +180,6 @@ function loadEffect(project, clipObj) {
|
|
|
180
180
|
...clipObj,
|
|
181
181
|
fadeIn: typeof clipObj.fadeIn === "number" ? clipObj.fadeIn : 0,
|
|
182
182
|
fadeOut: typeof clipObj.fadeOut === "number" ? clipObj.fadeOut : 0,
|
|
183
|
-
easing: clipObj.easing || "linear",
|
|
184
183
|
params: clipObj.params || {},
|
|
185
184
|
};
|
|
186
185
|
project.effectClips.push(clip);
|
|
@@ -5,23 +5,38 @@ module.exports = {
|
|
|
5
5
|
"Overlay adjustment clips that apply timed visual effects to the composed video (they do not create visual content by themselves).",
|
|
6
6
|
schema: `{
|
|
7
7
|
type: "effect"; // Required: clip type identifier
|
|
8
|
-
effect: "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust"
|
|
8
|
+
effect: "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust"
|
|
9
|
+
| "sepia" | "blackAndWhite" | "sharpen" | "chromaticAberration"
|
|
10
|
+
| "letterbox"; // Required: effect kind
|
|
9
11
|
position: number; // Required: start time on timeline (seconds)
|
|
10
12
|
end?: number; // End time on timeline (seconds). Use end OR duration, not both.
|
|
11
13
|
duration?: number; // Duration in seconds (alternative to end). end = position + duration.
|
|
12
14
|
fadeIn?: number; // Optional: seconds to ramp in from 0 to full intensity
|
|
13
15
|
fadeOut?: number; // Optional: seconds to ramp out from full intensity to 0
|
|
14
|
-
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out"; // Optional envelope easing (default: "linear")
|
|
15
16
|
params: EffectParams; // Required: effect-specific parameters
|
|
16
17
|
}`,
|
|
17
18
|
enums: {
|
|
18
|
-
EffectType: [
|
|
19
|
-
|
|
19
|
+
EffectType: [
|
|
20
|
+
"vignette",
|
|
21
|
+
"filmGrain",
|
|
22
|
+
"gaussianBlur",
|
|
23
|
+
"colorAdjust",
|
|
24
|
+
"sepia",
|
|
25
|
+
"blackAndWhite",
|
|
26
|
+
"sharpen",
|
|
27
|
+
"chromaticAberration",
|
|
28
|
+
"letterbox",
|
|
29
|
+
],
|
|
20
30
|
VignetteParams: `{ amount?: number; angle?: number; }`,
|
|
21
|
-
FilmGrainParams: `{ amount?: number; temporal?: boolean; }`,
|
|
31
|
+
FilmGrainParams: `{ amount?: number; strength?: number; temporal?: boolean; }`,
|
|
22
32
|
GaussianBlurParams: `{ amount?: number; sigma?: number; }`,
|
|
23
33
|
ColorAdjustParams:
|
|
24
34
|
`{ amount?: number; brightness?: number; contrast?: number; saturation?: number; gamma?: number; }`,
|
|
35
|
+
SepiaParams: `{ amount?: number; }`,
|
|
36
|
+
BlackAndWhiteParams: `{ amount?: number; contrast?: number; }`,
|
|
37
|
+
SharpenParams: `{ amount?: number; strength?: number; }`,
|
|
38
|
+
ChromaticAberrationParams: `{ amount?: number; shift?: number; }`,
|
|
39
|
+
LetterboxParams: `{ amount?: number; size?: number; color?: string; }`,
|
|
25
40
|
},
|
|
26
41
|
examples: [
|
|
27
42
|
{
|
|
@@ -36,7 +51,7 @@ module.exports = {
|
|
|
36
51
|
}`,
|
|
37
52
|
},
|
|
38
53
|
{
|
|
39
|
-
label: "Film grain
|
|
54
|
+
label: "Film grain with independent strength and blend",
|
|
40
55
|
code: `{
|
|
41
56
|
type: "effect",
|
|
42
57
|
effect: "filmGrain",
|
|
@@ -44,8 +59,7 @@ module.exports = {
|
|
|
44
59
|
end: 8,
|
|
45
60
|
fadeIn: 0.4,
|
|
46
61
|
fadeOut: 0.6,
|
|
47
|
-
|
|
48
|
-
params: { amount: 0.45, temporal: true }
|
|
62
|
+
params: { amount: 0.8, strength: 0.35, temporal: true }
|
|
49
63
|
}`,
|
|
50
64
|
},
|
|
51
65
|
{
|
|
@@ -63,6 +77,38 @@ module.exports = {
|
|
|
63
77
|
gamma: 1.04,
|
|
64
78
|
brightness: -0.02
|
|
65
79
|
}
|
|
80
|
+
}`,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: "Warm sepia tone",
|
|
84
|
+
code: `{
|
|
85
|
+
type: "effect",
|
|
86
|
+
effect: "sepia",
|
|
87
|
+
position: 0,
|
|
88
|
+
duration: 5,
|
|
89
|
+
fadeIn: 0.5,
|
|
90
|
+
params: { amount: 0.85 }
|
|
91
|
+
}`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Black and white with contrast boost",
|
|
95
|
+
code: `{
|
|
96
|
+
type: "effect",
|
|
97
|
+
effect: "blackAndWhite",
|
|
98
|
+
position: 2,
|
|
99
|
+
end: 6,
|
|
100
|
+
params: { amount: 1, contrast: 1.3 }
|
|
101
|
+
}`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: "Cinematic letterbox bars",
|
|
105
|
+
code: `{
|
|
106
|
+
type: "effect",
|
|
107
|
+
effect: "letterbox",
|
|
108
|
+
position: 0,
|
|
109
|
+
duration: 8,
|
|
110
|
+
fadeIn: 0.8,
|
|
111
|
+
params: { size: 0.12 }
|
|
66
112
|
}`,
|
|
67
113
|
},
|
|
68
114
|
],
|
|
@@ -71,7 +117,8 @@ module.exports = {
|
|
|
71
117
|
"Effects do not satisfy visual timeline continuity checks and do not fill gaps.",
|
|
72
118
|
"Use duration instead of end to specify length: end = position + duration. Cannot use both.",
|
|
73
119
|
"position is required for effect clips (no auto-sequencing).",
|
|
74
|
-
"fadeIn/fadeOut are optional envelope controls that avoid abrupt on/off changes.",
|
|
120
|
+
"fadeIn/fadeOut are optional linear envelope controls that avoid abrupt on/off changes.",
|
|
75
121
|
"params.amount is a normalized blend amount from 0 to 1 (default: 1).",
|
|
122
|
+
"filmGrain: use params.strength (0-1) for noise intensity, params.amount for blend alpha.",
|
|
76
123
|
],
|
|
77
124
|
};
|
package/src/simpleffmpeg.js
CHANGED
|
@@ -56,6 +56,7 @@ class SIMPLEFFMPEG {
|
|
|
56
56
|
* @param {number} options.fps - Frames per second (default: 30)
|
|
57
57
|
* @param {string} options.preset - Platform preset ('tiktok', 'youtube', 'instagram-post', etc.)
|
|
58
58
|
* @param {string} options.validationMode - Validation behavior: 'warn' or 'strict' (default: 'warn')
|
|
59
|
+
* @param {string} options.fontFile - Default font file path (.ttf, .otf) applied to all text clips unless overridden per-clip
|
|
59
60
|
*
|
|
60
61
|
* @example
|
|
61
62
|
* const project = new SIMPLEFFMPEG({ preset: 'tiktok' });
|
|
@@ -87,6 +88,7 @@ class SIMPLEFFMPEG {
|
|
|
87
88
|
height: options.height || presetConfig.height || C.DEFAULT_HEIGHT,
|
|
88
89
|
validationMode: options.validationMode || C.DEFAULT_VALIDATION_MODE,
|
|
89
90
|
preset: options.preset || null,
|
|
91
|
+
fontFile: options.fontFile || null,
|
|
90
92
|
};
|
|
91
93
|
this.videoOrAudioClips = [];
|
|
92
94
|
this.textClips = [];
|
package/types/index.d.mts
CHANGED
|
@@ -246,8 +246,16 @@ declare namespace SIMPLEFFMPEG {
|
|
|
246
246
|
transition?: { type: string; duration: number };
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
type EffectName =
|
|
250
|
-
|
|
249
|
+
type EffectName =
|
|
250
|
+
| "vignette"
|
|
251
|
+
| "filmGrain"
|
|
252
|
+
| "gaussianBlur"
|
|
253
|
+
| "colorAdjust"
|
|
254
|
+
| "sepia"
|
|
255
|
+
| "blackAndWhite"
|
|
256
|
+
| "sharpen"
|
|
257
|
+
| "chromaticAberration"
|
|
258
|
+
| "letterbox";
|
|
251
259
|
|
|
252
260
|
interface EffectParamsBase {
|
|
253
261
|
amount?: number;
|
|
@@ -258,6 +266,7 @@ declare namespace SIMPLEFFMPEG {
|
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
interface FilmGrainEffectParams extends EffectParamsBase {
|
|
269
|
+
strength?: number;
|
|
261
270
|
temporal?: boolean;
|
|
262
271
|
}
|
|
263
272
|
|
|
@@ -272,11 +281,35 @@ declare namespace SIMPLEFFMPEG {
|
|
|
272
281
|
gamma?: number;
|
|
273
282
|
}
|
|
274
283
|
|
|
284
|
+
interface SepiaEffectParams extends EffectParamsBase {}
|
|
285
|
+
|
|
286
|
+
interface BlackAndWhiteEffectParams extends EffectParamsBase {
|
|
287
|
+
contrast?: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface SharpenEffectParams extends EffectParamsBase {
|
|
291
|
+
strength?: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface ChromaticAberrationEffectParams extends EffectParamsBase {
|
|
295
|
+
shift?: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface LetterboxEffectParams extends EffectParamsBase {
|
|
299
|
+
size?: number;
|
|
300
|
+
color?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
275
303
|
type EffectParams =
|
|
276
304
|
| VignetteEffectParams
|
|
277
305
|
| FilmGrainEffectParams
|
|
278
306
|
| GaussianBlurEffectParams
|
|
279
|
-
| ColorAdjustEffectParams
|
|
307
|
+
| ColorAdjustEffectParams
|
|
308
|
+
| SepiaEffectParams
|
|
309
|
+
| BlackAndWhiteEffectParams
|
|
310
|
+
| SharpenEffectParams
|
|
311
|
+
| ChromaticAberrationEffectParams
|
|
312
|
+
| LetterboxEffectParams;
|
|
280
313
|
|
|
281
314
|
/** Effect clip — timed overlay adjustment layer over composed video */
|
|
282
315
|
interface EffectClip {
|
|
@@ -287,7 +320,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
287
320
|
duration?: number;
|
|
288
321
|
fadeIn?: number;
|
|
289
322
|
fadeOut?: number;
|
|
290
|
-
easing?: EffectEasing;
|
|
291
323
|
params: EffectParams;
|
|
292
324
|
}
|
|
293
325
|
|
|
@@ -389,6 +421,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
389
421
|
height?: number;
|
|
390
422
|
/** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
|
|
391
423
|
validationMode?: "warn" | "strict";
|
|
424
|
+
/** Default font file path (.ttf, .otf) applied to all text clips. Individual clips can override this with their own fontFile. */
|
|
425
|
+
fontFile?: string;
|
|
392
426
|
}
|
|
393
427
|
|
|
394
428
|
/** Log entry passed to onLog callback */
|
package/types/index.d.ts
CHANGED
|
@@ -246,8 +246,16 @@ declare namespace SIMPLEFFMPEG {
|
|
|
246
246
|
transition?: { type: string; duration: number };
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
type EffectName =
|
|
250
|
-
|
|
249
|
+
type EffectName =
|
|
250
|
+
| "vignette"
|
|
251
|
+
| "filmGrain"
|
|
252
|
+
| "gaussianBlur"
|
|
253
|
+
| "colorAdjust"
|
|
254
|
+
| "sepia"
|
|
255
|
+
| "blackAndWhite"
|
|
256
|
+
| "sharpen"
|
|
257
|
+
| "chromaticAberration"
|
|
258
|
+
| "letterbox";
|
|
251
259
|
|
|
252
260
|
interface EffectParamsBase {
|
|
253
261
|
/** Base blend amount from 0 to 1 (default: 1) */
|
|
@@ -260,6 +268,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
interface FilmGrainEffectParams extends EffectParamsBase {
|
|
271
|
+
/** Noise intensity 0-1 (default: 0.35). Independent from blend amount. */
|
|
272
|
+
strength?: number;
|
|
263
273
|
/** Temporal grain changes every frame (default: true) */
|
|
264
274
|
temporal?: boolean;
|
|
265
275
|
}
|
|
@@ -276,11 +286,40 @@ declare namespace SIMPLEFFMPEG {
|
|
|
276
286
|
gamma?: number;
|
|
277
287
|
}
|
|
278
288
|
|
|
289
|
+
interface SepiaEffectParams extends EffectParamsBase {}
|
|
290
|
+
|
|
291
|
+
interface BlackAndWhiteEffectParams extends EffectParamsBase {
|
|
292
|
+
/** Optional contrast boost (default: 1, range 0-3) */
|
|
293
|
+
contrast?: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface SharpenEffectParams extends EffectParamsBase {
|
|
297
|
+
/** Unsharp amount (default: 1.0, range 0-3) */
|
|
298
|
+
strength?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface ChromaticAberrationEffectParams extends EffectParamsBase {
|
|
302
|
+
/** Horizontal pixel offset for R/B channels (default: 4, range 0-20) */
|
|
303
|
+
shift?: number;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface LetterboxEffectParams extends EffectParamsBase {
|
|
307
|
+
/** Bar height as fraction of frame height (default: 0.12, range 0-0.5) */
|
|
308
|
+
size?: number;
|
|
309
|
+
/** Bar color (default: "black") */
|
|
310
|
+
color?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
279
313
|
type EffectParams =
|
|
280
314
|
| VignetteEffectParams
|
|
281
315
|
| FilmGrainEffectParams
|
|
282
316
|
| GaussianBlurEffectParams
|
|
283
|
-
| ColorAdjustEffectParams
|
|
317
|
+
| ColorAdjustEffectParams
|
|
318
|
+
| SepiaEffectParams
|
|
319
|
+
| BlackAndWhiteEffectParams
|
|
320
|
+
| SharpenEffectParams
|
|
321
|
+
| ChromaticAberrationEffectParams
|
|
322
|
+
| LetterboxEffectParams;
|
|
284
323
|
|
|
285
324
|
/** Effect clip — timed overlay adjustment layer over composed video */
|
|
286
325
|
interface EffectClip {
|
|
@@ -296,8 +335,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
296
335
|
fadeIn?: number;
|
|
297
336
|
/** Ramp-out duration in seconds */
|
|
298
337
|
fadeOut?: number;
|
|
299
|
-
/** Envelope easing for fade ramps */
|
|
300
|
-
easing?: EffectEasing;
|
|
301
338
|
/** Effect-specific params */
|
|
302
339
|
params: EffectParams;
|
|
303
340
|
}
|
|
@@ -400,6 +437,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
400
437
|
height?: number;
|
|
401
438
|
/** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
|
|
402
439
|
validationMode?: "warn" | "strict";
|
|
440
|
+
/** Default font file path (.ttf, .otf) applied to all text clips. Individual clips can override this with their own fontFile. */
|
|
441
|
+
fontFile?: string;
|
|
403
442
|
}
|
|
404
443
|
|
|
405
444
|
/** Log entry passed to onLog callback */
|