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/src/simpleffmpeg.js
CHANGED
|
@@ -7,6 +7,7 @@ const Loaders = require("./loaders");
|
|
|
7
7
|
const { buildVideoFilter } = require("./ffmpeg/video_builder");
|
|
8
8
|
const { buildAudioForVideoClips } = require("./ffmpeg/audio_builder");
|
|
9
9
|
const { buildBackgroundMusicMix } = require("./ffmpeg/bgm_builder");
|
|
10
|
+
const { buildEffectFilters } = require("./ffmpeg/effect_builder");
|
|
10
11
|
const {
|
|
11
12
|
getClipAudioString,
|
|
12
13
|
hasProblematicChars,
|
|
@@ -16,7 +17,6 @@ const {
|
|
|
16
17
|
validateConfig,
|
|
17
18
|
formatValidationResult,
|
|
18
19
|
ValidationCodes,
|
|
19
|
-
normalizeFillGaps,
|
|
20
20
|
} = require("./core/validation");
|
|
21
21
|
const {
|
|
22
22
|
SimpleffmpegError,
|
|
@@ -45,7 +45,6 @@ const {
|
|
|
45
45
|
const { getSchema, getSchemaModules } = require("./schema");
|
|
46
46
|
const { resolveClips } = require("./core/resolve");
|
|
47
47
|
const { probeMedia } = require("./core/media_info");
|
|
48
|
-
const { detectVisualGaps } = require("./core/gaps");
|
|
49
48
|
|
|
50
49
|
class SIMPLEFFMPEG {
|
|
51
50
|
/**
|
|
@@ -57,7 +56,6 @@ class SIMPLEFFMPEG {
|
|
|
57
56
|
* @param {number} options.fps - Frames per second (default: 30)
|
|
58
57
|
* @param {string} options.preset - Platform preset ('tiktok', 'youtube', 'instagram-post', etc.)
|
|
59
58
|
* @param {string} options.validationMode - Validation behavior: 'warn' or 'strict' (default: 'warn')
|
|
60
|
-
* @param {string|boolean} options.fillGaps - Gap handling: 'none'/false (disabled), true/'black' (black fill), or any valid FFmpeg color (default: 'none')
|
|
61
59
|
*
|
|
62
60
|
* @example
|
|
63
61
|
* const project = new SIMPLEFFMPEG({ preset: 'tiktok' });
|
|
@@ -66,16 +64,7 @@ class SIMPLEFFMPEG {
|
|
|
66
64
|
* const project = new SIMPLEFFMPEG({
|
|
67
65
|
* width: 1920,
|
|
68
66
|
* height: 1080,
|
|
69
|
-
* fps: 30
|
|
70
|
-
* fillGaps: 'black'
|
|
71
|
-
* });
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* // Fill gaps with a custom color
|
|
75
|
-
* const project = new SIMPLEFFMPEG({
|
|
76
|
-
* width: 1920,
|
|
77
|
-
* height: 1080,
|
|
78
|
-
* fillGaps: '#1a1a2e' // dark blue-gray
|
|
67
|
+
* fps: 30
|
|
79
68
|
* });
|
|
80
69
|
*/
|
|
81
70
|
constructor(options = {}) {
|
|
@@ -91,24 +80,18 @@ class SIMPLEFFMPEG {
|
|
|
91
80
|
);
|
|
92
81
|
}
|
|
93
82
|
|
|
94
|
-
// Normalise and validate fillGaps
|
|
95
|
-
const fillGapsResult = normalizeFillGaps(options.fillGaps);
|
|
96
|
-
if (fillGapsResult.error) {
|
|
97
|
-
throw new ValidationError(fillGapsResult.error);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
83
|
// Explicit options override preset values
|
|
101
84
|
this.options = {
|
|
102
85
|
fps: options.fps || presetConfig.fps || C.DEFAULT_FPS,
|
|
103
86
|
width: options.width || presetConfig.width || C.DEFAULT_WIDTH,
|
|
104
87
|
height: options.height || presetConfig.height || C.DEFAULT_HEIGHT,
|
|
105
88
|
validationMode: options.validationMode || C.DEFAULT_VALIDATION_MODE,
|
|
106
|
-
fillGaps: fillGapsResult.color, // "none" | valid FFmpeg color
|
|
107
89
|
preset: options.preset || null,
|
|
108
90
|
};
|
|
109
91
|
this.videoOrAudioClips = [];
|
|
110
92
|
this.textClips = [];
|
|
111
93
|
this.subtitleClips = [];
|
|
94
|
+
this.effectClips = [];
|
|
112
95
|
this.filesToClean = [];
|
|
113
96
|
this._isLoading = false; // Guard against concurrent load() calls
|
|
114
97
|
this._isExporting = false; // Guard against concurrent export() calls
|
|
@@ -121,9 +104,15 @@ class SIMPLEFFMPEG {
|
|
|
121
104
|
*/
|
|
122
105
|
_getInputStreams() {
|
|
123
106
|
return this.videoOrAudioClips
|
|
107
|
+
.filter((clip) => {
|
|
108
|
+
// Flat color clips use the color= filter source — no file input needed
|
|
109
|
+
if (clip.type === "color" && clip._isFlatColor) return false;
|
|
110
|
+
return true;
|
|
111
|
+
})
|
|
124
112
|
.map((clip) => {
|
|
125
113
|
const escapedUrl = escapeFilePath(clip.url);
|
|
126
|
-
|
|
114
|
+
// Gradient color clips and image clips are looped images
|
|
115
|
+
if (clip.type === "image" || (clip.type === "color" && !clip._isFlatColor)) {
|
|
127
116
|
const duration = Math.max(0, clip.end - clip.position || 0);
|
|
128
117
|
return `-loop 1 -t ${duration} -i "${escapedUrl}"`;
|
|
129
118
|
}
|
|
@@ -204,7 +193,7 @@ class SIMPLEFFMPEG {
|
|
|
204
193
|
* Load clips into the project for processing
|
|
205
194
|
*
|
|
206
195
|
* @param {Array} clipObjs - Array of clip configuration objects
|
|
207
|
-
* @param {string} clipObjs[].type - Clip type: 'video', 'audio', 'image', 'text', 'music', 'backgroundAudio', 'subtitle'
|
|
196
|
+
* @param {string} clipObjs[].type - Clip type: 'video', 'audio', 'image', 'color', 'text', 'effect', 'music', 'backgroundAudio', 'subtitle'
|
|
208
197
|
* @param {string} clipObjs[].url - Media file path (required for video, audio, image, music, subtitle)
|
|
209
198
|
* @param {number} clipObjs[].position - Start time on timeline in seconds
|
|
210
199
|
* @param {number} clipObjs[].end - End time on timeline in seconds
|
|
@@ -239,7 +228,6 @@ class SIMPLEFFMPEG {
|
|
|
239
228
|
|
|
240
229
|
// Merge resolution errors into validation
|
|
241
230
|
const result = validateConfig(resolved.clips, {
|
|
242
|
-
fillGaps: this.options.fillGaps,
|
|
243
231
|
width: this.options.width,
|
|
244
232
|
height: this.options.height,
|
|
245
233
|
});
|
|
@@ -271,12 +259,16 @@ class SIMPLEFFMPEG {
|
|
|
271
259
|
if (clipObj.type === "video" || clipObj.type === "audio") {
|
|
272
260
|
clipObj.volume = clipObj.volume != null ? clipObj.volume : 1;
|
|
273
261
|
clipObj.cutFrom = clipObj.cutFrom || 0;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
262
|
+
}
|
|
263
|
+
// Normalize transitions for all visual clip types
|
|
264
|
+
if (
|
|
265
|
+
(clipObj.type === "video" || clipObj.type === "image" || clipObj.type === "color") &&
|
|
266
|
+
clipObj.transition
|
|
267
|
+
) {
|
|
268
|
+
clipObj.transition = {
|
|
269
|
+
type: clipObj.transition.type || clipObj.transition,
|
|
270
|
+
duration: clipObj.transition.duration || 0.5,
|
|
271
|
+
};
|
|
280
272
|
}
|
|
281
273
|
if (clipObj.type === "video") {
|
|
282
274
|
return Loaders.loadVideo(this, clipObj);
|
|
@@ -287,9 +279,15 @@ class SIMPLEFFMPEG {
|
|
|
287
279
|
if (clipObj.type === "text") {
|
|
288
280
|
return Loaders.loadText(this, clipObj);
|
|
289
281
|
}
|
|
282
|
+
if (clipObj.type === "effect") {
|
|
283
|
+
return Loaders.loadEffect(this, clipObj);
|
|
284
|
+
}
|
|
290
285
|
if (clipObj.type === "image") {
|
|
291
286
|
return Loaders.loadImage(this, clipObj);
|
|
292
287
|
}
|
|
288
|
+
if (clipObj.type === "color") {
|
|
289
|
+
return Loaders.loadColor(this, clipObj);
|
|
290
|
+
}
|
|
293
291
|
if (clipObj.type === "music" || clipObj.type === "backgroundAudio") {
|
|
294
292
|
return Loaders.loadBackgroundAudio(this, clipObj);
|
|
295
293
|
}
|
|
@@ -397,8 +395,22 @@ class SIMPLEFFMPEG {
|
|
|
397
395
|
})
|
|
398
396
|
);
|
|
399
397
|
|
|
398
|
+
// Build a mapping from clip to its FFmpeg input stream index.
|
|
399
|
+
// Flat color clips use the color= filter source and do not have file inputs,
|
|
400
|
+
// so they are skipped by _getInputStreams(). All other clips' indices must
|
|
401
|
+
// account for this offset.
|
|
402
|
+
this._inputIndexMap = new Map();
|
|
403
|
+
let _inputIdx = 0;
|
|
404
|
+
for (const clip of this.videoOrAudioClips) {
|
|
405
|
+
if (clip.type === "color" && clip._isFlatColor) {
|
|
406
|
+
continue; // No file input for flat color clips
|
|
407
|
+
}
|
|
408
|
+
this._inputIndexMap.set(clip, _inputIdx);
|
|
409
|
+
_inputIdx++;
|
|
410
|
+
}
|
|
411
|
+
|
|
400
412
|
const videoClips = this.videoOrAudioClips.filter(
|
|
401
|
-
(clip) => clip.type === "video" || clip.type === "image"
|
|
413
|
+
(clip) => clip.type === "video" || clip.type === "image" || clip.type === "color"
|
|
402
414
|
);
|
|
403
415
|
const audioClips = this.videoOrAudioClips.filter(
|
|
404
416
|
(clip) => clip.type === "audio"
|
|
@@ -442,56 +454,6 @@ class SIMPLEFFMPEG {
|
|
|
442
454
|
.map((c) => (typeof c.end === "number" ? c.end : 0));
|
|
443
455
|
const bgOrAudioEnd = audioEnds.length > 0 ? Math.max(...audioEnds) : 0;
|
|
444
456
|
|
|
445
|
-
// Compute desired timeline end for trailing gap filling.
|
|
446
|
-
// When fillGaps is enabled, extend the video to cover text/audio clips
|
|
447
|
-
// that extend past the last visual clip (e.g. ending with text on black).
|
|
448
|
-
//
|
|
449
|
-
// The trailing gap duration must be precise so the video output ends
|
|
450
|
-
// exactly when the last content ends. Two factors affect this:
|
|
451
|
-
//
|
|
452
|
-
// 1. Transition compensation — when active (default), text timestamps
|
|
453
|
-
// shift left by the cumulative transition overlap, so the target end
|
|
454
|
-
// is the compensated value. When off, use the raw overall end.
|
|
455
|
-
//
|
|
456
|
-
// 2. Existing gaps — leading/middle gaps that will also be filled add
|
|
457
|
-
// to the video output but are NOT included in totalVideoDuration.
|
|
458
|
-
// We must subtract them so the trailing gap isn't oversized.
|
|
459
|
-
let timelineEnd;
|
|
460
|
-
if (this.options.fillGaps !== "none" && videoClips.length > 0) {
|
|
461
|
-
const visualEnd = Math.max(...videoClips.map((c) => c.end || 0));
|
|
462
|
-
const overallEnd = Math.max(visualEnd, textEnd, bgOrAudioEnd);
|
|
463
|
-
if (overallEnd - visualEnd > 1e-3) {
|
|
464
|
-
// Target output duration depends on whether text is compensated
|
|
465
|
-
let desiredOutputDuration;
|
|
466
|
-
if (exportOptions.compensateTransitions && videoClips.length > 1) {
|
|
467
|
-
const transitionOverlap = this._getTransitionOffsetAt(
|
|
468
|
-
videoClips,
|
|
469
|
-
overallEnd
|
|
470
|
-
);
|
|
471
|
-
desiredOutputDuration = overallEnd - transitionOverlap;
|
|
472
|
-
} else {
|
|
473
|
-
desiredOutputDuration = overallEnd;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Account for existing gaps (leading + middle) that will also be
|
|
477
|
-
// filled — these add to the video output but aren't reflected in
|
|
478
|
-
// totalVideoDuration (which only sums clip durations − transitions).
|
|
479
|
-
const existingGaps = detectVisualGaps(videoClips);
|
|
480
|
-
const existingGapDuration = existingGaps.reduce(
|
|
481
|
-
(sum, g) => sum + g.duration,
|
|
482
|
-
0
|
|
483
|
-
);
|
|
484
|
-
const videoOutputBeforeTrailing =
|
|
485
|
-
totalVideoDuration + existingGapDuration;
|
|
486
|
-
|
|
487
|
-
const trailingGapDuration =
|
|
488
|
-
desiredOutputDuration - videoOutputBeforeTrailing;
|
|
489
|
-
if (trailingGapDuration > 1e-3) {
|
|
490
|
-
timelineEnd = visualEnd + trailingGapDuration;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
457
|
let finalVisualEnd =
|
|
496
458
|
videoClips.length > 0
|
|
497
459
|
? Math.max(...videoClips.map((c) => c.end))
|
|
@@ -499,21 +461,18 @@ class SIMPLEFFMPEG {
|
|
|
499
461
|
|
|
500
462
|
// Build video filter
|
|
501
463
|
if (videoClips.length > 0) {
|
|
502
|
-
const vres = buildVideoFilter(this, videoClips
|
|
464
|
+
const vres = buildVideoFilter(this, videoClips);
|
|
503
465
|
filterComplex += vres.filter;
|
|
504
466
|
finalVideoLabel = vres.finalVideoLabel;
|
|
505
467
|
hasVideo = vres.hasVideo;
|
|
506
468
|
|
|
507
|
-
// Update durations to account for gap fills (including trailing gaps).
|
|
508
|
-
// videoDuration reflects the actual output length of the video filter
|
|
509
|
-
// chain, which includes any black gap-fill clips.
|
|
510
|
-
if (typeof vres.videoDuration === "number" && vres.videoDuration > 0) {
|
|
511
|
-
totalVideoDuration = vres.videoDuration;
|
|
512
|
-
}
|
|
513
469
|
// Use the actual video output length for finalVisualEnd so that
|
|
514
470
|
// audio trim and BGM duration match the real video stream length,
|
|
515
471
|
// rather than an original-timeline position that may differ due to
|
|
516
472
|
// transition compression.
|
|
473
|
+
if (typeof vres.videoDuration === "number" && vres.videoDuration > 0) {
|
|
474
|
+
totalVideoDuration = vres.videoDuration;
|
|
475
|
+
}
|
|
517
476
|
if (
|
|
518
477
|
typeof vres.videoDuration === "number" &&
|
|
519
478
|
vres.videoDuration > finalVisualEnd
|
|
@@ -522,6 +481,13 @@ class SIMPLEFFMPEG {
|
|
|
522
481
|
}
|
|
523
482
|
}
|
|
524
483
|
|
|
484
|
+
// Overlay effects (adjustment layer clips) on the composed video output.
|
|
485
|
+
if (this.effectClips.length > 0 && hasVideo && finalVideoLabel) {
|
|
486
|
+
const effectRes = buildEffectFilters(this.effectClips, finalVideoLabel);
|
|
487
|
+
filterComplex += effectRes.filter;
|
|
488
|
+
finalVideoLabel = effectRes.finalVideoLabel || finalVideoLabel;
|
|
489
|
+
}
|
|
490
|
+
|
|
525
491
|
// Audio for video clips (aligned amix)
|
|
526
492
|
// Compute cumulative transition offsets so audio adelay values
|
|
527
493
|
// match the xfade-compressed video timeline.
|
|
@@ -545,7 +511,9 @@ class SIMPLEFFMPEG {
|
|
|
545
511
|
let audioString = "";
|
|
546
512
|
let audioConcatInputs = [];
|
|
547
513
|
audioClips.forEach((clip) => {
|
|
548
|
-
const inputIndex = this.
|
|
514
|
+
const inputIndex = this._inputIndexMap
|
|
515
|
+
? this._inputIndexMap.get(clip)
|
|
516
|
+
: this.videoOrAudioClips.indexOf(clip);
|
|
549
517
|
const { audioStringPart, audioConcatInput } = getClipAudioString(
|
|
550
518
|
clip,
|
|
551
519
|
inputIndex
|
|
@@ -1203,7 +1171,6 @@ class SIMPLEFFMPEG {
|
|
|
1203
1171
|
* @param {Array} clips - Array of clip objects to validate
|
|
1204
1172
|
* @param {Object} options - Validation options
|
|
1205
1173
|
* @param {boolean} options.skipFileChecks - Skip file existence checks (useful for AI)
|
|
1206
|
-
* @param {string|boolean} options.fillGaps - Gap handling ('none'/false to disable, or any valid FFmpeg color) - affects gap validation
|
|
1207
1174
|
* @returns {Object} Validation result { valid, errors, warnings }
|
|
1208
1175
|
*
|
|
1209
1176
|
* @example
|
|
@@ -1250,9 +1217,9 @@ class SIMPLEFFMPEG {
|
|
|
1250
1217
|
// Resolve shorthand (duration → end, auto-sequencing)
|
|
1251
1218
|
const { clips: resolved } = resolveClips(clips);
|
|
1252
1219
|
|
|
1253
|
-
// Filter to visual clips (video + image)
|
|
1220
|
+
// Filter to visual clips (video + image + color)
|
|
1254
1221
|
const visual = resolved.filter(
|
|
1255
|
-
(c) => c.type === "video" || c.type === "image"
|
|
1222
|
+
(c) => c.type === "video" || c.type === "image" || c.type === "color"
|
|
1256
1223
|
);
|
|
1257
1224
|
|
|
1258
1225
|
if (visual.length === 0) return 0;
|
|
@@ -1463,7 +1430,7 @@ class SIMPLEFFMPEG {
|
|
|
1463
1430
|
* Get the list of available schema module IDs.
|
|
1464
1431
|
* Use these IDs with getSchema({ include: [...] }) or getSchema({ exclude: [...] }).
|
|
1465
1432
|
*
|
|
1466
|
-
* @returns {string[]} Array of module IDs: ['video', 'audio', 'image', 'text', 'subtitle', 'music']
|
|
1433
|
+
* @returns {string[]} Array of module IDs: ['video', 'audio', 'image', 'color', 'effect', 'text', 'subtitle', 'music']
|
|
1467
1434
|
*/
|
|
1468
1435
|
static getSchemaModules() {
|
|
1469
1436
|
return getSchemaModules();
|
package/types/index.d.mts
CHANGED
|
@@ -51,7 +51,9 @@ declare namespace SIMPLEFFMPEG {
|
|
|
51
51
|
| "music"
|
|
52
52
|
| "backgroundAudio"
|
|
53
53
|
| "image"
|
|
54
|
-
| "subtitle"
|
|
54
|
+
| "subtitle"
|
|
55
|
+
| "color"
|
|
56
|
+
| "effect";
|
|
55
57
|
|
|
56
58
|
interface BaseClip {
|
|
57
59
|
type: ClipType;
|
|
@@ -220,11 +222,82 @@ declare namespace SIMPLEFFMPEG {
|
|
|
220
222
|
opacity?: number;
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
/** Gradient specification for color clips */
|
|
226
|
+
interface GradientSpec {
|
|
227
|
+
type: "linear-gradient" | "radial-gradient";
|
|
228
|
+
/** Array of color strings (at least 2). Evenly distributed across the gradient. */
|
|
229
|
+
colors: string[];
|
|
230
|
+
/** For linear gradients: "vertical" (default), "horizontal", or angle in degrees */
|
|
231
|
+
direction?: "vertical" | "horizontal" | number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Color clip — solid color or gradient for filling gaps, transitions, etc. */
|
|
235
|
+
interface ColorClip {
|
|
236
|
+
type: "color";
|
|
237
|
+
/** Flat color string (e.g. "black", "#FF0000") or gradient specification */
|
|
238
|
+
color: string | GradientSpec;
|
|
239
|
+
/** Start time on timeline in seconds. Omit to auto-sequence after previous visual clip. */
|
|
240
|
+
position?: number;
|
|
241
|
+
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
242
|
+
end?: number;
|
|
243
|
+
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
244
|
+
duration?: number;
|
|
245
|
+
/** Transition effect from the previous visual clip */
|
|
246
|
+
transition?: { type: string; duration: number };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type EffectName = "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust";
|
|
250
|
+
type EffectEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
251
|
+
|
|
252
|
+
interface EffectParamsBase {
|
|
253
|
+
amount?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface VignetteEffectParams extends EffectParamsBase {
|
|
257
|
+
angle?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface FilmGrainEffectParams extends EffectParamsBase {
|
|
261
|
+
temporal?: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface GaussianBlurEffectParams extends EffectParamsBase {
|
|
265
|
+
sigma?: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface ColorAdjustEffectParams extends EffectParamsBase {
|
|
269
|
+
brightness?: number;
|
|
270
|
+
contrast?: number;
|
|
271
|
+
saturation?: number;
|
|
272
|
+
gamma?: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type EffectParams =
|
|
276
|
+
| VignetteEffectParams
|
|
277
|
+
| FilmGrainEffectParams
|
|
278
|
+
| GaussianBlurEffectParams
|
|
279
|
+
| ColorAdjustEffectParams;
|
|
280
|
+
|
|
281
|
+
/** Effect clip — timed overlay adjustment layer over composed video */
|
|
282
|
+
interface EffectClip {
|
|
283
|
+
type: "effect";
|
|
284
|
+
effect: EffectName;
|
|
285
|
+
position: number;
|
|
286
|
+
end?: number;
|
|
287
|
+
duration?: number;
|
|
288
|
+
fadeIn?: number;
|
|
289
|
+
fadeOut?: number;
|
|
290
|
+
easing?: EffectEasing;
|
|
291
|
+
params: EffectParams;
|
|
292
|
+
}
|
|
293
|
+
|
|
223
294
|
type Clip =
|
|
224
295
|
| VideoClip
|
|
225
296
|
| AudioClip
|
|
226
297
|
| BackgroundMusicClip
|
|
227
298
|
| ImageClip
|
|
299
|
+
| ColorClip
|
|
300
|
+
| EffectClip
|
|
228
301
|
| TextClip
|
|
229
302
|
| SubtitleClip;
|
|
230
303
|
|
|
@@ -297,8 +370,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
297
370
|
interface ValidateOptions {
|
|
298
371
|
/** Skip file existence checks (useful for AI generating configs before files exist) */
|
|
299
372
|
skipFileChecks?: boolean;
|
|
300
|
-
/** Gap handling mode - affects timeline gap validation. Any valid FFmpeg color, or "none"/false to disable. */
|
|
301
|
-
fillGaps?: "none" | string | boolean;
|
|
302
373
|
/** Project width - used to validate Ken Burns images are large enough */
|
|
303
374
|
width?: number;
|
|
304
375
|
/** Project height - used to validate Ken Burns images are large enough */
|
|
@@ -318,8 +389,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
318
389
|
height?: number;
|
|
319
390
|
/** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
|
|
320
391
|
validationMode?: "warn" | "strict";
|
|
321
|
-
/** How to handle visual gaps: 'none'/false (disabled), true/'black' (black fill), or any valid FFmpeg color name/hex (default: 'none') */
|
|
322
|
-
fillGaps?: "none" | string | boolean;
|
|
323
392
|
}
|
|
324
393
|
|
|
325
394
|
/** Log entry passed to onLog callback */
|
|
@@ -560,6 +629,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
560
629
|
| "video"
|
|
561
630
|
| "audio"
|
|
562
631
|
| "image"
|
|
632
|
+
| "color"
|
|
633
|
+
| "effect"
|
|
563
634
|
| "text"
|
|
564
635
|
| "subtitle"
|
|
565
636
|
| "music";
|
package/types/index.d.ts
CHANGED
|
@@ -51,7 +51,9 @@ declare namespace SIMPLEFFMPEG {
|
|
|
51
51
|
| "music"
|
|
52
52
|
| "backgroundAudio"
|
|
53
53
|
| "image"
|
|
54
|
-
| "subtitle"
|
|
54
|
+
| "subtitle"
|
|
55
|
+
| "color"
|
|
56
|
+
| "effect";
|
|
55
57
|
|
|
56
58
|
interface BaseClip {
|
|
57
59
|
type: ClipType;
|
|
@@ -220,11 +222,93 @@ declare namespace SIMPLEFFMPEG {
|
|
|
220
222
|
opacity?: number;
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
/** Gradient specification for color clips */
|
|
226
|
+
interface GradientSpec {
|
|
227
|
+
type: "linear-gradient" | "radial-gradient";
|
|
228
|
+
/** Array of color strings (at least 2). Evenly distributed across the gradient. */
|
|
229
|
+
colors: string[];
|
|
230
|
+
/** For linear gradients: "vertical" (default), "horizontal", or angle in degrees */
|
|
231
|
+
direction?: "vertical" | "horizontal" | number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Color clip — solid color or gradient for filling gaps, transitions, etc. */
|
|
235
|
+
interface ColorClip {
|
|
236
|
+
type: "color";
|
|
237
|
+
/** Flat color string (e.g. "black", "#FF0000") or gradient specification */
|
|
238
|
+
color: string | GradientSpec;
|
|
239
|
+
/** Start time on timeline in seconds. Omit to auto-sequence after previous visual clip. */
|
|
240
|
+
position?: number;
|
|
241
|
+
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
242
|
+
end?: number;
|
|
243
|
+
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
244
|
+
duration?: number;
|
|
245
|
+
/** Transition effect from the previous visual clip */
|
|
246
|
+
transition?: { type: string; duration: number };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type EffectName = "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust";
|
|
250
|
+
type EffectEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
251
|
+
|
|
252
|
+
interface EffectParamsBase {
|
|
253
|
+
/** Base blend amount from 0 to 1 (default: 1) */
|
|
254
|
+
amount?: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface VignetteEffectParams extends EffectParamsBase {
|
|
258
|
+
/** Vignette angle in radians (default: PI/5) */
|
|
259
|
+
angle?: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface FilmGrainEffectParams extends EffectParamsBase {
|
|
263
|
+
/** Temporal grain changes every frame (default: true) */
|
|
264
|
+
temporal?: boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface GaussianBlurEffectParams extends EffectParamsBase {
|
|
268
|
+
/** Gaussian blur sigma (default derived from amount) */
|
|
269
|
+
sigma?: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface ColorAdjustEffectParams extends EffectParamsBase {
|
|
273
|
+
brightness?: number;
|
|
274
|
+
contrast?: number;
|
|
275
|
+
saturation?: number;
|
|
276
|
+
gamma?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
type EffectParams =
|
|
280
|
+
| VignetteEffectParams
|
|
281
|
+
| FilmGrainEffectParams
|
|
282
|
+
| GaussianBlurEffectParams
|
|
283
|
+
| ColorAdjustEffectParams;
|
|
284
|
+
|
|
285
|
+
/** Effect clip — timed overlay adjustment layer over composed video */
|
|
286
|
+
interface EffectClip {
|
|
287
|
+
type: "effect";
|
|
288
|
+
effect: EffectName;
|
|
289
|
+
/** Start time on timeline in seconds. Required for effect clips. */
|
|
290
|
+
position: number;
|
|
291
|
+
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
292
|
+
end?: number;
|
|
293
|
+
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
294
|
+
duration?: number;
|
|
295
|
+
/** Ramp-in duration in seconds */
|
|
296
|
+
fadeIn?: number;
|
|
297
|
+
/** Ramp-out duration in seconds */
|
|
298
|
+
fadeOut?: number;
|
|
299
|
+
/** Envelope easing for fade ramps */
|
|
300
|
+
easing?: EffectEasing;
|
|
301
|
+
/** Effect-specific params */
|
|
302
|
+
params: EffectParams;
|
|
303
|
+
}
|
|
304
|
+
|
|
223
305
|
type Clip =
|
|
224
306
|
| VideoClip
|
|
225
307
|
| AudioClip
|
|
226
308
|
| BackgroundMusicClip
|
|
227
309
|
| ImageClip
|
|
310
|
+
| ColorClip
|
|
311
|
+
| EffectClip
|
|
228
312
|
| TextClip
|
|
229
313
|
| SubtitleClip;
|
|
230
314
|
|
|
@@ -297,8 +381,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
297
381
|
interface ValidateOptions {
|
|
298
382
|
/** Skip file existence checks (useful for AI generating configs before files exist) */
|
|
299
383
|
skipFileChecks?: boolean;
|
|
300
|
-
/** Gap handling mode - affects timeline gap validation. Any valid FFmpeg color, or "none"/false to disable. */
|
|
301
|
-
fillGaps?: "none" | string | boolean;
|
|
302
384
|
/** Project width - used to validate Ken Burns images are large enough */
|
|
303
385
|
width?: number;
|
|
304
386
|
/** Project height - used to validate Ken Burns images are large enough */
|
|
@@ -318,8 +400,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
318
400
|
height?: number;
|
|
319
401
|
/** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
|
|
320
402
|
validationMode?: "warn" | "strict";
|
|
321
|
-
/** How to handle visual gaps: 'none'/false (disabled), true/'black' (black fill), or any valid FFmpeg color name/hex (default: 'none') */
|
|
322
|
-
fillGaps?: "none" | string | boolean;
|
|
323
403
|
}
|
|
324
404
|
|
|
325
405
|
/** Log entry passed to onLog callback */
|
|
@@ -657,6 +737,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
657
737
|
| "video"
|
|
658
738
|
| "audio"
|
|
659
739
|
| "image"
|
|
740
|
+
| "color"
|
|
741
|
+
| "effect"
|
|
660
742
|
| "text"
|
|
661
743
|
| "subtitle"
|
|
662
744
|
| "music";
|