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 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",
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",
@@ -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({
@@ -474,12 +474,37 @@ function validateClip(clip, index, options = {}) {
474
474
  }
475
475
  }
476
476
 
477
- // Types that require position/end on timeline
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: number; // Required: start time on timeline (seconds)
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 for effect clips (no auto-sequencing).",
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: number; // Required: start time on timeline (seconds)
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)
@@ -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
- position: number;
163
- end: number;
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 for effect clips. */
341
- position: number;
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
- position: number;
163
- end: number;
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 for effect clips. */
341
- position: number;
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(),