@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
|
@@ -0,0 +1,146 @@
|
|
|
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.executeAddSubtitle = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const fs = __importStar(require("fs-extra"));
|
|
30
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
31
|
+
const utils_1 = require("../utils");
|
|
32
|
+
/**
|
|
33
|
+
* Escape special characters in file path for FFmpeg subtitles filter.
|
|
34
|
+
* FFmpeg subtitles filter requires escaping: \ : [ ] '
|
|
35
|
+
*/
|
|
36
|
+
function escapeSubtitlePath(filePath) {
|
|
37
|
+
return filePath
|
|
38
|
+
.replace(/\\/g, '\\\\\\\\') // Backslash (Windows paths)
|
|
39
|
+
.replace(/:/g, '\\:') // Colon
|
|
40
|
+
.replace(/\[/g, '\\[') // Square brackets
|
|
41
|
+
.replace(/\]/g, '\\]')
|
|
42
|
+
.replace(/'/g, "\\'"); // Single quote
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build ASS/SSA style string for subtitles filter force_style option.
|
|
46
|
+
* This converts our style options to FFmpeg's force_style format.
|
|
47
|
+
*/
|
|
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;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// bottom (default)
|
|
84
|
+
alignment = horizontalAlign === 'left' ? 1 : horizontalAlign === 'right' ? 3 : 2;
|
|
85
|
+
}
|
|
86
|
+
const styleParams = [
|
|
87
|
+
`FontName=${fontName}`,
|
|
88
|
+
`FontSize=${fontSize}`,
|
|
89
|
+
`PrimaryColour=${assColor}`,
|
|
90
|
+
`OutlineColour=&H00000000`,
|
|
91
|
+
`BackColour=&H80000000`,
|
|
92
|
+
`Bold=0`,
|
|
93
|
+
`Italic=0`,
|
|
94
|
+
`BorderStyle=4`,
|
|
95
|
+
`Outline=1`,
|
|
96
|
+
`Shadow=0`,
|
|
97
|
+
`Alignment=${alignment}`,
|
|
98
|
+
`MarginV=${marginV}`,
|
|
99
|
+
];
|
|
100
|
+
return styleParams.join(',');
|
|
101
|
+
}
|
|
102
|
+
async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
105
|
+
// 1. Get Font from fontKey
|
|
106
|
+
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
107
|
+
const fontKey = style.fontKey || 'noto-sans-kr';
|
|
108
|
+
const font = allFonts[fontKey];
|
|
109
|
+
if (!font || !font.path) {
|
|
110
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font key '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
|
|
111
|
+
}
|
|
112
|
+
const fontName = font.name || 'Sans';
|
|
113
|
+
const fontSize = style.size || 48;
|
|
114
|
+
const fontColor = style.color || 'white';
|
|
115
|
+
// 2. Handle position based on position type
|
|
116
|
+
const positionType = style.positionType || 'alignment';
|
|
117
|
+
let horizontalAlign = 'center';
|
|
118
|
+
let verticalAlign = 'bottom';
|
|
119
|
+
let marginV = 20;
|
|
120
|
+
if (positionType === 'alignment') {
|
|
121
|
+
horizontalAlign = style.horizontalAlign || 'center';
|
|
122
|
+
verticalAlign = style.verticalAlign || 'bottom';
|
|
123
|
+
marginV = (_b = (_a = style.paddingY) !== null && _a !== void 0 ? _a : style.padding) !== null && _b !== void 0 ? _b : 20;
|
|
124
|
+
}
|
|
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
|
+
const command = ffmpeg(video)
|
|
133
|
+
.videoFilters([subtitlesFilter])
|
|
134
|
+
.audioCodec('copy')
|
|
135
|
+
.save(outputPath);
|
|
136
|
+
try {
|
|
137
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
138
|
+
return outputPath;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Clean up output file if creation failed
|
|
142
|
+
await fs.remove(outputPath).catch(() => { });
|
|
143
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.executeAddSubtitle = executeAddSubtitle;
|
|
@@ -0,0 +1,108 @@
|
|
|
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.executeAddText = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
30
|
+
const utils_1 = require("../utils");
|
|
31
|
+
const fs = __importStar(require("fs-extra"));
|
|
32
|
+
function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY) {
|
|
33
|
+
let x;
|
|
34
|
+
let y;
|
|
35
|
+
// Set X position based on horizontal alignment
|
|
36
|
+
switch (horizontalAlign) {
|
|
37
|
+
case 'left':
|
|
38
|
+
x = `${paddingX}`;
|
|
39
|
+
break;
|
|
40
|
+
case 'right':
|
|
41
|
+
x = `w-text_w-${paddingX}`;
|
|
42
|
+
break;
|
|
43
|
+
case 'center':
|
|
44
|
+
default:
|
|
45
|
+
x = '(w-text_w)/2';
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
// Set Y position based on vertical alignment
|
|
49
|
+
switch (verticalAlign) {
|
|
50
|
+
case 'top':
|
|
51
|
+
y = `${paddingY}`;
|
|
52
|
+
break;
|
|
53
|
+
case 'bottom':
|
|
54
|
+
y = `h-th-${paddingY}`;
|
|
55
|
+
break;
|
|
56
|
+
case 'middle':
|
|
57
|
+
default:
|
|
58
|
+
y = '(h-text_h)/2';
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return { x, y };
|
|
62
|
+
}
|
|
63
|
+
async function executeAddText(video, text, options, itemIndex) {
|
|
64
|
+
var _a, _b, _c, _d;
|
|
65
|
+
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
66
|
+
const fontKey = options.fontKey || 'noto-sans-kr';
|
|
67
|
+
const font = allFonts[fontKey];
|
|
68
|
+
if (!font || !font.path) {
|
|
69
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font key '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
|
|
70
|
+
}
|
|
71
|
+
const fontPath = font.path;
|
|
72
|
+
// Set default values for text options
|
|
73
|
+
const fontSize = options.size || 48;
|
|
74
|
+
const fontColor = options.color || 'white';
|
|
75
|
+
const startTime = options.startTime || 0;
|
|
76
|
+
const endTime = options.endTime || 5;
|
|
77
|
+
// Handle position based on position type
|
|
78
|
+
let positionX;
|
|
79
|
+
let positionY;
|
|
80
|
+
const positionType = options.positionType || 'alignment';
|
|
81
|
+
if (positionType === 'alignment') {
|
|
82
|
+
const horizontalAlign = options.horizontalAlign || 'center';
|
|
83
|
+
const verticalAlign = options.verticalAlign || 'bottom';
|
|
84
|
+
const paddingX = (_b = (_a = options.paddingX) !== null && _a !== void 0 ? _a : options.padding) !== null && _b !== void 0 ? _b : 20;
|
|
85
|
+
const paddingY = (_d = (_c = options.paddingY) !== null && _c !== void 0 ? _c : options.padding) !== null && _d !== void 0 ? _d : 20;
|
|
86
|
+
const position = getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY);
|
|
87
|
+
positionX = position.x;
|
|
88
|
+
positionY = position.y;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Custom position
|
|
92
|
+
positionX = options.x || '(w-text_w)/2';
|
|
93
|
+
positionY = options.y || 'h-th-10';
|
|
94
|
+
}
|
|
95
|
+
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
96
|
+
const drawtext = `drawtext=fontfile=${fontPath}:text='${text.replace(/'/g, `''`)}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}:enable='between(t,${startTime},${endTime})'`;
|
|
97
|
+
const command = ffmpeg(video).videoFilters(drawtext).audioCodec('copy').save(outputPath);
|
|
98
|
+
try {
|
|
99
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
100
|
+
return outputPath;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Clean up output file if creation failed
|
|
104
|
+
await fs.remove(outputPath).catch(() => { });
|
|
105
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding text to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.executeAddText = executeAddText;
|
|
@@ -0,0 +1,57 @@
|
|
|
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.executeExtractAudio = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
29
|
+
const utils_1 = require("../utils");
|
|
30
|
+
const fs = __importStar(require("fs-extra"));
|
|
31
|
+
async function executeExtractAudio(input, format, codec, bitrate, itemIndex) {
|
|
32
|
+
const outputPath = (0, utils_1.getTempFile)(`.${format}`);
|
|
33
|
+
let finalCodec = codec;
|
|
34
|
+
// If stream copy ('copy') is selected for a format that requires a specific
|
|
35
|
+
// codec (like mp3), we must override it to prevent an FFmpeg error.
|
|
36
|
+
if (finalCodec === 'copy' && format === 'mp3') {
|
|
37
|
+
// Cannot stream copy a non-mp3 stream (e.g., aac) into an mp3 container.
|
|
38
|
+
// Default to a high-quality mp3 encoder.
|
|
39
|
+
finalCodec = 'libmp3lame';
|
|
40
|
+
}
|
|
41
|
+
const command = ffmpeg(input)
|
|
42
|
+
.output(outputPath)
|
|
43
|
+
.noVideo()
|
|
44
|
+
.audioCodec(finalCodec)
|
|
45
|
+
.audioBitrate(bitrate)
|
|
46
|
+
.outputOptions('-map', '0:a:0'); // Select first audio stream
|
|
47
|
+
try {
|
|
48
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
49
|
+
return outputPath;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// Clean up output file if creation failed
|
|
53
|
+
await fs.remove(outputPath).catch(() => { });
|
|
54
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error extracting audio. Please ensure the source video contains an audio track. FFmpeg error: ${error.message}`, { itemIndex });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.executeExtractAudio = executeExtractAudio;
|
|
@@ -0,0 +1,65 @@
|
|
|
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.executeImageToVideo = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const utils_1 = require("../utils");
|
|
29
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
30
|
+
const fs = __importStar(require("fs-extra"));
|
|
31
|
+
async function executeImageToVideo(imagePath, duration, videoSize, outputFormat, itemIndex) {
|
|
32
|
+
const outputPath = (0, utils_1.getTempFile)(`.${outputFormat}`);
|
|
33
|
+
const command = ffmpeg()
|
|
34
|
+
.input(imagePath)
|
|
35
|
+
.loop(duration)
|
|
36
|
+
// Add a silent audio track for compatibility
|
|
37
|
+
.input('anullsrc')
|
|
38
|
+
.inputFormat('lavfi')
|
|
39
|
+
// Use libx264 codec for broad compatibility
|
|
40
|
+
.videoCodec('libx264')
|
|
41
|
+
// Set pixel format
|
|
42
|
+
.outputOptions('-pix_fmt', 'yuv420p')
|
|
43
|
+
.audioCodec('aac')
|
|
44
|
+
.outputOptions('-shortest'); // End when the shortest input ends.
|
|
45
|
+
if (videoSize.width && videoSize.height) {
|
|
46
|
+
// Many codecs require even dimensions, but we'll let the user decide.
|
|
47
|
+
// For more robustness, we could add validation or rounding logic here.
|
|
48
|
+
command.videoFilters(`scale=${videoSize.width}:${videoSize.height}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Default behavior: ensure even dimensions for compatibility
|
|
52
|
+
command.videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2');
|
|
53
|
+
}
|
|
54
|
+
command.save(outputPath);
|
|
55
|
+
try {
|
|
56
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
57
|
+
return outputPath;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Clean up output file if creation failed
|
|
61
|
+
await fs.remove(outputPath).catch(() => { });
|
|
62
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error converting image to video. Please ensure the source image is valid. FFmpeg error: ${error.message}`, { itemIndex });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.executeImageToVideo = executeImageToVideo;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './merge';
|
|
2
|
+
export * from './trim';
|
|
3
|
+
export * from './speed';
|
|
4
|
+
export * from './mixAudio';
|
|
5
|
+
export * from './addSubtitle';
|
|
6
|
+
export * from './addText';
|
|
7
|
+
export * from './extractAudio';
|
|
8
|
+
export * from './separateAudio';
|
|
9
|
+
export * from './multiVideoTransition';
|
|
10
|
+
export * from './singleVideoFade';
|
|
11
|
+
export * from './imageToVideo';
|
|
12
|
+
export * from './stampImage';
|
|
13
|
+
export * from './overlayVideo';
|
|
@@ -0,0 +1,29 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./merge"), exports);
|
|
18
|
+
__exportStar(require("./trim"), exports);
|
|
19
|
+
__exportStar(require("./speed"), exports);
|
|
20
|
+
__exportStar(require("./mixAudio"), exports);
|
|
21
|
+
__exportStar(require("./addSubtitle"), exports);
|
|
22
|
+
__exportStar(require("./addText"), exports);
|
|
23
|
+
__exportStar(require("./extractAudio"), exports);
|
|
24
|
+
__exportStar(require("./separateAudio"), exports);
|
|
25
|
+
__exportStar(require("./multiVideoTransition"), exports);
|
|
26
|
+
__exportStar(require("./singleVideoFade"), exports);
|
|
27
|
+
__exportStar(require("./imageToVideo"), exports);
|
|
28
|
+
__exportStar(require("./stampImage"), exports);
|
|
29
|
+
__exportStar(require("./overlayVideo"), exports);
|
|
@@ -0,0 +1,121 @@
|
|
|
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.executeMerge = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
29
|
+
const fs = __importStar(require("fs-extra"));
|
|
30
|
+
const utils_1 = require("../utils");
|
|
31
|
+
async function normalizeVideo(inputPath, refInfo) {
|
|
32
|
+
const normalizedPath = (0, utils_1.getTempFile)('.ts');
|
|
33
|
+
const mainCleanup = () => fs.remove(normalizedPath);
|
|
34
|
+
let silentAudioCleanup = null;
|
|
35
|
+
const hasAudio = await (0, utils_1.fileHasAudio)(inputPath);
|
|
36
|
+
const command = ffmpeg(inputPath);
|
|
37
|
+
if (!hasAudio) {
|
|
38
|
+
const duration = await (0, utils_1.getDuration)(inputPath);
|
|
39
|
+
const { filePath: silentAudioPath, cleanup } = await (0, utils_1.createSilentAudio)(duration);
|
|
40
|
+
command.addInput(silentAudioPath);
|
|
41
|
+
silentAudioCleanup = cleanup;
|
|
42
|
+
}
|
|
43
|
+
const targetWidth = refInfo.width;
|
|
44
|
+
const targetHeight = refInfo.height;
|
|
45
|
+
const targetSar = (refInfo.sample_aspect_ratio && refInfo.sample_aspect_ratio !== 'N/A') ? refInfo.sample_aspect_ratio : '1:1';
|
|
46
|
+
const targetFrameRate = refInfo.r_frame_rate || '30';
|
|
47
|
+
const targetPixFmt = 'yuv420p';
|
|
48
|
+
const videoFilter = `[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:-1:-1:color=black,setsar=${targetSar},format=${targetPixFmt},fps=${targetFrameRate},setpts=PTS-STARTPTS[v_out]`;
|
|
49
|
+
const audioFilter = hasAudio
|
|
50
|
+
? `[0:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,asetpts=PTS-STARTPTS[a_out]`
|
|
51
|
+
: `[1:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,asetpts=PTS-STARTPTS[a_out]`;
|
|
52
|
+
command
|
|
53
|
+
.complexFilter([videoFilter, audioFilter])
|
|
54
|
+
.outputOptions(['-map', '[v_out]', '-map', '[a_out]'])
|
|
55
|
+
.videoCodec('libx264')
|
|
56
|
+
.audioCodec('aac')
|
|
57
|
+
.save(normalizedPath);
|
|
58
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
59
|
+
const combinedCleanup = async () => {
|
|
60
|
+
await mainCleanup();
|
|
61
|
+
if (silentAudioCleanup) {
|
|
62
|
+
await silentAudioCleanup();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return { normalizedPath, cleanup: combinedCleanup };
|
|
66
|
+
}
|
|
67
|
+
async function executeMerge(inputs, outputFormat, itemIndex) {
|
|
68
|
+
// Verify FFmpeg is available before proceeding
|
|
69
|
+
try {
|
|
70
|
+
(0, utils_1.verifyFfmpegAvailability)();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `FFmpeg is not available: ${error.message}`, { itemIndex });
|
|
74
|
+
}
|
|
75
|
+
if (inputs.length < 1) {
|
|
76
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Merge operation requires at least one source.', {
|
|
77
|
+
itemIndex,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const intermediateFiles = [];
|
|
81
|
+
let outputPath = null;
|
|
82
|
+
const finalCleanup = async () => {
|
|
83
|
+
for (const file of intermediateFiles) {
|
|
84
|
+
await file.cleanup();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const videoInfos = await Promise.all(inputs.map(utils_1.getVideoStreamInfo));
|
|
89
|
+
const refInfo = videoInfos.find((info) => info !== undefined);
|
|
90
|
+
if (!refInfo || !refInfo.width || !refInfo.height) {
|
|
91
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Could not determine reference video properties for merging. At least one input must be a valid video.', { itemIndex });
|
|
92
|
+
}
|
|
93
|
+
// 1. Normalization Stage
|
|
94
|
+
for (const inputPath of inputs) {
|
|
95
|
+
const { normalizedPath, cleanup } = await normalizeVideo.call(this, inputPath, refInfo);
|
|
96
|
+
intermediateFiles.push({ path: normalizedPath, cleanup });
|
|
97
|
+
}
|
|
98
|
+
// 2. Merging Stage - Using the concat protocol for stability
|
|
99
|
+
outputPath = (0, utils_1.getTempFile)(`.${outputFormat}`);
|
|
100
|
+
const normalizedPaths = intermediateFiles.map((f) => f.path);
|
|
101
|
+
const concatString = `concat:${normalizedPaths.join('|')}`;
|
|
102
|
+
const command = ffmpeg()
|
|
103
|
+
.input(concatString)
|
|
104
|
+
.outputOptions('-c', 'copy')
|
|
105
|
+
.save(outputPath);
|
|
106
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
107
|
+
return outputPath;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Clean up output file if creation failed
|
|
111
|
+
if (outputPath) {
|
|
112
|
+
await fs.remove(outputPath).catch(() => { });
|
|
113
|
+
}
|
|
114
|
+
await finalCleanup();
|
|
115
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error merging videos. Please ensure all source videos are valid. FFmpeg error: ${error.message}`, { itemIndex });
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await finalCleanup();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.executeMerge = executeMerge;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare function executeMixAudio(this: IExecuteFunctions, videoPath: string, audioPath: string, videoVolume: number, audioVolume: number, matchLength: 'shortest' | 'longest' | 'first', advancedMixing: IDataObject, itemIndex: number): Promise<string>;
|