@lee-jisoo/n8n-nodes-mediafx 1.6.0 → 1.6.1
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/dist/fonts/DejaVuSans.ttf +0 -0
- package/dist/fonts/Inter-Regular.ttf +0 -0
- package/dist/fonts/NanumGothic-Regular.ttf +0 -0
- package/dist/fonts/NotoSansKR-Regular.ttf +0 -0
- package/dist/fonts/Pretendard-Regular.otf +0 -0
- package/dist/fonts/Roboto-Regular.ttf +0 -0
- package/dist/nodes/MediaFX/MediaFX.node.d.ts +12 -0
- package/dist/nodes/MediaFX/MediaFX.node.js +559 -0
- package/dist/nodes/MediaFX/mediafx.png +0 -0
- package/dist/nodes/MediaFX/operations/addSubtitle.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/addSubtitle.js +146 -0
- package/dist/nodes/MediaFX/operations/addText.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/addText.js +108 -0
- package/dist/nodes/MediaFX/operations/extractAudio.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/extractAudio.js +57 -0
- package/dist/nodes/MediaFX/operations/imageToVideo.d.ts +5 -0
- package/dist/nodes/MediaFX/operations/imageToVideo.js +65 -0
- package/dist/nodes/MediaFX/operations/index.d.ts +13 -0
- package/dist/nodes/MediaFX/operations/index.js +29 -0
- package/dist/nodes/MediaFX/operations/merge.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/merge.js +121 -0
- package/dist/nodes/MediaFX/operations/mixAudio.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/mixAudio.js +141 -0
- package/dist/nodes/MediaFX/operations/multiVideoTransition.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/multiVideoTransition.js +245 -0
- package/dist/nodes/MediaFX/operations/overlayVideo.d.ts +16 -0
- package/dist/nodes/MediaFX/operations/overlayVideo.js +240 -0
- package/dist/nodes/MediaFX/operations/separateAudio.d.ts +17 -0
- package/dist/nodes/MediaFX/operations/separateAudio.js +78 -0
- package/dist/nodes/MediaFX/operations/singleVideoFade.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/singleVideoFade.js +60 -0
- package/dist/nodes/MediaFX/operations/speed.d.ts +12 -0
- package/dist/nodes/MediaFX/operations/speed.js +110 -0
- package/dist/nodes/MediaFX/operations/stampImage.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/stampImage.js +146 -0
- package/dist/nodes/MediaFX/operations/trim.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/trim.js +49 -0
- package/dist/nodes/MediaFX/properties/audio.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/audio.properties.js +394 -0
- package/dist/nodes/MediaFX/properties/font.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/font.properties.js +186 -0
- package/dist/nodes/MediaFX/properties/image.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/image.properties.js +333 -0
- package/dist/nodes/MediaFX/properties/resources.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/resources.properties.js +34 -0
- package/dist/nodes/MediaFX/properties/subtitle.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/subtitle.properties.js +306 -0
- package/dist/nodes/MediaFX/properties/video.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/video.properties.js +1135 -0
- package/dist/nodes/MediaFX/utils/ffmpegVersion.d.ts +14 -0
- package/dist/nodes/MediaFX/utils/ffmpegVersion.js +97 -0
- package/dist/nodes/MediaFX/utils.d.ts +43 -0
- package/dist/nodes/MediaFX/utils.js +410 -0
- package/package.json +1 -1
- package/CHANGELOG.md +0 -65
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, INodePropertyOptions, ILoadOptionsFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare class MediaFX implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getFonts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
getTransitionEffects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
|
+
getUserFonts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.MediaFX = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const fs = __importStar(require("fs-extra"));
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
// import ffmpeg = require('fluent-ffmpeg');
|
|
31
|
+
const utils_1 = require("./utils");
|
|
32
|
+
const operations_1 = require("./operations");
|
|
33
|
+
const audio_properties_1 = require("./properties/audio.properties");
|
|
34
|
+
const font_properties_1 = require("./properties/font.properties");
|
|
35
|
+
const image_properties_1 = require("./properties/image.properties");
|
|
36
|
+
const resources_properties_1 = require("./properties/resources.properties");
|
|
37
|
+
const subtitle_properties_1 = require("./properties/subtitle.properties");
|
|
38
|
+
const video_properties_1 = require("./properties/video.properties");
|
|
39
|
+
// --- OPERATION EXECUTORS ---
|
|
40
|
+
// ALL OPERATION EXECUTORS MOVED TO ./operations/*
|
|
41
|
+
class MediaFX {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.description = {
|
|
44
|
+
displayName: 'MediaFX',
|
|
45
|
+
name: 'mediaFX',
|
|
46
|
+
icon: 'file:mediafx.png',
|
|
47
|
+
group: ['transform'],
|
|
48
|
+
version: 1,
|
|
49
|
+
description: 'Process videos, audio, and media files with FFmpeg',
|
|
50
|
+
defaults: {
|
|
51
|
+
name: 'MediaFX',
|
|
52
|
+
},
|
|
53
|
+
inputs: ['main'],
|
|
54
|
+
outputs: ['main'],
|
|
55
|
+
// No credentials needed for local processing
|
|
56
|
+
properties: [
|
|
57
|
+
...resources_properties_1.resourceSelection,
|
|
58
|
+
...video_properties_1.videoProperties,
|
|
59
|
+
...audio_properties_1.audioProperties,
|
|
60
|
+
...subtitle_properties_1.subtitleProperties,
|
|
61
|
+
...image_properties_1.imageProperties,
|
|
62
|
+
...font_properties_1.fontProperties,
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
this.methods = {
|
|
66
|
+
loadOptions: {
|
|
67
|
+
// Load available fonts from API
|
|
68
|
+
async getFonts() {
|
|
69
|
+
try {
|
|
70
|
+
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
71
|
+
return Object.entries(allFonts).map(([key, font]) => ({
|
|
72
|
+
name: `${font.name || key} (${font.type})`,
|
|
73
|
+
value: key,
|
|
74
|
+
description: font.description || '',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
// Load available transition effects from a static list
|
|
82
|
+
async getTransitionEffects() {
|
|
83
|
+
// Mark effects that require FFmpeg 4.3+
|
|
84
|
+
const effects = [
|
|
85
|
+
{ name: 'Fade', value: 'fade', description: 'Works with all FFmpeg versions' },
|
|
86
|
+
{ name: 'Fade Black', value: 'fadeblack', description: 'Works with all FFmpeg versions' },
|
|
87
|
+
{ name: 'Fade White', value: 'fadewhite', description: 'Works with all FFmpeg versions' },
|
|
88
|
+
{ name: 'Wipe Left', value: 'wipeleft', description: 'Requires FFmpeg 4.3+' },
|
|
89
|
+
{ name: 'Wipe Right', value: 'wiperight', description: 'Requires FFmpeg 4.3+' },
|
|
90
|
+
{ name: 'Wipe Up', value: 'wipeup', description: 'Requires FFmpeg 4.3+' },
|
|
91
|
+
{ name: 'Wipe Down', value: 'wipedown', description: 'Requires FFmpeg 4.3+' },
|
|
92
|
+
{ name: 'Slide Left', value: 'slideleft', description: 'Requires FFmpeg 4.3+' },
|
|
93
|
+
{ name: 'Slide Right', value: 'slideright', description: 'Requires FFmpeg 4.3+' },
|
|
94
|
+
{ name: 'Slide Up', value: 'slideup', description: 'Requires FFmpeg 4.3+' },
|
|
95
|
+
{ name: 'Slide Down', value: 'slidedown', description: 'Requires FFmpeg 4.3+' },
|
|
96
|
+
{ name: 'Circle Crop', value: 'circlecrop', description: 'Requires FFmpeg 4.3+' },
|
|
97
|
+
{ name: 'Rect Crop', value: 'rectcrop', description: 'Requires FFmpeg 4.3+' },
|
|
98
|
+
{ name: 'Distance', value: 'distance', description: 'Requires FFmpeg 4.3+' },
|
|
99
|
+
{ name: 'Fade Grayscale', value: 'fadegrays', description: 'Requires FFmpeg 4.3+' },
|
|
100
|
+
{ name: 'Radial', value: 'radial', description: 'Requires FFmpeg 4.3+' },
|
|
101
|
+
{ name: 'Circle Open', value: 'circleopen', description: 'Requires FFmpeg 4.3+' },
|
|
102
|
+
{ name: 'Circle Close', value: 'circleclose', description: 'Requires FFmpeg 4.3+' },
|
|
103
|
+
{ name: 'Pixelize', value: 'pixelize', description: 'Requires FFmpeg 4.3+' },
|
|
104
|
+
{ name: 'Dissolve', value: 'dissolve', description: 'Requires FFmpeg 4.3+' },
|
|
105
|
+
{ name: 'Checkerboard', value: 'diagtl', description: 'Requires FFmpeg 4.3+' },
|
|
106
|
+
{ name: 'Box-in', value: 'boxin', description: 'Requires FFmpeg 4.3+' },
|
|
107
|
+
{ name: 'Iris', value: 'iris', description: 'Requires FFmpeg 4.3+' },
|
|
108
|
+
];
|
|
109
|
+
return effects;
|
|
110
|
+
},
|
|
111
|
+
async getUserFonts() {
|
|
112
|
+
try {
|
|
113
|
+
const userFonts = (0, utils_1.getUserFonts)();
|
|
114
|
+
return Object.entries(userFonts).map(([key, font]) => ({
|
|
115
|
+
name: `${font.name || key} (user)`,
|
|
116
|
+
value: key,
|
|
117
|
+
description: font.description || 'User uploaded font',
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// This is optional, so return empty on error
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async execute() {
|
|
129
|
+
const items = this.getInputData();
|
|
130
|
+
const returnData = [];
|
|
131
|
+
// Periodically clean up old temporary files (every 10th execution)
|
|
132
|
+
if (Math.random() < 0.1) {
|
|
133
|
+
(0, utils_1.cleanupOldTempFiles)(24).catch(() => {
|
|
134
|
+
// Ignore cleanup errors to avoid disrupting main operation
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
for (let i = 0; i < items.length; i++) {
|
|
138
|
+
let cleanup = async () => { }; // Initialize cleanup function for each iteration
|
|
139
|
+
let resource = ''; // Initialize resource variable for error handling
|
|
140
|
+
try {
|
|
141
|
+
resource = this.getNodeParameter('resource', i);
|
|
142
|
+
const operation = this.getNodeParameter('operation', i);
|
|
143
|
+
let resultData = null;
|
|
144
|
+
let outputPath = null;
|
|
145
|
+
// ===================================
|
|
146
|
+
// FONT RESOURCE OPERATIONS
|
|
147
|
+
// ===================================
|
|
148
|
+
if (resource === 'font') {
|
|
149
|
+
switch (operation) {
|
|
150
|
+
case 'list': {
|
|
151
|
+
const filterOptions = this.getNodeParameter('filterOptions', i, {});
|
|
152
|
+
const fontTypeFilter = filterOptions.fontType || 'all';
|
|
153
|
+
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
154
|
+
if (fontTypeFilter === 'all') {
|
|
155
|
+
resultData = allFonts;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
resultData = Object.fromEntries(Object.entries(allFonts).filter(([, font]) => font.type === fontTypeFilter));
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'upload': {
|
|
163
|
+
const fontSource = this.getNodeParameter('fontSource', i);
|
|
164
|
+
const fontKey = this.getNodeParameter('fontKeyUpload', i);
|
|
165
|
+
const fontName = this.getNodeParameter('fontName', i, '');
|
|
166
|
+
const description = this.getNodeParameter('description', i, '');
|
|
167
|
+
let buffer;
|
|
168
|
+
let originalname;
|
|
169
|
+
if (fontSource === 'binary') {
|
|
170
|
+
const binaryProperty = this.getNodeParameter('binaryProperty', i);
|
|
171
|
+
const binaryData = items[i].binary;
|
|
172
|
+
if (!binaryData || !binaryData[binaryProperty]) {
|
|
173
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No binary data found in property '${binaryProperty}'`, { itemIndex: i });
|
|
174
|
+
}
|
|
175
|
+
buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
|
|
176
|
+
originalname = binaryData[binaryProperty].fileName || 'font.ttf';
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// filepath
|
|
180
|
+
const filePath = this.getNodeParameter('filePath', i);
|
|
181
|
+
if (!fs.existsSync(filePath)) {
|
|
182
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Font file not found at path: ${filePath}`, { itemIndex: i });
|
|
183
|
+
}
|
|
184
|
+
buffer = fs.readFileSync(filePath);
|
|
185
|
+
originalname = path.basename(filePath);
|
|
186
|
+
}
|
|
187
|
+
resultData = (0, utils_1.saveUserFont)(fontKey, fontName, description, originalname, buffer);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'delete': {
|
|
191
|
+
const fontKey = this.getNodeParameter('fontKey', i);
|
|
192
|
+
(0, utils_1.deleteUserFont)(fontKey);
|
|
193
|
+
resultData = { message: `Font '${fontKey}' deleted successfully.` };
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ===================================
|
|
199
|
+
// MEDIA RESOURCE OPERATIONS
|
|
200
|
+
// ===================================
|
|
201
|
+
else {
|
|
202
|
+
switch (operation) {
|
|
203
|
+
// Video Operations
|
|
204
|
+
case 'merge': {
|
|
205
|
+
const sourcesParam = this.getNodeParameter('videoSources', i, {});
|
|
206
|
+
const sourcesConfig = sourcesParam.sources || [];
|
|
207
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, sourcesConfig);
|
|
208
|
+
cleanup = c;
|
|
209
|
+
const mergeOutputFormat = this.getNodeParameter('videoOutputFormat', i);
|
|
210
|
+
outputPath = await operations_1.executeMerge.call(this, paths, mergeOutputFormat, i);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'trim': {
|
|
214
|
+
const sourceParam = this.getNodeParameter('source', i, {});
|
|
215
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
216
|
+
cleanup = c;
|
|
217
|
+
const startTime = this.getNodeParameter('startTime', i, 0);
|
|
218
|
+
const endTime = this.getNodeParameter('endTime', i, 10);
|
|
219
|
+
const outputFormat = this.getNodeParameter('videoOutputFormat', i, 'mp4');
|
|
220
|
+
outputPath = await operations_1.executeTrim.call(this, paths[0], startTime, endTime, outputFormat, i);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case 'speed': {
|
|
224
|
+
const sourceParam = this.getNodeParameter('speedSource', i, {});
|
|
225
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
226
|
+
cleanup = c;
|
|
227
|
+
const speed = this.getNodeParameter('speed', i, 1);
|
|
228
|
+
const adjustAudio = this.getNodeParameter('adjustAudio', i, true);
|
|
229
|
+
const maintainPitch = this.getNodeParameter('maintainPitch', i, false);
|
|
230
|
+
const outputFormat = this.getNodeParameter('speedOutputFormat', i, 'mp4');
|
|
231
|
+
outputPath = await operations_1.executeSpeed.call(this, paths[0], speed, adjustAudio, maintainPitch, outputFormat, i);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
// Audio Operations
|
|
235
|
+
case 'extract': {
|
|
236
|
+
const sourceParam = this.getNodeParameter('source', i, {});
|
|
237
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam]);
|
|
238
|
+
cleanup = c;
|
|
239
|
+
const extractFormat = this.getNodeParameter('audioOutputFormat', i);
|
|
240
|
+
const advancedOptions = this.getNodeParameter('advancedOptions', i, {});
|
|
241
|
+
const extractCodec = advancedOptions.audioCodec || 'copy';
|
|
242
|
+
const extractBitrate = advancedOptions.audioBitrate || '192k';
|
|
243
|
+
outputPath = await operations_1.executeExtractAudio.call(this, paths[0], extractFormat, extractCodec, extractBitrate, i);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case 'mixAudio': {
|
|
247
|
+
// Construct source objects from flattened properties
|
|
248
|
+
const videoSourceType = this.getNodeParameter('mixVideoSourceType', i, 'url');
|
|
249
|
+
const videoSourceParam = {
|
|
250
|
+
sourceType: videoSourceType,
|
|
251
|
+
value: videoSourceType === 'url'
|
|
252
|
+
? this.getNodeParameter('mixVideoSourceUrl', i, '')
|
|
253
|
+
: '',
|
|
254
|
+
binaryProperty: videoSourceType === 'binary'
|
|
255
|
+
? this.getNodeParameter('mixVideoSourceBinary', i, 'data')
|
|
256
|
+
: '',
|
|
257
|
+
};
|
|
258
|
+
const audioSourceType = this.getNodeParameter('mixAudioSourceType', i, 'url');
|
|
259
|
+
const audioSourceParam = {
|
|
260
|
+
sourceType: audioSourceType,
|
|
261
|
+
value: audioSourceType === 'url'
|
|
262
|
+
? this.getNodeParameter('mixAudioSourceUrl', i, '')
|
|
263
|
+
: '',
|
|
264
|
+
binaryProperty: audioSourceType === 'binary'
|
|
265
|
+
? this.getNodeParameter('mixAudioSourceBinary', i, 'data')
|
|
266
|
+
: '',
|
|
267
|
+
};
|
|
268
|
+
const { paths: videoPaths, cleanup: videoCleanup } = await (0, utils_1.resolveInputs)(this, i, [
|
|
269
|
+
videoSourceParam,
|
|
270
|
+
]);
|
|
271
|
+
const { paths: audioPaths, cleanup: audioCleanup } = await (0, utils_1.resolveInputs)(this, i, [
|
|
272
|
+
audioSourceParam,
|
|
273
|
+
]);
|
|
274
|
+
cleanup = async () => {
|
|
275
|
+
await videoCleanup();
|
|
276
|
+
await audioCleanup();
|
|
277
|
+
};
|
|
278
|
+
const videoVol = this.getNodeParameter('videoVolume', i, 1.0);
|
|
279
|
+
const audioVol = this.getNodeParameter('audioVolume', i, 1.0);
|
|
280
|
+
const matchLength = this.getNodeParameter('matchLength', i, 'shortest');
|
|
281
|
+
// Get advanced mixing parameters directly
|
|
282
|
+
const enablePartialMix = this.getNodeParameter('enablePartialMix', i, false);
|
|
283
|
+
const advancedMixing = {
|
|
284
|
+
enablePartialMix,
|
|
285
|
+
startTime: enablePartialMix ? this.getNodeParameter('startTime', i, 0) : 0,
|
|
286
|
+
duration: enablePartialMix ? this.getNodeParameter('duration', i, undefined) : undefined,
|
|
287
|
+
loop: enablePartialMix ? this.getNodeParameter('loop', i, false) : false,
|
|
288
|
+
enableFadeIn: this.getNodeParameter('enableFadeIn', i, false),
|
|
289
|
+
fadeInDuration: this.getNodeParameter('fadeInDuration', i, 1),
|
|
290
|
+
enableFadeOut: this.getNodeParameter('enableFadeOut', i, false),
|
|
291
|
+
fadeOutDuration: this.getNodeParameter('fadeOutDuration', i, 1),
|
|
292
|
+
};
|
|
293
|
+
outputPath = await operations_1.executeMixAudio.call(this, videoPaths[0], audioPaths[0], videoVol, audioVol, matchLength, advancedMixing, i);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
// Subtitle Operations
|
|
297
|
+
case 'addSubtitle': {
|
|
298
|
+
const videoSourceParam = this.getNodeParameter('source', i);
|
|
299
|
+
const { paths: videoPaths, cleanup: videoCleanup } = await (0, utils_1.resolveInputs)(this, i, [videoSourceParam.source]);
|
|
300
|
+
const subFileParam = this.getNodeParameter('subtitleFileSource', i);
|
|
301
|
+
const { paths: subFilePaths, cleanup: subFileCleanup } = await (0, utils_1.resolveInputs)(this, i, [subFileParam.source]);
|
|
302
|
+
cleanup = async () => {
|
|
303
|
+
await videoCleanup();
|
|
304
|
+
await subFileCleanup();
|
|
305
|
+
};
|
|
306
|
+
// Collect style options from individual parameters
|
|
307
|
+
const style = {
|
|
308
|
+
fontKey: this.getNodeParameter('fontKey', i, 'noto-sans-kr'),
|
|
309
|
+
size: this.getNodeParameter('size', i, 48),
|
|
310
|
+
color: this.getNodeParameter('color', i, 'white'),
|
|
311
|
+
outlineWidth: this.getNodeParameter('outlineWidth', i, 1),
|
|
312
|
+
positionType: this.getNodeParameter('positionType', i, 'alignment'),
|
|
313
|
+
horizontalAlign: this.getNodeParameter('horizontalAlign', i, 'center'),
|
|
314
|
+
verticalAlign: this.getNodeParameter('verticalAlign', i, 'bottom'),
|
|
315
|
+
paddingX: this.getNodeParameter('paddingX', i, 20),
|
|
316
|
+
paddingY: this.getNodeParameter('paddingY', i, 20),
|
|
317
|
+
x: this.getNodeParameter('x', i, '(w-text_w)/2'),
|
|
318
|
+
y: this.getNodeParameter('y', i, 'h-th-50'),
|
|
319
|
+
};
|
|
320
|
+
outputPath = await operations_1.executeAddSubtitle.call(this, videoPaths[0], subFilePaths[0], style, i);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case 'addText': {
|
|
324
|
+
const sourceParam = this.getNodeParameter('source', i);
|
|
325
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
326
|
+
cleanup = c;
|
|
327
|
+
// Get text content and timing
|
|
328
|
+
const text = this.getNodeParameter('text', i, 'Hello, n8n!');
|
|
329
|
+
const startTime = this.getNodeParameter('startTime', i, 0);
|
|
330
|
+
const endTime = this.getNodeParameter('endTime', i, 5);
|
|
331
|
+
// Collect style options from individual parameters
|
|
332
|
+
const textOptions = {
|
|
333
|
+
fontKey: this.getNodeParameter('fontKey', i, 'noto-sans-kr'),
|
|
334
|
+
size: this.getNodeParameter('size', i, 48),
|
|
335
|
+
color: this.getNodeParameter('color', i, 'white'),
|
|
336
|
+
outlineWidth: this.getNodeParameter('outlineWidth', i, 1),
|
|
337
|
+
positionType: this.getNodeParameter('positionType', i, 'alignment'),
|
|
338
|
+
horizontalAlign: this.getNodeParameter('horizontalAlign', i, 'center'),
|
|
339
|
+
verticalAlign: this.getNodeParameter('verticalAlign', i, 'bottom'),
|
|
340
|
+
paddingX: this.getNodeParameter('paddingX', i, 20),
|
|
341
|
+
paddingY: this.getNodeParameter('paddingY', i, 20),
|
|
342
|
+
x: this.getNodeParameter('x', i, '(w-text_w)/2'),
|
|
343
|
+
y: this.getNodeParameter('y', i, 'h-th-10'),
|
|
344
|
+
startTime,
|
|
345
|
+
endTime,
|
|
346
|
+
};
|
|
347
|
+
outputPath = await operations_1.executeAddText.call(this, paths[0], text, textOptions, i);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case 'multiTransition': {
|
|
351
|
+
const sourcesParam = this.getNodeParameter('transitionSources', i, {});
|
|
352
|
+
const sourcesConfig = sourcesParam.sources || [];
|
|
353
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, sourcesConfig);
|
|
354
|
+
cleanup = c;
|
|
355
|
+
const transitionEffect = this.getNodeParameter('transitionEffect', i);
|
|
356
|
+
const transitionDuration = this.getNodeParameter('transitionDuration', i);
|
|
357
|
+
const transitionOutputFormat = this.getNodeParameter('transitionOutputFormat', i, 'mp4');
|
|
358
|
+
outputPath = await operations_1.executeMultiVideoTransition.call(this, paths, transitionEffect, transitionDuration, transitionOutputFormat, i);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case 'singleFade': {
|
|
362
|
+
const sourceParam = this.getNodeParameter('fadeSource', i);
|
|
363
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
364
|
+
cleanup = c;
|
|
365
|
+
const fadeEffect = this.getNodeParameter('fadeEffect', i);
|
|
366
|
+
const fadeStartTime = this.getNodeParameter('fadeStartTime', i);
|
|
367
|
+
const fadeDuration = this.getNodeParameter('fadeDuration', i);
|
|
368
|
+
const outputFormat = this.getNodeParameter('transitionOutputFormat', i, 'mp4');
|
|
369
|
+
outputPath = await operations_1.executeSingleVideoFade.call(this, paths[0], fadeEffect, fadeStartTime, fadeDuration, outputFormat, i);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case 'imageToVideo': {
|
|
373
|
+
const sourceParam = this.getNodeParameter('sourceImage', i, {});
|
|
374
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
375
|
+
cleanup = c;
|
|
376
|
+
const duration = this.getNodeParameter('duration', i, 10);
|
|
377
|
+
const videoSize = this.getNodeParameter('videoSize', i);
|
|
378
|
+
const outputFormat = this.getNodeParameter('imageOutputFormat', i, 'mp4');
|
|
379
|
+
outputPath = await operations_1.executeImageToVideo.call(this, paths[0], duration, videoSize, outputFormat, i);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case 'stampImage': {
|
|
383
|
+
const sourceVideo = this.getNodeParameter('sourceVideo.source', i);
|
|
384
|
+
const stampImage = this.getNodeParameter('stampImage.source', i);
|
|
385
|
+
// Get individual stamp options
|
|
386
|
+
const stampOptions = {
|
|
387
|
+
width: this.getNodeParameter('width', i, 150),
|
|
388
|
+
height: this.getNodeParameter('height', i, -1),
|
|
389
|
+
x: this.getNodeParameter('x', i, '10'),
|
|
390
|
+
y: this.getNodeParameter('y', i, '10'),
|
|
391
|
+
rotation: this.getNodeParameter('rotation', i, 0),
|
|
392
|
+
enableTimeControl: this.getNodeParameter('enableTimeControl', i, false),
|
|
393
|
+
startTime: this.getNodeParameter('startTime', i, 0),
|
|
394
|
+
endTime: this.getNodeParameter('endTime', i, 5),
|
|
395
|
+
opacity: this.getNodeParameter('opacity', i, 1.0),
|
|
396
|
+
};
|
|
397
|
+
const { paths: videoPaths, cleanup: videoCleanup } = await (0, utils_1.resolveInputs)(this, i, [
|
|
398
|
+
sourceVideo,
|
|
399
|
+
]);
|
|
400
|
+
const { paths: imagePaths, cleanup: imageCleanup } = await (0, utils_1.resolveInputs)(this, i, [
|
|
401
|
+
stampImage,
|
|
402
|
+
]);
|
|
403
|
+
cleanup = async () => {
|
|
404
|
+
await videoCleanup();
|
|
405
|
+
await imageCleanup();
|
|
406
|
+
};
|
|
407
|
+
outputPath = await operations_1.executeStampImage.call(this, videoPaths[0], imagePaths[0], stampOptions, i);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case 'separateAudio': {
|
|
411
|
+
const sourceParam = this.getNodeParameter('separateSource', i, {});
|
|
412
|
+
if (!sourceParam.source) {
|
|
413
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Video source is required. Please add a video source.', { itemIndex: i });
|
|
414
|
+
}
|
|
415
|
+
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
416
|
+
cleanup = c;
|
|
417
|
+
const videoFormat = this.getNodeParameter('separateVideoFormat', i, 'mp4');
|
|
418
|
+
const audioFormat = this.getNodeParameter('separateAudioFormat', i, 'mp3');
|
|
419
|
+
const audioCodec = this.getNodeParameter('separateAudioCodec', i, 'copy');
|
|
420
|
+
const audioBitrate = this.getNodeParameter('separateAudioBitrate', i, '192k');
|
|
421
|
+
const result = await operations_1.executeSeparateAudio.call(this, paths[0], videoFormat, audioFormat, audioCodec, audioBitrate, i);
|
|
422
|
+
// Get output field names
|
|
423
|
+
const videoFieldName = this.getNodeParameter('separateVideoFieldName', i, 'video');
|
|
424
|
+
const audioFieldName = this.getNodeParameter('separateAudioFieldName', i, 'audio');
|
|
425
|
+
// Read and prepare both binary outputs
|
|
426
|
+
const videoBinaryData = await fs.readFile(result.videoPath);
|
|
427
|
+
const audioBinaryData = await fs.readFile(result.audioPath);
|
|
428
|
+
const videoFileName = path.basename(result.videoPath);
|
|
429
|
+
const audioFileName = path.basename(result.audioPath);
|
|
430
|
+
const videoBinary = await this.helpers.prepareBinaryData(videoBinaryData, videoFileName);
|
|
431
|
+
const audioBinary = await this.helpers.prepareBinaryData(audioBinaryData, audioFileName);
|
|
432
|
+
// Clean up temp files
|
|
433
|
+
await fs.remove(result.videoPath);
|
|
434
|
+
await fs.remove(result.audioPath);
|
|
435
|
+
// Push result with both binary outputs
|
|
436
|
+
returnData.push({
|
|
437
|
+
json: {
|
|
438
|
+
success: true,
|
|
439
|
+
operation: 'separateAudio',
|
|
440
|
+
videoFormat,
|
|
441
|
+
audioFormat,
|
|
442
|
+
},
|
|
443
|
+
binary: {
|
|
444
|
+
[videoFieldName]: videoBinary,
|
|
445
|
+
[audioFieldName]: audioBinary,
|
|
446
|
+
},
|
|
447
|
+
pairedItem: { item: i },
|
|
448
|
+
});
|
|
449
|
+
// Skip normal output processing for this operation
|
|
450
|
+
await cleanup();
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
case 'overlayVideo': {
|
|
454
|
+
const mainSourceParam = this.getNodeParameter('overlayMainSource', i, {});
|
|
455
|
+
const overlaySourceParam = this.getNodeParameter('overlaySource', i, {});
|
|
456
|
+
if (!mainSourceParam.source) {
|
|
457
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Main video source is required. Please add a main video source.', { itemIndex: i });
|
|
458
|
+
}
|
|
459
|
+
if (!overlaySourceParam.source) {
|
|
460
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Overlay video source is required. Please add an overlay video source.', { itemIndex: i });
|
|
461
|
+
}
|
|
462
|
+
const { paths: mainPaths, cleanup: mainCleanup } = await (0, utils_1.resolveInputs)(this, i, [mainSourceParam.source]);
|
|
463
|
+
const { paths: overlayPaths, cleanup: overlayCleanup } = await (0, utils_1.resolveInputs)(this, i, [overlaySourceParam.source]);
|
|
464
|
+
cleanup = async () => {
|
|
465
|
+
await mainCleanup();
|
|
466
|
+
await overlayCleanup();
|
|
467
|
+
};
|
|
468
|
+
// Get overlay options
|
|
469
|
+
const sizeMode = this.getNodeParameter('overlaySizeMode', i, 'percentage');
|
|
470
|
+
const positionMode = this.getNodeParameter('overlayPositionMode', i, 'alignment');
|
|
471
|
+
const overlayOptions = {
|
|
472
|
+
// Position options
|
|
473
|
+
positionMode,
|
|
474
|
+
// Alignment mode options
|
|
475
|
+
horizontalAlign: positionMode === 'alignment' ? this.getNodeParameter('overlayHorizontalAlign', i, 'center') : undefined,
|
|
476
|
+
verticalAlign: positionMode === 'alignment' ? this.getNodeParameter('overlayVerticalAlign', i, 'middle') : undefined,
|
|
477
|
+
paddingX: positionMode === 'alignment' ? this.getNodeParameter('overlayPaddingX', i, 0) : undefined,
|
|
478
|
+
paddingY: positionMode === 'alignment' ? this.getNodeParameter('overlayPaddingY', i, 0) : undefined,
|
|
479
|
+
// Coordinates mode options
|
|
480
|
+
x: positionMode === 'coordinates' ? this.getNodeParameter('overlayX', i, '0') : undefined,
|
|
481
|
+
y: positionMode === 'coordinates' ? this.getNodeParameter('overlayY', i, '0') : undefined,
|
|
482
|
+
// Size options
|
|
483
|
+
sizeMode,
|
|
484
|
+
widthPercent: sizeMode === 'percentage' ? this.getNodeParameter('overlayWidthPercent', i, 50) : undefined,
|
|
485
|
+
heightMode: sizeMode === 'percentage' ? this.getNodeParameter('overlayHeightMode', i, 'auto') : undefined,
|
|
486
|
+
heightPercent: sizeMode === 'percentage' ? this.getNodeParameter('overlayHeightPercent', i, 50) : undefined,
|
|
487
|
+
widthPixels: sizeMode === 'pixels' ? this.getNodeParameter('overlayWidthPixels', i, 640) : undefined,
|
|
488
|
+
heightPixels: sizeMode === 'pixels' ? this.getNodeParameter('overlayHeightPixels', i, -1) : undefined,
|
|
489
|
+
opacity: this.getNodeParameter('overlayOpacity', i, 1),
|
|
490
|
+
enableTimeControl: this.getNodeParameter('overlayEnableTimeControl', i, false),
|
|
491
|
+
startTime: this.getNodeParameter('overlayStartTime', i, 0),
|
|
492
|
+
endTime: this.getNodeParameter('overlayEndTime', i, 0),
|
|
493
|
+
audioHandling: this.getNodeParameter('overlayAudioHandling', i, 'main'),
|
|
494
|
+
mainVolume: this.getNodeParameter('overlayMainVolume', i, 1),
|
|
495
|
+
overlayVolume: this.getNodeParameter('overlayOverlayVolume', i, 0.5),
|
|
496
|
+
outputFormat: this.getNodeParameter('overlayOutputFormat', i, 'mp4'),
|
|
497
|
+
};
|
|
498
|
+
outputPath = await operations_1.executeOverlayVideo.call(this, mainPaths[0], overlayPaths[0], overlayOptions, i);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Always cleanup after operations
|
|
503
|
+
await cleanup();
|
|
504
|
+
}
|
|
505
|
+
// ===================================
|
|
506
|
+
// FINAL OUTPUT PROCESSING
|
|
507
|
+
// ===================================
|
|
508
|
+
if (outputPath) {
|
|
509
|
+
// Operation resulted in a file to be returned
|
|
510
|
+
const binaryData = await fs.readFile(outputPath);
|
|
511
|
+
const fileName = path.basename(outputPath);
|
|
512
|
+
const binary = await this.helpers.prepareBinaryData(binaryData, fileName);
|
|
513
|
+
// Get the output field name from parameters (default to 'data')
|
|
514
|
+
const outputFieldName = this.getNodeParameter('outputFieldName', i, 'data');
|
|
515
|
+
await fs.remove(outputPath); // Clean up temp file
|
|
516
|
+
returnData.push({ json: {}, binary: { [outputFieldName]: binary }, pairedItem: { item: i } });
|
|
517
|
+
}
|
|
518
|
+
else if (resultData) {
|
|
519
|
+
// Operation resulted in JSON data
|
|
520
|
+
returnData.push({
|
|
521
|
+
json: {
|
|
522
|
+
success: true,
|
|
523
|
+
operation: this.getNodeParameter('operation', i),
|
|
524
|
+
data: resultData,
|
|
525
|
+
},
|
|
526
|
+
pairedItem: { item: i },
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
else if (resource !== 'font') {
|
|
530
|
+
// This case handles non-font operations that might not produce output
|
|
531
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation "${operation}" on resource "${resource}" did not produce an output.`, { itemIndex: i });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
// Ensure cleanup is called even if an error occurs
|
|
536
|
+
// Note: cleanup is only available for media operations, not font operations
|
|
537
|
+
if (resource !== 'font' && typeof cleanup === 'function') {
|
|
538
|
+
await cleanup().catch(() => {
|
|
539
|
+
// Ignore cleanup errors to avoid masking the original error
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (this.continueOnFail()) {
|
|
543
|
+
returnData.push({
|
|
544
|
+
json: {
|
|
545
|
+
error: error instanceof Error ? error.message : String(error),
|
|
546
|
+
operation: this.getNodeParameter('operation', i),
|
|
547
|
+
success: false,
|
|
548
|
+
},
|
|
549
|
+
pairedItem: { item: i },
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return [returnData];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
exports.MediaFX = MediaFX;
|
|
Binary file
|