simple-ffmpegjs 0.3.6 → 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 +244 -137
- package/package.json +1 -1
- package/src/core/gaps.js +5 -31
- package/src/core/resolve.js +1 -1
- package/src/core/validation.js +339 -77
- package/src/ffmpeg/audio_builder.js +3 -1
- package/src/ffmpeg/bgm_builder.js +23 -5
- package/src/ffmpeg/effect_builder.js +220 -0
- package/src/ffmpeg/video_builder.js +20 -37
- package/src/lib/gradient.js +257 -0
- package/src/loaders.js +46 -1
- package/src/schema/formatter.js +2 -0
- package/src/schema/index.js +4 -0
- package/src/schema/modules/color.js +54 -0
- package/src/schema/modules/effect.js +124 -0
- package/src/simpleffmpeg.js +61 -92
- package/types/index.d.mts +110 -5
- package/types/index.d.ts +126 -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
|
-
- [Gap Handling](#gap-handling)
|
|
35
35
|
- [Examples](#examples)
|
|
36
36
|
- [Clips & Transitions](#clips--transitions)
|
|
37
37
|
- [Text & Animations](#text--animations)
|
|
@@ -66,18 +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
|
-
- **Gap Handling** — Auto-fill timeline gaps with any color (including trailing gaps for text-on-background endings)
|
|
81
89
|
- **Auto-Batching** — Automatically splits complex filter graphs to avoid OS command limits
|
|
82
90
|
- **Schema Export** — Generate a structured description of the clip format for documentation, code generation, or AI context
|
|
83
91
|
- **Pre-Validation** — Validate clip configurations before processing with structured, machine-readable error codes
|
|
@@ -237,7 +245,7 @@ const schema = SIMPLEFFMPEG.getSchema({ exclude: ["text", "subtitle"] });
|
|
|
237
245
|
|
|
238
246
|
// See all available module IDs
|
|
239
247
|
SIMPLEFFMPEG.getSchemaModules();
|
|
240
|
-
// ['video', 'audio', 'image', 'text', 'subtitle', 'music']
|
|
248
|
+
// ['video', 'audio', 'image', 'color', 'effect', 'text', 'subtitle', 'music']
|
|
241
249
|
```
|
|
242
250
|
|
|
243
251
|
Available modules:
|
|
@@ -247,6 +255,8 @@ Available modules:
|
|
|
247
255
|
| `video` | Video clips, transitions, volume, trimming |
|
|
248
256
|
| `audio` | Standalone audio clips |
|
|
249
257
|
| `image` | Image clips, Ken Burns effects |
|
|
258
|
+
| `color` | Color clips — flat colors, linear/radial gradients |
|
|
259
|
+
| `effect` | Overlay adjustment effects — vignette, grain, blur, color adjust, sepia, B&W, sharpen, chromatic aberration, letterbox |
|
|
250
260
|
| `text` | Text overlays — all modes, animations, positioning, styling |
|
|
251
261
|
| `subtitle` | Subtitle file import (SRT, VTT, ASS, SSA) |
|
|
252
262
|
| `music` | Background music / background audio, looping |
|
|
@@ -284,11 +294,28 @@ new SIMPLEFFMPEG(options?: {
|
|
|
284
294
|
height?: number; // Output height (default: 1080)
|
|
285
295
|
fps?: number; // Frame rate (default: 30)
|
|
286
296
|
validationMode?: 'warn' | 'strict'; // Validation behavior (default: 'warn')
|
|
287
|
-
fillGaps?: boolean | string; // Gap handling: true/"black", any FFmpeg color, or "none"/false (default: "none")
|
|
288
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)
|
|
289
299
|
})
|
|
290
300
|
```
|
|
291
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
|
+
|
|
292
319
|
### Methods
|
|
293
320
|
|
|
294
321
|
#### `project.load(clips)`
|
|
@@ -318,43 +345,6 @@ SIMPLEFFMPEG.getDuration(clips); // 14.5
|
|
|
318
345
|
|
|
319
346
|
Useful for computing text overlay timings or background music end times before calling `load()`.
|
|
320
347
|
|
|
321
|
-
**Duration and Auto-Sequencing:**
|
|
322
|
-
|
|
323
|
-
For video, image, and audio clips, you can use shorthand to avoid specifying explicit `position` and `end` values:
|
|
324
|
-
|
|
325
|
-
- **`duration`** — Use instead of `end`. The library computes `end = position + duration`. You cannot specify both `duration` and `end` on the same clip.
|
|
326
|
-
- **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`.
|
|
327
|
-
|
|
328
|
-
These can be combined:
|
|
329
|
-
|
|
330
|
-
```ts
|
|
331
|
-
// Before: manual position/end for every clip
|
|
332
|
-
await project.load([
|
|
333
|
-
{ type: "video", url: "./a.mp4", position: 0, end: 5 },
|
|
334
|
-
{ type: "video", url: "./b.mp4", position: 5, end: 10 },
|
|
335
|
-
{ type: "video", url: "./c.mp4", position: 10, end: 18, cutFrom: 3 },
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
// After: auto-sequencing + duration
|
|
339
|
-
await project.load([
|
|
340
|
-
{ type: "video", url: "./a.mp4", duration: 5 },
|
|
341
|
-
{ type: "video", url: "./b.mp4", duration: 5 },
|
|
342
|
-
{ type: "video", url: "./c.mp4", duration: 8, cutFrom: 3 },
|
|
343
|
-
]);
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
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:
|
|
347
|
-
|
|
348
|
-
```ts
|
|
349
|
-
await project.load([
|
|
350
|
-
{ type: "video", url: "./a.mp4", duration: 5 }, // position: 0, end: 5
|
|
351
|
-
{ type: "video", url: "./b.mp4", position: 10, end: 15 }, // explicit gap
|
|
352
|
-
{ type: "video", url: "./c.mp4", duration: 5 }, // position: 15, end: 20
|
|
353
|
-
]);
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
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.
|
|
357
|
-
|
|
358
348
|
#### `SIMPLEFFMPEG.probe(filePath)`
|
|
359
349
|
|
|
360
350
|
Probe a media file and return comprehensive metadata using ffprobe. Works with video, audio, and image files.
|
|
@@ -480,6 +470,43 @@ await project.preview(options?: ExportOptions): Promise<{
|
|
|
480
470
|
}>
|
|
481
471
|
```
|
|
482
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
|
+
|
|
483
510
|
### Clip Types
|
|
484
511
|
|
|
485
512
|
#### Video Clip
|
|
@@ -502,75 +529,155 @@ await project.preview(options?: ExportOptions): Promise<{
|
|
|
502
529
|
|
|
503
530
|
All [xfade transitions](https://trac.ffmpeg.org/wiki/Xfade) are supported.
|
|
504
531
|
|
|
505
|
-
####
|
|
532
|
+
#### Image Clip
|
|
506
533
|
|
|
507
534
|
```ts
|
|
508
535
|
{
|
|
509
|
-
type: "
|
|
536
|
+
type: "image";
|
|
510
537
|
url: string;
|
|
511
|
-
position?: number; // Omit to auto-sequence after previous
|
|
538
|
+
position?: number; // Omit to auto-sequence after previous video/image clip
|
|
512
539
|
end?: number; // Use end OR duration, not both
|
|
513
540
|
duration?: number; // Duration in seconds (alternative to end)
|
|
514
|
-
|
|
515
|
-
|
|
541
|
+
width?: number; // Optional: source image width (skip probe / override)
|
|
542
|
+
height?: number; // Optional: source image height (skip probe / override)
|
|
543
|
+
kenBurns?:
|
|
544
|
+
| "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "pan-up" | "pan-down"
|
|
545
|
+
| "smart" | "custom"
|
|
546
|
+
| {
|
|
547
|
+
type?: "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "pan-up" | "pan-down" | "smart" | "custom";
|
|
548
|
+
startZoom?: number;
|
|
549
|
+
endZoom?: number;
|
|
550
|
+
startX?: number; // 0 = left, 1 = right
|
|
551
|
+
startY?: number; // 0 = top, 1 = bottom
|
|
552
|
+
endX?: number;
|
|
553
|
+
endY?: number;
|
|
554
|
+
anchor?: "top" | "bottom" | "left" | "right";
|
|
555
|
+
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
556
|
+
};
|
|
516
557
|
}
|
|
517
558
|
```
|
|
518
559
|
|
|
519
|
-
####
|
|
560
|
+
#### Color Clip
|
|
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.
|
|
520
563
|
|
|
521
564
|
```ts
|
|
522
565
|
{
|
|
523
|
-
type: "
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
566
|
+
type: "color";
|
|
567
|
+
color: string | { // Flat color string or gradient spec
|
|
568
|
+
type: "linear-gradient" | "radial-gradient";
|
|
569
|
+
colors: string[]; // 2+ color stops (named, hex, or 0x hex)
|
|
570
|
+
direction?: "vertical" | "horizontal"; // For linear gradients (default: "vertical")
|
|
571
|
+
};
|
|
572
|
+
position?: number; // Timeline start (seconds). Omit to auto-sequence.
|
|
573
|
+
end?: number; // Timeline end. Use end OR duration, not both.
|
|
574
|
+
duration?: number; // Duration in seconds (alternative to end).
|
|
575
|
+
transition?: {
|
|
576
|
+
type: string; // Any xfade transition (e.g., 'fade', 'wipeleft')
|
|
577
|
+
duration: number;
|
|
578
|
+
};
|
|
530
579
|
}
|
|
531
580
|
```
|
|
532
581
|
|
|
533
|
-
|
|
582
|
+
`color` accepts any valid FFmpeg color name or hex code:
|
|
534
583
|
|
|
535
|
-
|
|
584
|
+
```ts
|
|
585
|
+
{ type: "color", color: "navy", position: 0, end: 3 }
|
|
586
|
+
{ type: "color", color: "#1a1a2e", position: 0, end: 3 }
|
|
587
|
+
```
|
|
536
588
|
|
|
537
|
-
|
|
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:**
|
|
538
618
|
|
|
539
619
|
```ts
|
|
540
620
|
await project.load([
|
|
541
|
-
{ type: "
|
|
542
|
-
{
|
|
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
|
+
},
|
|
543
644
|
]);
|
|
544
645
|
```
|
|
545
646
|
|
|
546
|
-
|
|
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
|
+
|
|
649
|
+
#### Effect Clip
|
|
650
|
+
|
|
651
|
+
Effects are overlay adjustment layers. They apply to the already-composed video
|
|
652
|
+
for a time window, and can ramp in/out smoothly (instead of appearing instantly):
|
|
547
653
|
|
|
548
654
|
```ts
|
|
549
655
|
{
|
|
550
|
-
type: "
|
|
551
|
-
|
|
552
|
-
position
|
|
553
|
-
end?: number;
|
|
554
|
-
duration?: number;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
| "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "pan-up" | "pan-down"
|
|
559
|
-
| "smart" | "custom"
|
|
560
|
-
| {
|
|
561
|
-
type?: "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "pan-up" | "pan-down" | "smart" | "custom";
|
|
562
|
-
startZoom?: number;
|
|
563
|
-
endZoom?: number;
|
|
564
|
-
startX?: number; // 0 = left, 1 = right
|
|
565
|
-
startY?: number; // 0 = top, 1 = bottom
|
|
566
|
-
endX?: number;
|
|
567
|
-
endY?: number;
|
|
568
|
-
anchor?: "top" | "bottom" | "left" | "right";
|
|
569
|
-
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
570
|
-
};
|
|
656
|
+
type: "effect";
|
|
657
|
+
effect: EffectName; // See table below
|
|
658
|
+
position: number; // Required timeline start (seconds)
|
|
659
|
+
end?: number; // Use end OR duration, not both
|
|
660
|
+
duration?: number; // Duration in seconds (alternative to end)
|
|
661
|
+
fadeIn?: number; // Optional smooth ramp-in (seconds)
|
|
662
|
+
fadeOut?: number; // Optional smooth ramp-out (seconds)
|
|
663
|
+
params: EffectParams; // Effect-specific parameters (see table below)
|
|
571
664
|
}
|
|
572
665
|
```
|
|
573
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
|
+
|
|
574
681
|
#### Text Clip
|
|
575
682
|
|
|
576
683
|
```ts
|
|
@@ -640,6 +747,47 @@ Import external subtitle files (SRT, VTT, ASS/SSA):
|
|
|
640
747
|
}
|
|
641
748
|
```
|
|
642
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
|
+
|
|
643
791
|
### Platform Presets
|
|
644
792
|
|
|
645
793
|
Use platform presets to quickly configure optimal dimensions for social media:
|
|
@@ -852,47 +1000,6 @@ try {
|
|
|
852
1000
|
}
|
|
853
1001
|
```
|
|
854
1002
|
|
|
855
|
-
### Gap Handling
|
|
856
|
-
|
|
857
|
-
By default, timeline gaps (periods with no video/image content) produce a validation warning. Enable automatic gap filling to insert solid-color frames wherever there's no visual media — leading gaps, middle gaps, and trailing gaps are all handled:
|
|
858
|
-
|
|
859
|
-
```ts
|
|
860
|
-
const project = new SIMPLEFFMPEG({
|
|
861
|
-
fillGaps: "black", // Fill gaps with black frames
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
await project.load([
|
|
865
|
-
{ type: "video", url: "./clip.mp4", position: 2, end: 5 }, // Leading gap from 0-2s filled with black
|
|
866
|
-
]);
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
`fillGaps` accepts any valid FFmpeg color — named colors, hex codes, or `true` as shorthand for `"black"`:
|
|
870
|
-
|
|
871
|
-
```ts
|
|
872
|
-
// Custom color fill
|
|
873
|
-
const project = new SIMPLEFFMPEG({ fillGaps: "#0a0a2e" }); // dark navy
|
|
874
|
-
const project = new SIMPLEFFMPEG({ fillGaps: "navy" }); // named color
|
|
875
|
-
const project = new SIMPLEFFMPEG({ fillGaps: true }); // same as "black"
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
All three gap types are supported:
|
|
879
|
-
|
|
880
|
-
- **Leading gaps** — no visual media at the start of the timeline (e.g. video starts at 2s, gap from 0-2s)
|
|
881
|
-
- **Middle gaps** — periods between visual clips with no media
|
|
882
|
-
- **Trailing gaps** — text or audio extends past the last visual clip, so the video is extended with the fill color
|
|
883
|
-
|
|
884
|
-
Trailing gaps are useful for ending a video with text on a solid background:
|
|
885
|
-
|
|
886
|
-
```ts
|
|
887
|
-
const project = new SIMPLEFFMPEG({ fillGaps: "#1a1a2e" });
|
|
888
|
-
|
|
889
|
-
await project.load([
|
|
890
|
-
{ type: "video", url: "./clip.mp4", position: 0, end: 5 },
|
|
891
|
-
// Text extends 5 seconds past the video — dark blue fill from 5-10s
|
|
892
|
-
{ type: "text", text: "The End", position: 4, end: 10, fontSize: 64, fontColor: "white" },
|
|
893
|
-
]);
|
|
894
|
-
```
|
|
895
|
-
|
|
896
1003
|
## Examples
|
|
897
1004
|
|
|
898
1005
|
### Clips & Transitions
|
|
@@ -954,7 +1061,7 @@ await project.load([
|
|
|
954
1061
|
]);
|
|
955
1062
|
```
|
|
956
1063
|
|
|
957
|
-
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.
|
|
958
1065
|
|
|
959
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.
|
|
960
1067
|
> If you pass `width`/`height`, they override probed dimensions (useful for remote or generated images).
|
|
@@ -1419,7 +1526,7 @@ npm run test:watch
|
|
|
1419
1526
|
For visual verification, run the demo suite to generate sample videos covering all major features. Each demo outputs to its own subfolder under `examples/output/` and includes annotated expected timelines so you know exactly what to look for:
|
|
1420
1527
|
|
|
1421
1528
|
```bash
|
|
1422
|
-
# Run all demos (
|
|
1529
|
+
# Run all demos (color clips, effects, transitions, text, Ken Burns, audio, watermarks, karaoke, torture test)
|
|
1423
1530
|
node examples/run-examples.js
|
|
1424
1531
|
|
|
1425
1532
|
# Run a specific demo by name (partial match)
|
|
@@ -1429,16 +1536,17 @@ node examples/run-examples.js torture ken
|
|
|
1429
1536
|
|
|
1430
1537
|
Available demo scripts (can also be run individually):
|
|
1431
1538
|
|
|
1432
|
-
| Script
|
|
1433
|
-
|
|
|
1434
|
-
| `demo-
|
|
1435
|
-
| `demo-
|
|
1436
|
-
| `demo-
|
|
1437
|
-
| `demo-
|
|
1438
|
-
| `demo-
|
|
1439
|
-
| `demo-
|
|
1440
|
-
| `demo-
|
|
1441
|
-
| `demo-
|
|
1539
|
+
| Script | What it tests |
|
|
1540
|
+
| ------------------------------- | -------------------------------------------------------------------------------------- |
|
|
1541
|
+
| `demo-color-clips.js` | Flat colors, linear/radial gradients, transitions, full composition with color clips |
|
|
1542
|
+
| `demo-effects.js` | Timed overlay effects (all 9 effects) with smooth fade ramps |
|
|
1543
|
+
| `demo-transitions.js` | Fade, wipe, slide, dissolve, fadeblack/white, short/long durations, image transitions |
|
|
1544
|
+
| `demo-text-and-animations.js` | Positioning, fade, pop, pop-bounce, typewriter, scale-in, pulse, styling, word-replace |
|
|
1545
|
+
| `demo-ken-burns.js` | All 6 presets, smart anchors, custom diagonal, slideshow with transitions |
|
|
1546
|
+
| `demo-audio-mixing.js` | Volume levels, background music, standalone audio, loop, multi-source mix |
|
|
1547
|
+
| `demo-watermarks.js` | Text/image watermarks, all positions, timed appearance, styled over transitions |
|
|
1548
|
+
| `demo-karaoke-and-subtitles.js` | Smooth/instant karaoke, word timestamps, multiline, SRT, VTT, mixed text+karaoke |
|
|
1549
|
+
| `demo-torture-test.js` | Kitchen sink, many clips+gaps+transitions, 6 simultaneous text animations, edge cases |
|
|
1442
1550
|
|
|
1443
1551
|
Each script header contains a `WHAT TO CHECK` section describing the expected visual output at every timestamp, making it easy to spot regressions.
|
|
1444
1552
|
|
|
@@ -1459,4 +1567,3 @@ Inspired by [ezffmpeg](https://github.com/ezffmpeg/ezffmpeg) by John Chen.
|
|
|
1459
1567
|
## License
|
|
1460
1568
|
|
|
1461
1569
|
MIT
|
|
1462
|
-
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-ffmpegjs",
|
|
3
|
-
"version": "0.
|
|
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/gaps.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Detect visual gaps in a timeline of video/image clips.
|
|
2
|
+
* Detect visual gaps in a timeline of video/image/color clips.
|
|
3
3
|
* Returns an array of gap objects with {start, end, duration} properties.
|
|
4
4
|
*
|
|
5
5
|
* @param {Array<{type: string, position: number, end: number}>} clips - Array of clips
|
|
6
6
|
* @param {Object} options - Options
|
|
7
7
|
* @param {number} options.epsilon - Tolerance for gap detection (default 0.001)
|
|
8
|
-
* @param {number} [options.timelineEnd] - Desired end of the timeline; if set and greater
|
|
9
|
-
* than the last visual clip's end, a trailing gap is appended.
|
|
10
8
|
* @returns {Array<{start: number, end: number, duration: number}>} Array of gaps
|
|
11
9
|
*/
|
|
12
10
|
function detectVisualGaps(clips, options = {}) {
|
|
13
|
-
const { epsilon = 1e-3
|
|
11
|
+
const { epsilon = 1e-3 } = options;
|
|
14
12
|
|
|
15
|
-
// Filter to only visual clips (video/image) and sort by position
|
|
13
|
+
// Filter to only visual clips (video/image/color) and sort by position
|
|
16
14
|
const visual = clips
|
|
17
|
-
.filter((c) => c.type === "video" || c.type === "image")
|
|
15
|
+
.filter((c) => c.type === "video" || c.type === "image" || c.type === "color")
|
|
18
16
|
.map((c) => ({
|
|
19
17
|
position: c.position || 0,
|
|
20
18
|
end: c.end || 0,
|
|
@@ -24,18 +22,6 @@ function detectVisualGaps(clips, options = {}) {
|
|
|
24
22
|
const gaps = [];
|
|
25
23
|
|
|
26
24
|
if (visual.length === 0) {
|
|
27
|
-
// If no visual clips but a timeline end is specified, the entire range is a gap
|
|
28
|
-
if (
|
|
29
|
-
typeof timelineEnd === "number" &&
|
|
30
|
-
Number.isFinite(timelineEnd) &&
|
|
31
|
-
timelineEnd > epsilon
|
|
32
|
-
) {
|
|
33
|
-
gaps.push({
|
|
34
|
-
start: 0,
|
|
35
|
-
end: timelineEnd,
|
|
36
|
-
duration: timelineEnd,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
25
|
return gaps;
|
|
40
26
|
}
|
|
41
27
|
|
|
@@ -62,18 +48,6 @@ function detectVisualGaps(clips, options = {}) {
|
|
|
62
48
|
}
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
// Check for trailing gap (gap at the end after last clip)
|
|
66
|
-
if (typeof timelineEnd === "number" && Number.isFinite(timelineEnd)) {
|
|
67
|
-
const lastEnd = visual[visual.length - 1].end;
|
|
68
|
-
if (timelineEnd - lastEnd > epsilon) {
|
|
69
|
-
gaps.push({
|
|
70
|
-
start: lastEnd,
|
|
71
|
-
end: timelineEnd,
|
|
72
|
-
duration: timelineEnd - lastEnd,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
51
|
return gaps;
|
|
78
52
|
}
|
|
79
53
|
|
|
@@ -95,7 +69,7 @@ function hasVisualGaps(clips, options = {}) {
|
|
|
95
69
|
* @returns {number} The end time of the last visual clip, or 0 if no visual clips
|
|
96
70
|
*/
|
|
97
71
|
function getVisualTimelineEnd(clips) {
|
|
98
|
-
const visual = clips.filter((c) => c.type === "video" || c.type === "image");
|
|
72
|
+
const visual = clips.filter((c) => c.type === "video" || c.type === "image" || c.type === "color");
|
|
99
73
|
if (visual.length === 0) return 0;
|
|
100
74
|
return Math.max(...visual.map((c) => c.end || 0));
|
|
101
75
|
}
|
package/src/core/resolve.js
CHANGED