simple-ffmpegjs 0.3.6 → 0.4.0
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 +139 -38
- package/package.json +1 -1
- package/src/core/gaps.js +5 -31
- package/src/core/resolve.js +1 -1
- package/src/core/validation.js +295 -77
- package/src/ffmpeg/audio_builder.js +3 -1
- package/src/ffmpeg/bgm_builder.js +3 -1
- package/src/ffmpeg/effect_builder.js +138 -0
- package/src/ffmpeg/video_builder.js +20 -37
- package/src/lib/gradient.js +257 -0
- package/src/loaders.js +46 -0
- 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 +77 -0
- package/src/simpleffmpeg.js +59 -92
- package/types/index.d.mts +76 -5
- package/types/index.d.ts +87 -5
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
- [Logging](#logging)
|
|
32
32
|
- [Error Handling](#error-handling)
|
|
33
33
|
- [Cancellation](#cancellation)
|
|
34
|
-
- [
|
|
34
|
+
- [Color Clips](#color-clips)
|
|
35
35
|
- [Examples](#examples)
|
|
36
36
|
- [Clips & Transitions](#clips--transitions)
|
|
37
37
|
- [Text & Animations](#text--animations)
|
|
@@ -77,7 +77,8 @@ _Click to watch a "Wonders of the World" video created with simple-ffmpeg — co
|
|
|
77
77
|
- **Image Support** — Ken Burns effects (zoom, pan) for static images
|
|
78
78
|
- **Progress Tracking** — Real-time export progress callbacks
|
|
79
79
|
- **Cancellation** — AbortController support for stopping exports
|
|
80
|
-
- **
|
|
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
|
|
81
82
|
- **Auto-Batching** — Automatically splits complex filter graphs to avoid OS command limits
|
|
82
83
|
- **Schema Export** — Generate a structured description of the clip format for documentation, code generation, or AI context
|
|
83
84
|
- **Pre-Validation** — Validate clip configurations before processing with structured, machine-readable error codes
|
|
@@ -237,7 +238,7 @@ const schema = SIMPLEFFMPEG.getSchema({ exclude: ["text", "subtitle"] });
|
|
|
237
238
|
|
|
238
239
|
// See all available module IDs
|
|
239
240
|
SIMPLEFFMPEG.getSchemaModules();
|
|
240
|
-
// ['video', 'audio', 'image', 'text', 'subtitle', 'music']
|
|
241
|
+
// ['video', 'audio', 'image', 'color', 'effect', 'text', 'subtitle', 'music']
|
|
241
242
|
```
|
|
242
243
|
|
|
243
244
|
Available modules:
|
|
@@ -247,6 +248,8 @@ Available modules:
|
|
|
247
248
|
| `video` | Video clips, transitions, volume, trimming |
|
|
248
249
|
| `audio` | Standalone audio clips |
|
|
249
250
|
| `image` | Image clips, Ken Burns effects |
|
|
251
|
+
| `color` | Color clips — flat colors, linear/radial gradients |
|
|
252
|
+
| `effect` | Overlay adjustment effects — vignette, grain, blur, grading |
|
|
250
253
|
| `text` | Text overlays — all modes, animations, positioning, styling |
|
|
251
254
|
| `subtitle` | Subtitle file import (SRT, VTT, ASS, SSA) |
|
|
252
255
|
| `music` | Background music / background audio, looping |
|
|
@@ -284,7 +287,6 @@ new SIMPLEFFMPEG(options?: {
|
|
|
284
287
|
height?: number; // Output height (default: 1080)
|
|
285
288
|
fps?: number; // Frame rate (default: 30)
|
|
286
289
|
validationMode?: 'warn' | 'strict'; // Validation behavior (default: 'warn')
|
|
287
|
-
fillGaps?: boolean | string; // Gap handling: true/"black", any FFmpeg color, or "none"/false (default: "none")
|
|
288
290
|
preset?: string; // Platform preset (e.g., 'tiktok', 'youtube', 'instagram-post')
|
|
289
291
|
})
|
|
290
292
|
```
|
|
@@ -571,6 +573,62 @@ await project.load([
|
|
|
571
573
|
}
|
|
572
574
|
```
|
|
573
575
|
|
|
576
|
+
#### Color Clip
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
{
|
|
580
|
+
type: "color";
|
|
581
|
+
color: string | { // Flat color string or gradient spec
|
|
582
|
+
type: "linear-gradient" | "radial-gradient";
|
|
583
|
+
colors: string[]; // 2+ color stops (named, hex, or 0x hex)
|
|
584
|
+
direction?: "vertical" | "horizontal"; // For linear gradients (default: "vertical")
|
|
585
|
+
};
|
|
586
|
+
position?: number; // Timeline start (seconds). Omit to auto-sequence.
|
|
587
|
+
end?: number; // Timeline end. Use end OR duration, not both.
|
|
588
|
+
duration?: number; // Duration in seconds (alternative to end).
|
|
589
|
+
transition?: {
|
|
590
|
+
type: string; // Any xfade transition (e.g., 'fade', 'wipeleft')
|
|
591
|
+
duration: number;
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
#### Effect Clip
|
|
597
|
+
|
|
598
|
+
Effects are overlay adjustment layers. They apply to the already-composed video
|
|
599
|
+
for a time window, and can ramp in/out smoothly (instead of appearing instantly):
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
{
|
|
603
|
+
type: "effect";
|
|
604
|
+
effect: "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust";
|
|
605
|
+
position: number; // Required timeline start (seconds)
|
|
606
|
+
end?: number; // Use end OR duration, not both
|
|
607
|
+
duration?: number; // Duration in seconds (alternative to end)
|
|
608
|
+
fadeIn?: number; // Optional smooth ramp-in (seconds)
|
|
609
|
+
fadeOut?: number; // Optional smooth ramp-out (seconds)
|
|
610
|
+
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out"; // default: "linear"
|
|
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
|
+
};
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
574
632
|
#### Text Clip
|
|
575
633
|
|
|
576
634
|
```ts
|
|
@@ -852,47 +910,90 @@ try {
|
|
|
852
910
|
}
|
|
853
911
|
```
|
|
854
912
|
|
|
855
|
-
###
|
|
913
|
+
### Color Clips
|
|
856
914
|
|
|
857
|
-
|
|
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:
|
|
858
916
|
|
|
859
|
-
|
|
860
|
-
const project = new SIMPLEFFMPEG({
|
|
861
|
-
fillGaps: "black", // Fill gaps with black frames
|
|
862
|
-
});
|
|
917
|
+
**Flat color:**
|
|
863
918
|
|
|
919
|
+
```ts
|
|
864
920
|
await project.load([
|
|
865
|
-
|
|
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 },
|
|
866
925
|
]);
|
|
867
926
|
```
|
|
868
927
|
|
|
869
|
-
`
|
|
928
|
+
`color` accepts any valid FFmpeg color name or hex code:
|
|
870
929
|
|
|
871
930
|
```ts
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const project = new SIMPLEFFMPEG({ fillGaps: "navy" }); // named color
|
|
875
|
-
const project = new SIMPLEFFMPEG({ fillGaps: true }); // same as "black"
|
|
931
|
+
{ type: "color", color: "navy", position: 0, end: 3 }
|
|
932
|
+
{ type: "color", color: "#1a1a2e", position: 0, end: 3 }
|
|
876
933
|
```
|
|
877
934
|
|
|
878
|
-
|
|
935
|
+
**Gradients:**
|
|
879
936
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
+
}
|
|
883
945
|
|
|
884
|
-
|
|
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
|
+
}
|
|
885
953
|
|
|
886
|
-
|
|
887
|
-
|
|
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:
|
|
888
966
|
|
|
967
|
+
```ts
|
|
889
968
|
await project.load([
|
|
890
|
-
{ type: "
|
|
891
|
-
|
|
892
|
-
|
|
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
|
+
},
|
|
893
992
|
]);
|
|
894
993
|
```
|
|
895
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
|
+
|
|
896
997
|
## Examples
|
|
897
998
|
|
|
898
999
|
### Clips & Transitions
|
|
@@ -1419,7 +1520,7 @@ npm run test:watch
|
|
|
1419
1520
|
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
1521
|
|
|
1421
1522
|
```bash
|
|
1422
|
-
# Run all demos (
|
|
1523
|
+
# Run all demos (color clips, effects, transitions, text, Ken Burns, audio, watermarks, karaoke, torture test)
|
|
1423
1524
|
node examples/run-examples.js
|
|
1424
1525
|
|
|
1425
1526
|
# Run a specific demo by name (partial match)
|
|
@@ -1429,16 +1530,17 @@ node examples/run-examples.js torture ken
|
|
|
1429
1530
|
|
|
1430
1531
|
Available demo scripts (can also be run individually):
|
|
1431
1532
|
|
|
1432
|
-
| Script
|
|
1433
|
-
|
|
|
1434
|
-
| `demo-
|
|
1435
|
-
| `demo-
|
|
1436
|
-
| `demo-
|
|
1437
|
-
| `demo-
|
|
1438
|
-
| `demo-
|
|
1439
|
-
| `demo-
|
|
1440
|
-
| `demo-
|
|
1441
|
-
| `demo-
|
|
1533
|
+
| Script | What it tests |
|
|
1534
|
+
| ------------------------------- | -------------------------------------------------------------------------------------- |
|
|
1535
|
+
| `demo-color-clips.js` | Flat colors, linear/radial gradients, transitions, full composition with color clips |
|
|
1536
|
+
| `demo-effects-pack-1.js` | Timed overlay effects (vignette, grain, blur, color adjustment) with smooth ramps |
|
|
1537
|
+
| `demo-transitions.js` | Fade, wipe, slide, dissolve, fadeblack/white, short/long durations, image transitions |
|
|
1538
|
+
| `demo-text-and-animations.js` | Positioning, fade, pop, pop-bounce, typewriter, scale-in, pulse, styling, word-replace |
|
|
1539
|
+
| `demo-ken-burns.js` | All 6 presets, smart anchors, custom diagonal, slideshow with transitions |
|
|
1540
|
+
| `demo-audio-mixing.js` | Volume levels, background music, standalone audio, loop, multi-source mix |
|
|
1541
|
+
| `demo-watermarks.js` | Text/image watermarks, all positions, timed appearance, styled over transitions |
|
|
1542
|
+
| `demo-karaoke-and-subtitles.js` | Smooth/instant karaoke, word timestamps, multiline, SRT, VTT, mixed text+karaoke |
|
|
1543
|
+
| `demo-torture-test.js` | Kitchen sink, many clips+gaps+transitions, 6 simultaneous text animations, edge cases |
|
|
1442
1544
|
|
|
1443
1545
|
Each script header contains a `WHAT TO CHECK` section describing the expected visual output at every timestamp, making it easy to spot regressions.
|
|
1444
1546
|
|
|
@@ -1459,4 +1561,3 @@ Inspired by [ezffmpeg](https://github.com/ezffmpeg/ezffmpeg) by John Chen.
|
|
|
1459
1561
|
## License
|
|
1460
1562
|
|
|
1461
1563
|
MIT
|
|
1462
|
-
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-ffmpegjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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