@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, '\\\\\\\\')
|
|
39
|
-
.replace(/:/g, '\\:')
|
|
40
|
-
.replace(/\[/g, '\\[')
|
|
37
|
+
.replace(/\\/g, '\\\\\\\\')
|
|
38
|
+
.replace(/:/g, '\\:')
|
|
39
|
+
.replace(/\[/g, '\\[')
|
|
41
40
|
.replace(/\]/g, '\\]')
|
|
42
|
-
.replace(/'/g, "\\'");
|
|
41
|
+
.replace(/'/g, "\\'");
|
|
43
42
|
}
|
|
44
43
|
/**
|
|
45
|
-
*
|
|
46
|
-
* This converts our style options to FFmpeg's force_style format.
|
|
44
|
+
* Convert color to ASS format (&HAABBGGRR)
|
|
47
45
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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=${
|
|
90
|
-
`OutlineColour
|
|
91
|
-
`BackColour
|
|
100
|
+
`PrimaryColour=${primaryColor}`,
|
|
101
|
+
`OutlineColour=${outlineColor}`,
|
|
102
|
+
`BackColour=${backColor}`,
|
|
92
103
|
`Bold=0`,
|
|
93
104
|
`Italic=0`,
|
|
94
|
-
`BorderStyle
|
|
95
|
-
`Outline
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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