simple-ffmpegjs 0.5.3 → 0.5.4
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 +1 -0
- package/package.json +1 -1
- package/src/core/resolve.js +33 -0
- package/src/core/validation.js +27 -2
- package/src/schema/modules/effect.js +5 -4
- package/src/schema/modules/text.js +4 -3
- package/src/simpleffmpeg.js +14 -0
- package/types/index.d.mts +33 -39
- package/types/index.d.ts +13 -39
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
9
9
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js ≥20"></a>
|
|
10
10
|
<a href="https://codecov.io/gh/Fats403/simple-ffmpegjs"><img src="https://codecov.io/gh/Fats403/simple-ffmpegjs/branch/main/graph/badge.svg" alt="Coverage"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg" alt="Zero Dependencies">
|
|
11
12
|
</p>
|
|
12
13
|
|
|
13
14
|
<p align="center">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-ffmpegjs",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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/resolve.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* `position`, it is placed immediately after the previous clip on the
|
|
12
12
|
* same track (visual or audio). The first clip defaults to position 0.
|
|
13
13
|
*
|
|
14
|
+
* 3. **fullDuration**: If an effect or text clip has `fullDuration: true`,
|
|
15
|
+
* its position defaults to 0 and end is resolved later in _prepareExport()
|
|
16
|
+
* once the visual timeline duration is known.
|
|
17
|
+
*
|
|
14
18
|
* Clips are shallow-cloned — the caller's original objects are not mutated.
|
|
15
19
|
*/
|
|
16
20
|
|
|
@@ -48,6 +52,35 @@ function resolveClips(clips) {
|
|
|
48
52
|
const c = { ...clip };
|
|
49
53
|
const path = `clips[${index}]`;
|
|
50
54
|
|
|
55
|
+
// ── fullDuration shorthand ─────────────────────────────────────────
|
|
56
|
+
if (c.fullDuration === true) {
|
|
57
|
+
if (c.end != null) {
|
|
58
|
+
errors.push({
|
|
59
|
+
code: "INVALID_VALUE",
|
|
60
|
+
path: `${path}`,
|
|
61
|
+
message:
|
|
62
|
+
"Cannot specify both 'fullDuration' and 'end'. fullDuration spans the entire visual timeline automatically.",
|
|
63
|
+
received: { fullDuration: true, end: c.end },
|
|
64
|
+
});
|
|
65
|
+
return c;
|
|
66
|
+
}
|
|
67
|
+
if (c.duration != null) {
|
|
68
|
+
errors.push({
|
|
69
|
+
code: "INVALID_VALUE",
|
|
70
|
+
path: `${path}`,
|
|
71
|
+
message:
|
|
72
|
+
"Cannot specify both 'fullDuration' and 'duration'. fullDuration spans the entire visual timeline automatically.",
|
|
73
|
+
received: { fullDuration: true, duration: c.duration },
|
|
74
|
+
});
|
|
75
|
+
return c;
|
|
76
|
+
}
|
|
77
|
+
// Default position to 0; end is resolved in _prepareExport()
|
|
78
|
+
if (c.position == null) {
|
|
79
|
+
c.position = 0;
|
|
80
|
+
}
|
|
81
|
+
return c;
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
// ── Conflict check: duration + end ──────────────────────────────────
|
|
52
85
|
if (c.duration != null && c.end != null) {
|
|
53
86
|
errors.push({
|
package/src/core/validation.js
CHANGED
|
@@ -474,12 +474,37 @@ function validateClip(clip, index, options = {}) {
|
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
//
|
|
477
|
+
// fullDuration validation
|
|
478
|
+
const fullDurationTypes = ["effect", "text"];
|
|
479
|
+
if (clip.fullDuration != null) {
|
|
480
|
+
if (clip.fullDuration !== true) {
|
|
481
|
+
errors.push(
|
|
482
|
+
createIssue(
|
|
483
|
+
ValidationCodes.INVALID_VALUE,
|
|
484
|
+
`${path}.fullDuration`,
|
|
485
|
+
"fullDuration must be true when specified",
|
|
486
|
+
clip.fullDuration,
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
} else if (!fullDurationTypes.includes(clip.type)) {
|
|
490
|
+
errors.push(
|
|
491
|
+
createIssue(
|
|
492
|
+
ValidationCodes.INVALID_VALUE,
|
|
493
|
+
`${path}.fullDuration`,
|
|
494
|
+
`fullDuration is only supported on ${fullDurationTypes.join(", ")} clips`,
|
|
495
|
+
clip.type,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Types that require position/end on timeline (unless fullDuration is set)
|
|
502
|
+
const hasFullDuration = clip.fullDuration === true && fullDurationTypes.includes(clip.type);
|
|
478
503
|
const requiresTimeline = ["video", "audio", "text", "image", "color", "effect"].includes(
|
|
479
504
|
clip.type,
|
|
480
505
|
);
|
|
481
506
|
|
|
482
|
-
if (requiresTimeline) {
|
|
507
|
+
if (requiresTimeline && !hasFullDuration) {
|
|
483
508
|
if (typeof clip.position !== "number") {
|
|
484
509
|
errors.push(
|
|
485
510
|
createIssue(
|
|
@@ -8,9 +8,10 @@ module.exports = {
|
|
|
8
8
|
effect: "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust"
|
|
9
9
|
| "sepia" | "blackAndWhite" | "sharpen" | "chromaticAberration"
|
|
10
10
|
| "letterbox"; // Required: effect kind
|
|
11
|
-
position
|
|
12
|
-
end?: number; // End time on timeline (seconds). Use end OR duration, not both.
|
|
13
|
-
duration?: number; // Duration in seconds (alternative to end). end = position + duration.
|
|
11
|
+
position?: number; // Start time on timeline (seconds). Required unless fullDuration is true.
|
|
12
|
+
end?: number; // End time on timeline (seconds). Use end OR duration, not both. Mutually exclusive with fullDuration.
|
|
13
|
+
duration?: number; // Duration in seconds (alternative to end). end = position + duration. Mutually exclusive with fullDuration.
|
|
14
|
+
fullDuration?: boolean; // When true, spans the full visual timeline. Mutually exclusive with end and duration.
|
|
14
15
|
fadeIn?: number; // Optional: seconds to ramp in from 0 to full intensity
|
|
15
16
|
fadeOut?: number; // Optional: seconds to ramp out from full intensity to 0
|
|
16
17
|
params: EffectParams; // Required: effect-specific parameters
|
|
@@ -116,7 +117,7 @@ module.exports = {
|
|
|
116
117
|
"Effect clips are adjustment layers: they modify underlying video during their active window.",
|
|
117
118
|
"Effects do not satisfy visual timeline continuity checks and do not fill gaps.",
|
|
118
119
|
"Use duration instead of end to specify length: end = position + duration. Cannot use both.",
|
|
119
|
-
"position is required
|
|
120
|
+
"position is required unless fullDuration: true is set, which spans the entire visual timeline.",
|
|
120
121
|
"fadeIn/fadeOut are optional linear envelope controls that avoid abrupt on/off changes.",
|
|
121
122
|
"params.amount is a normalized blend amount from 0 to 1 (default: 1).",
|
|
122
123
|
"filmGrain: use params.strength (0-1) for noise intensity, params.amount for blend alpha.",
|
|
@@ -5,9 +5,10 @@ module.exports = {
|
|
|
5
5
|
"Render text overlays on the video with multiple display modes, positioning, styling, and animations.",
|
|
6
6
|
schema: `{
|
|
7
7
|
type: "text"; // Required: clip type identifier
|
|
8
|
-
position
|
|
9
|
-
end?: number; // End time on timeline (seconds). Use end OR duration, not both.
|
|
10
|
-
duration?: number; // Duration in seconds (alternative to end). end = position + duration.
|
|
8
|
+
position?: number; // Start time on timeline (seconds). Required unless fullDuration is true.
|
|
9
|
+
end?: number; // End time on timeline (seconds). Use end OR duration, not both. Mutually exclusive with fullDuration.
|
|
10
|
+
duration?: number; // Duration in seconds (alternative to end). end = position + duration. Mutually exclusive with fullDuration.
|
|
11
|
+
fullDuration?: boolean; // When true, spans the full visual timeline. Mutually exclusive with end and duration.
|
|
11
12
|
|
|
12
13
|
// Content
|
|
13
14
|
text?: string; // Text content (required for "static" mode)
|
package/src/simpleffmpeg.js
CHANGED
|
@@ -561,6 +561,20 @@ class SIMPLEFFMPEG {
|
|
|
561
561
|
}
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
// Expand fullDuration clips now that finalVisualEnd is known
|
|
565
|
+
for (const clip of this.effectClips) {
|
|
566
|
+
if (clip.fullDuration === true) {
|
|
567
|
+
clip.position = clip.position ?? 0;
|
|
568
|
+
clip.end = finalVisualEnd;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const clip of this.textClips) {
|
|
572
|
+
if (clip.fullDuration === true) {
|
|
573
|
+
clip.position = clip.position ?? 0;
|
|
574
|
+
clip.end = finalVisualEnd;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
564
578
|
// Overlay effects (adjustment layer clips) on the composed video output.
|
|
565
579
|
if (this.effectClips.length > 0 && hasVideo && finalVideoLabel) {
|
|
566
580
|
const effectRes = buildEffectFilters(this.effectClips, finalVideoLabel);
|
package/types/index.d.mts
CHANGED
|
@@ -4,9 +4,7 @@ declare namespace SIMPLEFFMPEG {
|
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
6
6
|
/** Base error class for all simple-ffmpeg errors */
|
|
7
|
-
class SimpleffmpegError extends Error {
|
|
8
|
-
name: "SimpleffmpegError";
|
|
9
|
-
}
|
|
7
|
+
class SimpleffmpegError extends Error {}
|
|
10
8
|
|
|
11
9
|
/** Thrown when clip validation fails */
|
|
12
10
|
class ValidationError extends SimpleffmpegError {
|
|
@@ -159,8 +157,12 @@ declare namespace SIMPLEFFMPEG {
|
|
|
159
157
|
interface TextClip {
|
|
160
158
|
type: "text";
|
|
161
159
|
text?: string;
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
/** Start time on timeline in seconds. Required unless fullDuration is true. */
|
|
161
|
+
position?: number;
|
|
162
|
+
/** End time on timeline in seconds. Mutually exclusive with fullDuration. */
|
|
163
|
+
end?: number;
|
|
164
|
+
/** When true, the clip spans the full visual timeline (position 0 to end of last video/image/color clip). Mutually exclusive with end and duration. */
|
|
165
|
+
fullDuration?: boolean;
|
|
164
166
|
mode?: TextMode;
|
|
165
167
|
words?: TextWordWindow[];
|
|
166
168
|
wordTimestamps?: number[];
|
|
@@ -337,12 +339,14 @@ declare namespace SIMPLEFFMPEG {
|
|
|
337
339
|
interface EffectClip {
|
|
338
340
|
type: "effect";
|
|
339
341
|
effect: EffectName;
|
|
340
|
-
/** Start time on timeline in seconds. Required
|
|
341
|
-
position
|
|
342
|
-
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
342
|
+
/** Start time on timeline in seconds. Required unless fullDuration is true. */
|
|
343
|
+
position?: number;
|
|
344
|
+
/** End time on timeline in seconds. Mutually exclusive with duration and fullDuration. */
|
|
343
345
|
end?: number;
|
|
344
|
-
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
346
|
+
/** Duration in seconds (alternative to end). end = position + duration. Mutually exclusive with fullDuration. */
|
|
345
347
|
duration?: number;
|
|
348
|
+
/** When true, the clip spans the full visual timeline (position 0 to end of last video/image/color clip). Mutually exclusive with end and duration. */
|
|
349
|
+
fullDuration?: boolean;
|
|
346
350
|
/** Ramp-in duration in seconds */
|
|
347
351
|
fadeIn?: number;
|
|
348
352
|
/** Ramp-out duration in seconds */
|
|
@@ -979,6 +983,26 @@ declare class SIMPLEFFMPEG {
|
|
|
979
983
|
*/
|
|
980
984
|
static getDuration(clips: SIMPLEFFMPEG.Clip[]): number;
|
|
981
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Calculate the total transition overlap for a clips configuration.
|
|
988
|
+
* Returns the total seconds consumed by xfade transition overlaps
|
|
989
|
+
* among visual clips (video, image, color).
|
|
990
|
+
*
|
|
991
|
+
* Pure function — same clips always produce the same result. No file I/O.
|
|
992
|
+
*
|
|
993
|
+
* @param clips - Array of clip objects
|
|
994
|
+
* @returns Total transition overlap in seconds
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* const overlap = SIMPLEFFMPEG.getTransitionOverlap([
|
|
998
|
+
* { type: "video", url: "./a.mp4", duration: 5 },
|
|
999
|
+
* { type: "video", url: "./b.mp4", duration: 10,
|
|
1000
|
+
* transition: { type: "fade", duration: 0.5 } },
|
|
1001
|
+
* ]);
|
|
1002
|
+
* // overlap === 0.5
|
|
1003
|
+
*/
|
|
1004
|
+
static getTransitionOverlap(clips: SIMPLEFFMPEG.Clip[]): number;
|
|
1005
|
+
|
|
982
1006
|
/**
|
|
983
1007
|
* Probe a media file and return comprehensive metadata.
|
|
984
1008
|
*
|
|
@@ -1066,36 +1090,6 @@ declare class SIMPLEFFMPEG {
|
|
|
1066
1090
|
*/
|
|
1067
1091
|
static formatValidationResult(result: SIMPLEFFMPEG.ValidationResult): string;
|
|
1068
1092
|
|
|
1069
|
-
/**
|
|
1070
|
-
* Validation error codes for programmatic handling
|
|
1071
|
-
*/
|
|
1072
|
-
static readonly ValidationCodes: typeof SIMPLEFFMPEG.ValidationCodes;
|
|
1073
|
-
|
|
1074
|
-
/**
|
|
1075
|
-
* Base error class for all simple-ffmpeg errors
|
|
1076
|
-
*/
|
|
1077
|
-
static readonly SimpleffmpegError: typeof SIMPLEFFMPEG.SimpleffmpegError;
|
|
1078
|
-
|
|
1079
|
-
/**
|
|
1080
|
-
* Thrown when clip validation fails
|
|
1081
|
-
*/
|
|
1082
|
-
static readonly ValidationError: typeof SIMPLEFFMPEG.ValidationError;
|
|
1083
|
-
|
|
1084
|
-
/**
|
|
1085
|
-
* Thrown when FFmpeg command execution fails
|
|
1086
|
-
*/
|
|
1087
|
-
static readonly FFmpegError: typeof SIMPLEFFMPEG.FFmpegError;
|
|
1088
|
-
|
|
1089
|
-
/**
|
|
1090
|
-
* Thrown when a media file cannot be found or accessed
|
|
1091
|
-
*/
|
|
1092
|
-
static readonly MediaNotFoundError: typeof SIMPLEFFMPEG.MediaNotFoundError;
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Thrown when export is cancelled via AbortSignal
|
|
1096
|
-
*/
|
|
1097
|
-
static readonly ExportCancelledError: typeof SIMPLEFFMPEG.ExportCancelledError;
|
|
1098
|
-
|
|
1099
1093
|
/**
|
|
1100
1094
|
* Get the clip schema as formatted prompt-ready text.
|
|
1101
1095
|
* Returns a structured description of all clip types accepted by load(),
|
package/types/index.d.ts
CHANGED
|
@@ -4,9 +4,7 @@ declare namespace SIMPLEFFMPEG {
|
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
6
6
|
/** Base error class for all simple-ffmpeg errors */
|
|
7
|
-
class SimpleffmpegError extends Error {
|
|
8
|
-
name: "SimpleffmpegError";
|
|
9
|
-
}
|
|
7
|
+
class SimpleffmpegError extends Error {}
|
|
10
8
|
|
|
11
9
|
/** Thrown when clip validation fails */
|
|
12
10
|
class ValidationError extends SimpleffmpegError {
|
|
@@ -159,8 +157,12 @@ declare namespace SIMPLEFFMPEG {
|
|
|
159
157
|
interface TextClip {
|
|
160
158
|
type: "text";
|
|
161
159
|
text?: string;
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
/** Start time on timeline in seconds. Required unless fullDuration is true. */
|
|
161
|
+
position?: number;
|
|
162
|
+
/** End time on timeline in seconds. Mutually exclusive with fullDuration. */
|
|
163
|
+
end?: number;
|
|
164
|
+
/** When true, the clip spans the full visual timeline (position 0 to end of last video/image/color clip). Mutually exclusive with end and duration. */
|
|
165
|
+
fullDuration?: boolean;
|
|
164
166
|
mode?: TextMode;
|
|
165
167
|
words?: TextWordWindow[];
|
|
166
168
|
wordTimestamps?: number[];
|
|
@@ -337,12 +339,14 @@ declare namespace SIMPLEFFMPEG {
|
|
|
337
339
|
interface EffectClip {
|
|
338
340
|
type: "effect";
|
|
339
341
|
effect: EffectName;
|
|
340
|
-
/** Start time on timeline in seconds. Required
|
|
341
|
-
position
|
|
342
|
-
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
342
|
+
/** Start time on timeline in seconds. Required unless fullDuration is true. */
|
|
343
|
+
position?: number;
|
|
344
|
+
/** End time on timeline in seconds. Mutually exclusive with duration and fullDuration. */
|
|
343
345
|
end?: number;
|
|
344
|
-
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
346
|
+
/** Duration in seconds (alternative to end). end = position + duration. Mutually exclusive with fullDuration. */
|
|
345
347
|
duration?: number;
|
|
348
|
+
/** When true, the clip spans the full visual timeline (position 0 to end of last video/image/color clip). Mutually exclusive with end and duration. */
|
|
349
|
+
fullDuration?: boolean;
|
|
346
350
|
/** Ramp-in duration in seconds */
|
|
347
351
|
fadeIn?: number;
|
|
348
352
|
/** Ramp-out duration in seconds */
|
|
@@ -1086,36 +1090,6 @@ declare class SIMPLEFFMPEG {
|
|
|
1086
1090
|
*/
|
|
1087
1091
|
static formatValidationResult(result: SIMPLEFFMPEG.ValidationResult): string;
|
|
1088
1092
|
|
|
1089
|
-
/**
|
|
1090
|
-
* Validation error codes for programmatic handling
|
|
1091
|
-
*/
|
|
1092
|
-
static readonly ValidationCodes: typeof SIMPLEFFMPEG.ValidationCodes;
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Base error class for all simple-ffmpeg errors
|
|
1096
|
-
*/
|
|
1097
|
-
static readonly SimpleffmpegError: typeof SIMPLEFFMPEG.SimpleffmpegError;
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Thrown when clip validation fails
|
|
1101
|
-
*/
|
|
1102
|
-
static readonly ValidationError: typeof SIMPLEFFMPEG.ValidationError;
|
|
1103
|
-
|
|
1104
|
-
/**
|
|
1105
|
-
* Thrown when FFmpeg command execution fails
|
|
1106
|
-
*/
|
|
1107
|
-
static readonly FFmpegError: typeof SIMPLEFFMPEG.FFmpegError;
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Thrown when a media file cannot be found or accessed
|
|
1111
|
-
*/
|
|
1112
|
-
static readonly MediaNotFoundError: typeof SIMPLEFFMPEG.MediaNotFoundError;
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* Thrown when export is cancelled via AbortSignal
|
|
1116
|
-
*/
|
|
1117
|
-
static readonly ExportCancelledError: typeof SIMPLEFFMPEG.ExportCancelledError;
|
|
1118
|
-
|
|
1119
1093
|
/**
|
|
1120
1094
|
* Get the clip schema as formatted prompt-ready text.
|
|
1121
1095
|
* Returns a structured description of all clip types accepted by load(),
|