@lee-jisoo/n8n-nodes-mediafx 1.6.1 → 1.6.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.6.0] - 2025-01-25
9
+
10
+ ### Added
11
+ - **Speed operation**: Adjust video playback speed (slow motion or fast forward)
12
+ - Support speed range from 0.25x to 4x
13
+ - Option to adjust audio speed along with video
14
+ - Option to maintain original audio pitch when changing speed
15
+ - Output format selection (MP4, MOV, AVI, MKV)
16
+
17
+ ### Fixed
18
+ - **addSubtitle operation**: Fixed subtitle display issue with slideshow/image-sequence videos
19
+ - Changed from `drawtext` filter chain to `subtitles` filter
20
+ - The `drawtext` filter's `enable='between(t,X,Y)'` condition was unreliable with image-sequence videos
21
+ - Now uses FFmpeg's native SRT parsing which handles timing correctly
22
+ - Added ASS/SSA style support for better subtitle formatting
23
+
24
+ ## [1.5.0] - 2025-01-20
25
+
26
+ ### Added
27
+ - **Overlay Video operation**: Overlay a video on top of another video as a layer
28
+
29
+ ## [1.4.0] - 2025-01-13
30
+
31
+ ### Added
32
+ - **Customizable output field names**: Users can now specify custom binary output field names for all media operations
33
+ - Added "Output Field Name" option to Video, Audio, Image, and Text/Subtitle resources
34
+ - Default field name remains "data" for backward compatibility
35
+ - Allows better organization when chaining multiple MediaFX nodes in workflows
36
+
37
+ ### Fixed
38
+ - **Merge node compatibility**: Improved binary data handling when using n8n's Merge node
39
+ - Automatically detects binary properties in merged items (data1, data2, etc.)
40
+ - Searches both current item and first item for binary data
41
+ - Enhanced error messages with available properties list
42
+ - **Video transition resolution handling**: Fixed concat filter errors with different video resolutions
43
+ - Automatically scales all videos to common resolution before transitions
44
+ - Maintains aspect ratio with padding when needed
45
+ - Ensures compatibility with FFmpeg 4.4 and earlier versions
46
+
47
+ ### Changed
48
+ - Updated MediaFX.node.ts to use dynamic output field names based on user configuration
49
+ - Improved Binary Property field descriptions in UI with Merge node usage guidance
50
+ - Enhanced error messages for better debugging of binary data issues
51
+
52
+ ## [1.3.2] - Previous Release
53
+
54
+ ### Fixed
55
+ - Resolved parameter issues in Video operations
56
+
57
+ ## [1.3.1] - Previous Release
58
+
59
+ ### Fixed
60
+ - Restored missing mixAudio operation for Audio resource
61
+
62
+ ## [1.3.0] - Previous Release
63
+
64
+ ### Added
65
+ - Improved resource and action naming for better UX
@@ -31,68 +31,79 @@ const ffmpeg = require("fluent-ffmpeg");
31
31
  const utils_1 = require("../utils");
32
32
  /**
33
33
  * Escape special characters in file path for FFmpeg subtitles filter.
34
- * FFmpeg subtitles filter requires escaping: \ : [ ] '
35
34
  */
36
35
  function escapeSubtitlePath(filePath) {
37
36
  return filePath
38
- .replace(/\\/g, '\\\\\\\\') // Backslash (Windows paths)
39
- .replace(/:/g, '\\:') // Colon
40
- .replace(/\[/g, '\\[') // Square brackets
37
+ .replace(/\\/g, '\\\\\\\\')
38
+ .replace(/:/g, '\\:')
39
+ .replace(/\[/g, '\\[')
41
40
  .replace(/\]/g, '\\]')
42
- .replace(/'/g, "\\'"); // Single quote
41
+ .replace(/'/g, "\\'");
43
42
  }
44
43
  /**
45
- * Build ASS/SSA style string for subtitles filter force_style option.
46
- * This converts our style options to FFmpeg's force_style format.
44
+ * Convert color to ASS format (&HAABBGGRR)
47
45
  */
48
- function buildForceStyle(fontName, fontSize, fontColor, horizontalAlign, verticalAlign, marginV) {
49
- // Convert color from CSS format to ASS format (AABBGGRR)
50
- // Default white = &H00FFFFFF
51
- let assColor = '&H00FFFFFF';
52
- if (fontColor && fontColor.startsWith('#') && fontColor.length === 7) {
53
- // Convert #RRGGBB to &H00BBGGRR (ASS uses BGR order with alpha prefix)
54
- const r = fontColor.substring(1, 3);
55
- const g = fontColor.substring(3, 5);
56
- const b = fontColor.substring(5, 7);
57
- assColor = `&H00${b}${g}${r}`.toUpperCase();
58
- }
59
- else if (fontColor === 'white') {
60
- assColor = '&H00FFFFFF';
61
- }
62
- else if (fontColor === 'black') {
63
- assColor = '&H00000000';
64
- }
65
- else if (fontColor === 'yellow') {
66
- assColor = '&H0000FFFF';
67
- }
68
- else if (fontColor === 'red') {
69
- assColor = '&H000000FF';
70
- }
71
- // Alignment mapping for ASS (numpad style)
72
- // 1=bottom-left, 2=bottom-center, 3=bottom-right
73
- // 4=middle-left, 5=middle-center, 6=middle-right
74
- // 7=top-left, 8=top-center, 9=top-right
75
- let alignment = 2; // default: bottom-center
76
- if (verticalAlign === 'top') {
77
- alignment = horizontalAlign === 'left' ? 7 : horizontalAlign === 'right' ? 9 : 8;
78
- }
79
- else if (verticalAlign === 'middle') {
80
- alignment = horizontalAlign === 'left' ? 4 : horizontalAlign === 'right' ? 6 : 5;
46
+ function colorToASS(color, opacity = 1) {
47
+ let r = 'FF', g = 'FF', b = 'FF';
48
+ if (color && color.startsWith('#') && color.length === 7) {
49
+ r = color.substring(1, 3);
50
+ g = color.substring(3, 5);
51
+ b = color.substring(5, 7);
81
52
  }
82
53
  else {
83
- // bottom (default)
84
- alignment = horizontalAlign === 'left' ? 1 : horizontalAlign === 'right' ? 3 : 2;
54
+ const namedColors = {
55
+ white: 'FFFFFF',
56
+ black: '000000',
57
+ red: 'FF0000',
58
+ green: '00FF00',
59
+ blue: '0000FF',
60
+ yellow: 'FFFF00',
61
+ cyan: '00FFFF',
62
+ magenta: 'FF00FF',
63
+ orange: 'FFA500',
64
+ };
65
+ const hex = namedColors[(color || 'white').toLowerCase()] || 'FFFFFF';
66
+ r = hex.substring(0, 2);
67
+ g = hex.substring(2, 4);
68
+ b = hex.substring(4, 6);
85
69
  }
70
+ const alpha = Math.round((1 - opacity) * 255).toString(16).padStart(2, '0').toUpperCase();
71
+ return `&H${alpha}${b}${g}${r}`.toUpperCase();
72
+ }
73
+ /**
74
+ * Get ASS alignment number (numpad style)
75
+ */
76
+ function getASSAlignment(horizontalAlign, verticalAlign) {
77
+ var _a, _b;
78
+ const alignMap = {
79
+ top: { left: 7, center: 8, right: 9 },
80
+ middle: { left: 4, center: 5, right: 6 },
81
+ bottom: { left: 1, center: 2, right: 3 },
82
+ };
83
+ return (_b = (_a = alignMap[verticalAlign]) === null || _a === void 0 ? void 0 : _a[horizontalAlign]) !== null && _b !== void 0 ? _b : 2;
84
+ }
85
+ /**
86
+ * Check if file is ASS/SSA format
87
+ */
88
+ function isASSFile(filePath) {
89
+ const ext = path.extname(filePath).toLowerCase();
90
+ return ext === '.ass' || ext === '.ssa';
91
+ }
92
+ /**
93
+ * Build force_style string for subtitles filter
94
+ */
95
+ function buildForceStyle(fontName, fontSize, primaryColor, outlineColor, outlineWidth, backColor, enableBackground, alignment, marginV) {
96
+ const borderStyle = enableBackground ? 4 : 1;
86
97
  const styleParams = [
87
98
  `FontName=${fontName}`,
88
99
  `FontSize=${fontSize}`,
89
- `PrimaryColour=${assColor}`,
90
- `OutlineColour=&H00000000`,
91
- `BackColour=&H80000000`,
100
+ `PrimaryColour=${primaryColor}`,
101
+ `OutlineColour=${outlineColor}`,
102
+ `BackColour=${backColor}`,
92
103
  `Bold=0`,
93
104
  `Italic=0`,
94
- `BorderStyle=4`,
95
- `Outline=1`,
105
+ `BorderStyle=${borderStyle}`,
106
+ `Outline=${outlineWidth}`,
96
107
  `Shadow=0`,
97
108
  `Alignment=${alignment}`,
98
109
  `MarginV=${marginV}`,
@@ -100,9 +111,9 @@ function buildForceStyle(fontName, fontSize, fontColor, horizontalAlign, vertica
100
111
  return styleParams.join(',');
101
112
  }
102
113
  async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
103
- var _a, _b;
114
+ var _a, _b, _c, _d;
104
115
  const outputPath = (0, utils_1.getTempFile)(path.extname(video));
105
- // 1. Get Font from fontKey
116
+ // 1. Get Font
106
117
  const allFonts = (0, utils_1.getAvailableFonts)();
107
118
  const fontKey = style.fontKey || 'noto-sans-kr';
108
119
  const font = allFonts[fontKey];
@@ -112,7 +123,12 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
112
123
  const fontName = font.name || 'Sans';
113
124
  const fontSize = style.size || 48;
114
125
  const fontColor = style.color || 'white';
115
- // 2. Handle position based on position type
126
+ const outlineWidth = (_a = style.outlineWidth) !== null && _a !== void 0 ? _a : 1;
127
+ const outlineColor = style.outlineColor || 'black';
128
+ const enableBackground = (_b = style.enableBackground) !== null && _b !== void 0 ? _b : false;
129
+ const backgroundColor = style.backgroundColor || 'black';
130
+ const backgroundOpacity = (_c = style.backgroundOpacity) !== null && _c !== void 0 ? _c : 0.5;
131
+ // 2. Get alignment
116
132
  const positionType = style.positionType || 'alignment';
117
133
  let horizontalAlign = 'center';
118
134
  let verticalAlign = 'bottom';
@@ -120,15 +136,25 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
120
136
  if (positionType === 'alignment') {
121
137
  horizontalAlign = style.horizontalAlign || 'center';
122
138
  verticalAlign = style.verticalAlign || 'bottom';
123
- marginV = (_b = (_a = style.paddingY) !== null && _a !== void 0 ? _a : style.padding) !== null && _b !== void 0 ? _b : 20;
139
+ marginV = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
140
+ }
141
+ const alignment = getASSAlignment(horizontalAlign, verticalAlign);
142
+ // 3. Convert colors to ASS format
143
+ const primaryColorASS = colorToASS(fontColor, 1);
144
+ const outlineColorASS = colorToASS(outlineColor, 1);
145
+ const backColorASS = colorToASS(backgroundColor, backgroundOpacity);
146
+ // 4. Build FFmpeg command
147
+ const escapedPath = escapeSubtitlePath(subtitleFile);
148
+ let subtitlesFilter;
149
+ if (isASSFile(subtitleFile)) {
150
+ // ASS/SSA file: use directly without force_style
151
+ subtitlesFilter = `subtitles='${escapedPath}'`;
152
+ }
153
+ else {
154
+ // SRT file: apply force_style
155
+ const forceStyle = buildForceStyle(fontName, fontSize, primaryColorASS, outlineColorASS, outlineWidth, backColorASS, enableBackground, alignment, marginV);
156
+ subtitlesFilter = `subtitles='${escapedPath}':force_style='${forceStyle}'`;
124
157
  }
125
- // 3. Build force_style for subtitles filter
126
- const forceStyle = buildForceStyle(fontName, fontSize, fontColor, horizontalAlign, verticalAlign, marginV);
127
- // 4. Escape subtitle file path for FFmpeg
128
- const escapedSubtitlePath = escapeSubtitlePath(subtitleFile);
129
- // 5. Build subtitles filter (much more reliable than drawtext chain)
130
- // Using subtitles filter processes the entire SRT file at once
131
- const subtitlesFilter = `subtitles='${escapedSubtitlePath}':force_style='${forceStyle}'`;
132
158
  const command = ffmpeg(video)
133
159
  .videoFilters([subtitlesFilter])
134
160
  .audioCodec('copy')
@@ -138,7 +164,6 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
138
164
  return outputPath;
139
165
  }
140
166
  catch (error) {
141
- // Clean up output file if creation failed
142
167
  await fs.remove(outputPath).catch(() => { });
143
168
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
144
169
  }
@@ -178,6 +178,61 @@ exports.subtitleProperties = [
178
178
  },
179
179
  description: 'Width of the text border/outline',
180
180
  },
181
+ {
182
+ displayName: 'Outline Color',
183
+ name: 'outlineColor',
184
+ type: 'string',
185
+ default: 'black',
186
+ displayOptions: {
187
+ show: {
188
+ resource: ['subtitle'],
189
+ },
190
+ },
191
+ description: 'Color of the text outline (e.g., black, #000000)',
192
+ },
193
+ {
194
+ displayName: 'Enable Background Box',
195
+ name: 'enableBackground',
196
+ type: 'boolean',
197
+ default: false,
198
+ displayOptions: {
199
+ show: {
200
+ resource: ['subtitle'],
201
+ },
202
+ },
203
+ description: 'Whether to show a background box behind the text',
204
+ },
205
+ {
206
+ displayName: 'Background Color',
207
+ name: 'backgroundColor',
208
+ type: 'string',
209
+ default: 'black',
210
+ displayOptions: {
211
+ show: {
212
+ resource: ['subtitle'],
213
+ enableBackground: [true],
214
+ },
215
+ },
216
+ description: 'Color of the background box (e.g., black, #000000)',
217
+ },
218
+ {
219
+ displayName: 'Background Opacity',
220
+ name: 'backgroundOpacity',
221
+ type: 'number',
222
+ typeOptions: {
223
+ minValue: 0,
224
+ maxValue: 1,
225
+ numberStepSize: 0.1,
226
+ },
227
+ default: 0.5,
228
+ displayOptions: {
229
+ show: {
230
+ resource: ['subtitle'],
231
+ enableBackground: [true],
232
+ },
233
+ },
234
+ description: 'Opacity of the background box (0 = transparent, 1 = solid)',
235
+ },
181
236
  // ===================
182
237
  // POSITION OPTIONS
183
238
  // ===================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "N8N custom nodes for video editing and media processing (Enhanced fork with Speed control and Subtitle fixes)",
5
5
  "license": "MIT",
6
6
  "author": {