n8n-nodes-ffmpeg-wasm 1.2.7 → 1.3.0

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FFmpegWasm = void 0;
4
4
  const ffmpeg_1 = require("@ffmpeg/ffmpeg");
5
5
  const helpers_1 = require("./helpers");
6
+ const operations_1 = require("./operations");
6
7
  class FFmpegWasm {
7
8
  constructor() {
8
9
  this.description = {
@@ -952,6 +953,27 @@ class FFmpegWasm {
952
953
  default: false,
953
954
  description: "Whether to log FFmpeg output to console",
954
955
  },
956
+ {
957
+ displayName: "Encoding Preset",
958
+ name: "encodingPreset",
959
+ type: "options",
960
+ default: "ultrafast",
961
+ description: "Encoding speed preset for x264/x265. Faster presets produce larger files but process much quicker in WebAssembly",
962
+ options: [
963
+ {
964
+ name: "Ultra Fast (fastest, larger files)",
965
+ value: "ultrafast",
966
+ },
967
+ { name: "Super Fast", value: "superfast" },
968
+ { name: "Very Fast", value: "veryfast" },
969
+ { name: "Faster", value: "faster" },
970
+ { name: "Fast", value: "fast" },
971
+ {
972
+ name: "Medium (FFmpeg default, slowest)",
973
+ value: "medium",
974
+ },
975
+ ],
976
+ },
955
977
  ],
956
978
  },
957
979
  ],
@@ -960,6 +982,13 @@ class FFmpegWasm {
960
982
  async execute() {
961
983
  const items = this.getInputData();
962
984
  const returnData = [];
985
+ try {
986
+ require.resolve("@ffmpeg/core");
987
+ }
988
+ catch {
989
+ throw new Error("FFmpeg.wasm core module (@ffmpeg/core) is not installed. " +
990
+ "Please uninstall and reinstall the n8n-nodes-ffmpeg-wasm community node.");
991
+ }
963
992
  let corePath;
964
993
  try {
965
994
  const credentials = await this.getCredentials("ffmpegWasmApi");
@@ -969,17 +998,34 @@ class FFmpegWasm {
969
998
  }
970
999
  catch {
971
1000
  }
972
- let lastLogOutput = "";
973
1001
  const firstOpts = this.getNodeParameter("additionalOptions", 0, {});
1002
+ const logLines = [];
1003
+ const getLog = () => logLines.join("\n");
1004
+ const resetLog = () => { logLines.length = 0; };
974
1005
  const ffmpeg = (0, ffmpeg_1.createFFmpeg)({
975
1006
  log: firstOpts.enableLogging || false,
976
1007
  logger: ({ message }) => {
977
- lastLogOutput += message + "\n";
1008
+ logLines.push(message);
978
1009
  },
979
1010
  ...(corePath ? { corePath } : {}),
980
1011
  });
981
1012
  try {
982
- await ffmpeg.load();
1013
+ const globalAny = globalThis;
1014
+ const savedFetch = globalAny.fetch;
1015
+ try {
1016
+ delete globalAny.fetch;
1017
+ await ffmpeg.load();
1018
+ }
1019
+ catch (loadError) {
1020
+ const msg = loadError instanceof Error
1021
+ ? loadError.message
1022
+ : String(loadError);
1023
+ throw new Error(`Failed to initialize FFmpeg.wasm: ${msg}. ` +
1024
+ `Ensure the n8n instance has sufficient memory (512MB+ recommended).`);
1025
+ }
1026
+ finally {
1027
+ globalAny.fetch = savedFetch;
1028
+ }
983
1029
  for (let i = 0; i < items.length; i++) {
984
1030
  try {
985
1031
  const binaryPropertyName = this.getNodeParameter("binaryPropertyName", i);
@@ -994,793 +1040,95 @@ class FFmpegWasm {
994
1040
  const outputFilename = `output_${i}_${Date.now()}`;
995
1041
  const inputData = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
996
1042
  ffmpeg.FS("writeFile", inputFilename, new Uint8Array(inputData));
997
- let ffmpegCommand = [];
998
- let outputExt = "";
1043
+ const preset = additionalOptions.encodingPreset || "ultrafast";
1044
+ const handlerParams = {
1045
+ ctx: this,
1046
+ ffmpeg,
1047
+ i,
1048
+ inputFilename,
1049
+ outputFilename,
1050
+ items,
1051
+ preset,
1052
+ getLog,
1053
+ resetLog,
1054
+ };
999
1055
  if (operation === "metadata") {
1000
- lastLogOutput = "";
1001
- try {
1002
- await ffmpeg.run("-i", inputFilename, "-hide_banner");
1003
- }
1004
- catch {
1005
- }
1006
- const metadata = (0, helpers_1.parseMetadataFromLogs)(lastLogOutput);
1056
+ const result = await (0, operations_1.handleMetadata)(handlerParams);
1007
1057
  returnData.push({
1008
1058
  json: {
1009
1059
  ...items[i].json,
1010
- ffmpeg: { operation, ...metadata },
1060
+ ffmpeg: { operation, ...result.metadata },
1011
1061
  },
1012
1062
  });
1013
- try {
1014
- ffmpeg.FS("unlink", inputFilename);
1015
- }
1016
- catch { }
1063
+ safeUnlink(ffmpeg, inputFilename);
1064
+ for (const f of result.tempFiles)
1065
+ safeUnlink(ffmpeg, f);
1017
1066
  continue;
1018
1067
  }
1019
1068
  const outputBinaryPropertyName = this.getNodeParameter("outputBinaryPropertyName", i);
1020
- switch (operation) {
1021
- case "convert": {
1022
- const outputFormat = this.getNodeParameter("outputFormat", i);
1023
- const videoCodec = this.getNodeParameter("videoCodec", i);
1024
- const audioCodec = this.getNodeParameter("audioCodec", i);
1025
- const crf = this.getNodeParameter("crf", i);
1026
- outputExt = (0, helpers_1.normalizeExtension)(outputFormat);
1027
- const outputName = `${outputFilename}${outputExt}`;
1028
- ffmpegCommand = ["-i", inputFilename];
1029
- if (videoCodec !== "auto") {
1030
- ffmpegCommand.push("-c:v", videoCodec);
1031
- }
1032
- if (audioCodec !== "auto") {
1033
- ffmpegCommand.push("-c:a", audioCodec);
1034
- }
1035
- if (crf >= 0) {
1036
- ffmpegCommand.push("-crf", crf.toString());
1037
- }
1038
- ffmpegCommand.push("-y", outputName);
1039
- break;
1040
- }
1041
- case "extractAudio": {
1042
- const audioFormat = this.getNodeParameter("audioFormat", i);
1043
- const audioQuality = this.getNodeParameter("audioQuality", i);
1044
- outputExt = `.${audioFormat}`;
1045
- const outputName = `${outputFilename}${outputExt}`;
1046
- ffmpegCommand = [
1047
- "-i",
1048
- inputFilename,
1049
- "-vn",
1050
- "-ar",
1051
- "44100",
1052
- "-ac",
1053
- "2",
1054
- "-b:a",
1055
- audioQuality,
1056
- "-y",
1057
- outputName,
1058
- ];
1059
- break;
1060
- }
1061
- case "resize": {
1062
- const width = this.getNodeParameter("width", i);
1063
- const height = this.getNodeParameter("height", i);
1064
- const keepAspectRatio = this.getNodeParameter("keepAspectRatio", i);
1065
- outputExt = ".mp4";
1066
- const outputName = `${outputFilename}${outputExt}`;
1067
- const heightStr = keepAspectRatio
1068
- ? "-1"
1069
- : height.toString();
1070
- ffmpegCommand = [
1071
- "-i",
1072
- inputFilename,
1073
- "-vf",
1074
- `scale=${width}:${heightStr}`,
1075
- "-c:a",
1076
- "copy",
1077
- "-y",
1078
- outputName,
1079
- ];
1080
- break;
1081
- }
1082
- case "thumbnail": {
1083
- const timestamp = this.getNodeParameter("timestamp", i);
1084
- const thumbnailWidth = this.getNodeParameter("thumbnailWidth", i);
1085
- const thumbnailHeight = this.getNodeParameter("thumbnailHeight", i);
1086
- outputExt = ".jpg";
1087
- const outputName = `${outputFilename}${outputExt}`;
1088
- ffmpegCommand = [
1089
- "-ss",
1090
- timestamp,
1091
- "-i",
1092
- inputFilename,
1093
- "-vframes",
1094
- "1",
1095
- "-q:v",
1096
- "2",
1097
- "-vf",
1098
- `scale=${thumbnailWidth}:${thumbnailHeight}`,
1099
- "-y",
1100
- outputName,
1101
- ];
1102
- break;
1103
- }
1104
- case "custom": {
1105
- const ffmpegArgs = this.getNodeParameter("ffmpegArgs", i);
1106
- const outputExtension = this.getNodeParameter("outputExtension", i);
1107
- outputExt = (0, helpers_1.normalizeExtension)(outputExtension);
1108
- const outputName = `${outputFilename}${outputExt}`;
1109
- const argsString = ffmpegArgs
1110
- .replace(/\binput\b/g, inputFilename)
1111
- .replace(/\boutput\b/g, outputName);
1112
- ffmpegCommand = argsString
1113
- .split(/\s+/)
1114
- .filter((arg) => arg.length > 0);
1115
- break;
1116
- }
1117
- case "merge": {
1118
- const videoBinaryProperties = this.getNodeParameter("videoBinaryProperties", i);
1119
- const mergeOutputFormat = this.getNodeParameter("mergeOutputFormat", i);
1120
- const addTransition = this.getNodeParameter("addTransition", i);
1121
- const binaryProps = videoBinaryProperties
1122
- .split(",")
1123
- .map((p) => p.trim())
1124
- .filter((p) => p.length > 0);
1125
- if (binaryProps.length < 2) {
1126
- throw new Error("At least 2 video binary properties are required for merging");
1127
- }
1128
- const inputFiles = [];
1129
- for (let j = 0; j < binaryProps.length; j++) {
1130
- const propName = binaryProps[j];
1131
- const propBinary = items[i].binary?.[propName];
1132
- const ext = propBinary
1133
- ? (0, helpers_1.getInputExtension)(propBinary)
1134
- : ".mp4";
1135
- const videoData = await this.helpers.getBinaryDataBuffer(i, propName);
1136
- const inputName = `input_${i}_${j}_${Date.now()}${ext}`;
1137
- ffmpeg.FS("writeFile", inputName, new Uint8Array(videoData));
1138
- inputFiles.push(inputName);
1139
- }
1140
- const concatList = inputFiles
1141
- .map((f) => `file '${f}'`)
1142
- .join("\n");
1143
- const listFilename = `list_${i}_${Date.now()}.txt`;
1144
- ffmpeg.FS("writeFile", listFilename, new TextEncoder().encode(concatList));
1145
- outputExt = (0, helpers_1.normalizeExtension)(mergeOutputFormat);
1146
- const outputName = `${outputFilename}${outputExt}`;
1147
- if (addTransition) {
1148
- ffmpegCommand = [
1149
- "-f",
1150
- "concat",
1151
- "-safe",
1152
- "0",
1153
- "-i",
1154
- listFilename,
1155
- "-vf",
1156
- "fade=in:st=0:d=0.5,format=yuv420p",
1157
- "-c:v",
1158
- "libx264",
1159
- "-preset",
1160
- "fast",
1161
- "-y",
1162
- outputName,
1163
- ];
1164
- }
1165
- else {
1166
- ffmpegCommand = [
1167
- "-f",
1168
- "concat",
1169
- "-safe",
1170
- "0",
1171
- "-i",
1172
- listFilename,
1173
- "-c",
1174
- "copy",
1175
- "-y",
1176
- outputName,
1177
- ];
1178
- }
1179
- break;
1180
- }
1181
- case "trim": {
1182
- const startTime = this.getNodeParameter("startTime", i);
1183
- const endTime = this.getNodeParameter("endTime", i);
1184
- const duration = this.getNodeParameter("duration", i);
1185
- outputExt = ".mp4";
1186
- const outputName = `${outputFilename}${outputExt}`;
1187
- ffmpegCommand = ["-i", inputFilename, "-ss", startTime];
1188
- if (duration) {
1189
- ffmpegCommand.push("-t", duration);
1190
- }
1191
- else if (endTime) {
1192
- ffmpegCommand.push("-to", endTime);
1193
- }
1194
- ffmpegCommand.push("-c", "copy", "-y", outputName);
1195
- break;
1196
- }
1197
- case "videoFilters": {
1198
- const brightness = this.getNodeParameter("brightness", i);
1199
- const contrast = this.getNodeParameter("contrast", i);
1200
- const saturation = this.getNodeParameter("saturation", i);
1201
- const blur = this.getNodeParameter("blur", i);
1202
- const grayscale = this.getNodeParameter("grayscale", i);
1203
- const sepia = this.getNodeParameter("sepia", i);
1204
- const filtersOutputFormat = this.getNodeParameter("filtersOutputFormat", i);
1205
- const vfFilters = [];
1206
- const eqParts = [];
1207
- if (brightness !== 0)
1208
- eqParts.push(`brightness=${brightness}`);
1209
- if (contrast !== 1)
1210
- eqParts.push(`contrast=${contrast}`);
1211
- if (saturation !== 1)
1212
- eqParts.push(`saturation=${saturation}`);
1213
- if (eqParts.length > 0) {
1214
- vfFilters.push(`eq=${eqParts.join(":")}`);
1215
- }
1216
- if (blur > 0) {
1217
- vfFilters.push(`gblur=sigma=${blur}`);
1218
- }
1219
- if (grayscale) {
1220
- vfFilters.push("format=gray");
1221
- }
1222
- if (sepia) {
1223
- vfFilters.push("colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131");
1224
- }
1225
- outputExt = (0, helpers_1.normalizeExtension)(filtersOutputFormat);
1226
- const outputName = `${outputFilename}${outputExt}`;
1227
- if (vfFilters.length > 0) {
1228
- ffmpegCommand = [
1229
- "-i",
1230
- inputFilename,
1231
- "-vf",
1232
- vfFilters.join(","),
1233
- "-c:a",
1234
- "copy",
1235
- "-y",
1236
- outputName,
1237
- ];
1238
- }
1239
- else {
1240
- ffmpegCommand = [
1241
- "-i",
1242
- inputFilename,
1243
- "-c",
1244
- "copy",
1245
- "-y",
1246
- outputName,
1247
- ];
1248
- }
1249
- break;
1250
- }
1251
- case "speed": {
1252
- const speedValue = this.getNodeParameter("speedValue", i);
1253
- const adjustAudioPitch = this.getNodeParameter("adjustAudioPitch", i);
1254
- const speedOutputFormat = this.getNodeParameter("speedOutputFormat", i);
1255
- const speed = parseFloat(speedValue);
1256
- outputExt = (0, helpers_1.normalizeExtension)(speedOutputFormat);
1257
- const outputName = `${outputFilename}${outputExt}`;
1258
- const videoFilter = `setpts=${1 / speed}*PTS`;
1259
- if (adjustAudioPitch) {
1260
- const audioFilter = (0, helpers_1.buildAtempoFilter)(speed);
1261
- ffmpegCommand = [
1262
- "-i",
1263
- inputFilename,
1264
- "-vf",
1265
- videoFilter,
1266
- "-af",
1267
- audioFilter,
1268
- "-y",
1269
- outputName,
1270
- ];
1271
- }
1272
- else {
1273
- ffmpegCommand = [
1274
- "-i",
1275
- inputFilename,
1276
- "-vf",
1277
- videoFilter,
1278
- "-an",
1279
- "-y",
1280
- outputName,
1281
- ];
1282
- }
1283
- break;
1284
- }
1285
- case "rotate": {
1286
- const rotation = this.getNodeParameter("rotation", i);
1287
- const flipHorizontal = this.getNodeParameter("flipHorizontal", i);
1288
- const flipVertical = this.getNodeParameter("flipVertical", i);
1289
- const rotateOutputFormat = this.getNodeParameter("rotateOutputFormat", i);
1290
- const vfParts = [];
1291
- switch (rotation) {
1292
- case "90":
1293
- vfParts.push("transpose=1");
1294
- break;
1295
- case "270":
1296
- vfParts.push("transpose=2");
1297
- break;
1298
- case "180":
1299
- vfParts.push("transpose=1");
1300
- vfParts.push("transpose=1");
1301
- break;
1302
- case "0":
1303
- default:
1304
- break;
1305
- }
1306
- if (flipHorizontal) {
1307
- vfParts.push("hflip");
1308
- }
1309
- if (flipVertical) {
1310
- vfParts.push("vflip");
1311
- }
1312
- outputExt = (0, helpers_1.normalizeExtension)(rotateOutputFormat);
1313
- const outputName = `${outputFilename}${outputExt}`;
1314
- if (vfParts.length > 0) {
1315
- ffmpegCommand = [
1316
- "-i",
1317
- inputFilename,
1318
- "-vf",
1319
- vfParts.join(","),
1320
- "-c:a",
1321
- "copy",
1322
- "-y",
1323
- outputName,
1324
- ];
1325
- }
1326
- else {
1327
- ffmpegCommand = [
1328
- "-i",
1329
- inputFilename,
1330
- "-c",
1331
- "copy",
1332
- "-y",
1333
- outputName,
1334
- ];
1335
- }
1336
- break;
1337
- }
1338
- case "audioMix": {
1339
- const audioBinaryProperties = this.getNodeParameter("audioBinaryProperties", i);
1340
- const audioMixOutputFormat = this.getNodeParameter("audioMixOutputFormat", i);
1341
- const binaryProps = audioBinaryProperties
1342
- .split(",")
1343
- .map((p) => p.trim())
1344
- .filter((p) => p.length > 0);
1345
- if (binaryProps.length < 2) {
1346
- throw new Error("At least 2 audio binary properties are required for mixing");
1347
- }
1348
- const inputFiles = [];
1349
- for (let j = 0; j < binaryProps.length; j++) {
1350
- const propName = binaryProps[j];
1351
- const propBinary = items[i].binary?.[propName];
1352
- const ext = propBinary
1353
- ? (0, helpers_1.getInputExtension)(propBinary)
1354
- : ".wav";
1355
- const audioData = await this.helpers.getBinaryDataBuffer(i, propName);
1356
- const inputName = `audio_input_${i}_${j}_${Date.now()}${ext}`;
1357
- ffmpeg.FS("writeFile", inputName, new Uint8Array(audioData));
1358
- inputFiles.push(inputName);
1359
- }
1360
- outputExt = (0, helpers_1.normalizeExtension)(audioMixOutputFormat);
1361
- const outputName = `${outputFilename}${outputExt}`;
1362
- const filterComplex = inputFiles.map((_f, idx) => `[${idx}:a]`).join("") +
1363
- `amix=inputs=${inputFiles.length}:duration=longest[aout]`;
1364
- const inputs = [];
1365
- for (const f of inputFiles) {
1366
- inputs.push("-i", f);
1367
- }
1368
- ffmpegCommand = [
1369
- ...inputs,
1370
- "-filter_complex",
1371
- filterComplex,
1372
- "-map",
1373
- "[aout]",
1374
- "-y",
1375
- outputName,
1376
- ];
1377
- break;
1378
- }
1379
- case "audioFilters": {
1380
- const volume = this.getNodeParameter("volume", i);
1381
- const bassBoost = this.getNodeParameter("bassBoost", i);
1382
- const trebleBoost = this.getNodeParameter("trebleBoost", i);
1383
- const highPass = this.getNodeParameter("highPass", i);
1384
- const lowPass = this.getNodeParameter("lowPass", i);
1385
- const audioFiltersOutputFormat = this.getNodeParameter("audioFiltersOutputFormat", i);
1386
- const afFilters = [];
1387
- if (volume !== 1.0)
1388
- afFilters.push(`volume=${volume}`);
1389
- if (bassBoost > 0)
1390
- afFilters.push(`bass=g=${bassBoost}`);
1391
- if (trebleBoost > 0)
1392
- afFilters.push(`treble=g=${trebleBoost}`);
1393
- if (highPass > 0)
1394
- afFilters.push(`highpass=f=${highPass}`);
1395
- if (lowPass > 0)
1396
- afFilters.push(`lowpass=f=${lowPass}`);
1397
- outputExt = (0, helpers_1.normalizeExtension)(audioFiltersOutputFormat);
1398
- const outputName = `${outputFilename}${outputExt}`;
1399
- if (afFilters.length > 0) {
1400
- ffmpegCommand = [
1401
- "-i",
1402
- inputFilename,
1403
- "-af",
1404
- afFilters.join(","),
1405
- "-y",
1406
- outputName,
1407
- ];
1408
- }
1409
- else {
1410
- ffmpegCommand = [
1411
- "-i",
1412
- inputFilename,
1413
- "-c:a",
1414
- "copy",
1415
- "-y",
1416
- outputName,
1417
- ];
1418
- }
1419
- break;
1420
- }
1421
- case "audioNormalize": {
1422
- const targetLoudness = this.getNodeParameter("targetLoudness", i);
1423
- const truePeak = this.getNodeParameter("truePeak", i);
1424
- const audioNormalizeOutputFormat = this.getNodeParameter("audioNormalizeOutputFormat", i);
1425
- outputExt = (0, helpers_1.normalizeExtension)(audioNormalizeOutputFormat);
1426
- const outputName = `${outputFilename}${outputExt}`;
1427
- ffmpegCommand = [
1428
- "-i",
1429
- inputFilename,
1430
- "-af",
1431
- `loudnorm=I=${targetLoudness}:TP=${truePeak}:LRA=11`,
1432
- "-y",
1433
- outputName,
1434
- ];
1435
- break;
1436
- }
1437
- case "overlay": {
1438
- const overlayBinaryProperty = this.getNodeParameter("overlayBinaryProperty", i);
1439
- const overlayType = this.getNodeParameter("overlayType", i);
1440
- const overlayX = this.getNodeParameter("overlayX", i);
1441
- const overlayY = this.getNodeParameter("overlayY", i);
1442
- const overlayWidth = this.getNodeParameter("overlayWidth", i);
1443
- const overlayHeight = this.getNodeParameter("overlayHeight", i);
1444
- const overlayOpacity = this.getNodeParameter("overlayOpacity", i);
1445
- const overlayOutputFormat = this.getNodeParameter("overlayOutputFormat", i);
1446
- const overlayBin = items[i].binary?.[overlayBinaryProperty];
1447
- const overlayExt = overlayBin
1448
- ? (0, helpers_1.getInputExtension)(overlayBin)
1449
- : ".png";
1450
- const overlayData = await this.helpers.getBinaryDataBuffer(i, overlayBinaryProperty);
1451
- const overlayFilename = `overlay_${i}_${Date.now()}${overlayExt}`;
1452
- ffmpeg.FS("writeFile", overlayFilename, new Uint8Array(overlayData));
1453
- outputExt = (0, helpers_1.normalizeExtension)(overlayOutputFormat);
1454
- const outputName = `${outputFilename}${outputExt}`;
1455
- let videoFilter = "";
1456
- if (overlayWidth > 0 || overlayHeight > 0) {
1457
- const wStr = overlayWidth > 0
1458
- ? overlayWidth.toString()
1459
- : "-1";
1460
- const hStr = overlayHeight > 0
1461
- ? overlayHeight.toString()
1462
- : "-1";
1463
- videoFilter = `[1:v]scale=${wStr}:${hStr}`;
1464
- if (overlayOpacity < 1.0) {
1465
- videoFilter += `,format=rgba,colorchannelmixer=aa=${overlayOpacity}`;
1466
- }
1467
- videoFilter += `[ovrl];[0:v][ovrl]overlay=${overlayX}:${overlayY}[outv]`;
1468
- }
1469
- else {
1470
- if (overlayOpacity < 1.0) {
1471
- videoFilter = `[1:v]format=rgba,colorchannelmixer=aa=${overlayOpacity}[ovrl];[0:v][ovrl]overlay=${overlayX}:${overlayY}[outv]`;
1472
- }
1473
- else {
1474
- videoFilter = `[0:v][1:v]overlay=${overlayX}:${overlayY}[outv]`;
1475
- }
1476
- }
1477
- if (overlayType === "pip") {
1478
- const fullFilter = `${videoFilter};[0:a][1:a]amix=inputs=2:duration=first[outa]`;
1479
- ffmpegCommand = [
1480
- "-i",
1481
- inputFilename,
1482
- "-i",
1483
- overlayFilename,
1484
- "-filter_complex",
1485
- fullFilter,
1486
- "-map",
1487
- "[outv]",
1488
- "-map",
1489
- "[outa]",
1490
- "-y",
1491
- outputName,
1492
- ];
1493
- }
1494
- else {
1495
- ffmpegCommand = [
1496
- "-i",
1497
- inputFilename,
1498
- "-i",
1499
- overlayFilename,
1500
- "-filter_complex",
1501
- videoFilter,
1502
- "-map",
1503
- "[outv]",
1504
- "-map",
1505
- "0:a?",
1506
- "-c:a",
1507
- "copy",
1508
- "-y",
1509
- outputName,
1510
- ];
1511
- }
1512
- break;
1513
- }
1514
- case "subtitle": {
1515
- const subtitleBinaryProperty = this.getNodeParameter("subtitleBinaryProperty", i);
1516
- const subtitleFormat = this.getNodeParameter("subtitleFormat", i);
1517
- const subtitleFontSize = this.getNodeParameter("subtitleFontSize", i);
1518
- const subtitleFontColor = this.getNodeParameter("subtitleFontColor", i);
1519
- const subtitleBgOpacity = this.getNodeParameter("subtitleBgOpacity", i);
1520
- const subtitlePosition = this.getNodeParameter("subtitlePosition", i);
1521
- const subtitleOutputFormat = this.getNodeParameter("subtitleOutputFormat", i);
1522
- const subtitleData = await this.helpers.getBinaryDataBuffer(i, subtitleBinaryProperty);
1523
- const subtitleFilename = `subtitle_${i}_${Date.now()}.${subtitleFormat}`;
1524
- ffmpeg.FS("writeFile", subtitleFilename, new Uint8Array(subtitleData));
1525
- outputExt = (0, helpers_1.normalizeExtension)(subtitleOutputFormat);
1526
- const outputName = `${outputFilename}${outputExt}`;
1527
- const assColor = (0, helpers_1.colorToAssFormat)(subtitleFontColor);
1528
- let alignment = "2";
1529
- if (subtitlePosition === "top") {
1530
- alignment = "6";
1531
- }
1532
- else if (subtitlePosition === "center") {
1533
- alignment = "5";
1534
- }
1535
- let subtitleFilter = "";
1536
- if (subtitleBgOpacity > 0) {
1537
- const alphaHex = Math.round(subtitleBgOpacity * 255)
1538
- .toString(16)
1539
- .padStart(2, "0")
1540
- .toUpperCase();
1541
- subtitleFilter = `subtitles=${subtitleFilename}:force_style='FontSize=${subtitleFontSize},PrimaryColour=${assColor},Alignment=${alignment},OutlineColour=&H00000000&,Outline=1,BorderStyle=4,BackColour=&H${alphaHex}000000&'`;
1542
- }
1543
- else {
1544
- subtitleFilter = `subtitles=${subtitleFilename}:force_style='FontSize=${subtitleFontSize},PrimaryColour=${assColor},Alignment=${alignment}'`;
1545
- }
1546
- ffmpegCommand = [
1547
- "-i",
1548
- inputFilename,
1549
- "-vf",
1550
- subtitleFilter,
1551
- "-c:a",
1552
- "copy",
1553
- "-y",
1554
- outputName,
1555
- ];
1556
- break;
1557
- }
1558
- case "gif": {
1559
- const gifOutputFormat = this.getNodeParameter("gifOutputFormat", i);
1560
- const gifWidth = this.getNodeParameter("gifWidth", i);
1561
- const gifHeight = this.getNodeParameter("gifHeight", i);
1562
- const gifFps = this.getNodeParameter("gifFps", i);
1563
- const gifStartTime = this.getNodeParameter("gifStartTime", i);
1564
- const gifDuration = this.getNodeParameter("gifDuration", i);
1565
- const gifColors = this.getNodeParameter("gifColors", i);
1566
- const gifDither = this.getNodeParameter("gifDither", i);
1567
- const gifLoop = this.getNodeParameter("gifLoop", i);
1568
- outputExt = `.${gifOutputFormat}`;
1569
- const outputName = `${outputFilename}${outputExt}`;
1570
- const wStr = gifWidth > 0 ? gifWidth.toString() : "-1";
1571
- const hStr = gifHeight > 0 ? gifHeight.toString() : "-1";
1572
- const scaleFilter = `fps=${gifFps},scale=${wStr}:${hStr}:flags=lanczos`;
1573
- if (gifOutputFormat === "gif") {
1574
- const loopValue = gifLoop ? "0" : "-1";
1575
- const gifFilter = `[0:v]${scaleFilter},split[s0][s1];[s0]palettegen=max_colors=${gifColors}[p];[s1][p]paletteuse=dither=${gifDither}`;
1576
- ffmpegCommand = [
1577
- "-ss",
1578
- gifStartTime,
1579
- "-t",
1580
- gifDuration,
1581
- "-i",
1582
- inputFilename,
1583
- "-filter_complex",
1584
- gifFilter,
1585
- "-loop",
1586
- loopValue,
1587
- "-y",
1588
- outputName,
1589
- ];
1590
- }
1591
- else {
1592
- const loopValue = gifLoop ? "0" : "1";
1593
- ffmpegCommand = [
1594
- "-ss",
1595
- gifStartTime,
1596
- "-t",
1597
- gifDuration,
1598
- "-i",
1599
- inputFilename,
1600
- "-vf",
1601
- scaleFilter,
1602
- "-loop",
1603
- loopValue,
1604
- "-y",
1605
- outputName,
1606
- ];
1607
- }
1608
- break;
1609
- }
1610
- case "imageSequence": {
1611
- const sequenceOutputFormat = this.getNodeParameter("sequenceOutputFormat", i);
1612
- const sequenceWidth = this.getNodeParameter("sequenceWidth", i);
1613
- const sequenceHeight = this.getNodeParameter("sequenceHeight", i);
1614
- const sequenceFps = this.getNodeParameter("sequenceFps", i);
1615
- const sequenceStartTime = this.getNodeParameter("sequenceStartTime", i);
1616
- const sequenceDuration = this.getNodeParameter("sequenceDuration", i);
1617
- const sequenceQuality = this.getNodeParameter("sequenceQuality", i);
1618
- const outputPattern = `frame_%04d.${sequenceOutputFormat}`;
1619
- const wStr = sequenceWidth > 0
1620
- ? sequenceWidth.toString()
1621
- : "-1";
1622
- const hStr = sequenceHeight > 0
1623
- ? sequenceHeight.toString()
1624
- : "-1";
1625
- const vfFilter = `fps=1/${sequenceFps},scale=${wStr}:${hStr}`;
1626
- const cmdArgs = ["-ss", sequenceStartTime];
1627
- if (sequenceDuration && sequenceDuration.length > 0) {
1628
- cmdArgs.push("-t", sequenceDuration);
1629
- }
1630
- cmdArgs.push("-i", inputFilename, "-vf", vfFilter);
1631
- if (sequenceOutputFormat === "jpg" ||
1632
- sequenceOutputFormat === "jpeg") {
1633
- cmdArgs.push("-q:v", Math.round(((100 - sequenceQuality) / 100) * 31).toString());
1634
- }
1635
- else if (sequenceOutputFormat === "webp") {
1636
- cmdArgs.push("-q:v", sequenceQuality.toString());
1637
- }
1638
- cmdArgs.push("-y", outputPattern);
1639
- ffmpegCommand = cmdArgs;
1640
- const seqTimeoutMs = (additionalOptions.timeout || 300) * 1000;
1641
- lastLogOutput = "";
1642
- await Promise.race([
1643
- ffmpeg.run(...ffmpegCommand),
1644
- new Promise((_, reject) => setTimeout(() => reject(new Error(`FFmpeg timed out after ${additionalOptions.timeout || 300}s`)), seqTimeoutMs)),
1645
- ]);
1646
- const mimeType = (0, helpers_1.getMimeTypeFromExtension)(sequenceOutputFormat);
1647
- let frameIndex = 1;
1648
- while (true) {
1649
- const frameName = `frame_${String(frameIndex).padStart(4, "0")}.${sequenceOutputFormat}`;
1650
- try {
1651
- const frameData = ffmpeg.FS("readFile", frameName);
1652
- returnData.push({
1653
- json: {
1654
- ...items[i].json,
1655
- ffmpeg: {
1656
- operation,
1657
- inputFilename: binaryData.fileName || "input",
1658
- outputFilename: frameName,
1659
- frameIndex,
1660
- size: frameData.length,
1661
- },
1662
- },
1663
- binary: {
1664
- [outputBinaryPropertyName]: {
1665
- data: Buffer.from(frameData).toString("base64"),
1666
- fileName: frameName,
1667
- mimeType,
1668
- },
1669
- },
1670
- });
1671
- try {
1672
- ffmpeg.FS("unlink", frameName);
1673
- }
1674
- catch { }
1675
- frameIndex++;
1676
- }
1677
- catch {
1678
- break;
1679
- }
1680
- }
1681
- try {
1682
- ffmpeg.FS("unlink", inputFilename);
1683
- }
1684
- catch { }
1685
- continue;
1069
+ if (operation === "imageSequence") {
1070
+ const result = await (0, operations_1.handleImageSequence)(handlerParams);
1071
+ const seqTimeoutMs = (additionalOptions.timeout || 300) * 1000;
1072
+ resetLog();
1073
+ let seqTimeoutId;
1074
+ await Promise.race([
1075
+ ffmpeg.run(...result.command),
1076
+ new Promise((_, reject) => {
1077
+ seqTimeoutId = setTimeout(() => reject(new Error(`FFmpeg timed out after ${additionalOptions.timeout || 300}s`)), seqTimeoutMs);
1078
+ }),
1079
+ ]);
1080
+ clearTimeout(seqTimeoutId);
1081
+ const frameResults = (0, operations_1.readImageSequenceFrames)(ffmpeg, result.outputFormat, outputBinaryPropertyName, items[i].json, binaryData.fileName || "input");
1082
+ returnData.push(...frameResults);
1083
+ safeUnlink(ffmpeg, inputFilename);
1084
+ for (const f of result.tempFiles)
1085
+ safeUnlink(ffmpeg, f);
1086
+ continue;
1087
+ }
1088
+ let commandResult;
1089
+ if (operation === "trim") {
1090
+ const startTime = this.getNodeParameter("startTime", i);
1091
+ const endTime = this.getNodeParameter("endTime", i);
1092
+ const duration = this.getNodeParameter("duration", i);
1093
+ const outputExt = ".mp4";
1094
+ const outputName = `${outputFilename}${outputExt}`;
1095
+ const command = ["-i", inputFilename, "-ss", startTime];
1096
+ if (duration) {
1097
+ command.push("-t", duration);
1686
1098
  }
1687
- case "remux": {
1688
- const remuxOutputFormat = this.getNodeParameter("remuxOutputFormat", i);
1689
- outputExt = (0, helpers_1.normalizeExtension)(remuxOutputFormat);
1690
- const outputName = `${outputFilename}${outputExt}`;
1691
- ffmpegCommand = [
1692
- "-i",
1693
- inputFilename,
1694
- "-c",
1695
- "copy",
1696
- "-y",
1697
- outputName,
1698
- ];
1699
- break;
1099
+ else if (endTime) {
1100
+ command.push("-to", endTime);
1700
1101
  }
1701
- case "socialMedia": {
1702
- const presetKey = this.getNodeParameter("socialMediaPreset", i);
1703
- const preset = helpers_1.SOCIAL_MEDIA_PRESETS[presetKey];
1704
- if (!preset) {
1705
- throw new Error(`Unknown social media preset: ${presetKey}`);
1706
- }
1707
- outputExt = ".mp4";
1708
- const outputName = `${outputFilename}${outputExt}`;
1709
- ffmpegCommand = [
1710
- "-i",
1711
- inputFilename,
1712
- "-vf",
1713
- `scale=${preset.width}:${preset.height}:force_original_aspect_ratio=decrease,pad=${preset.width}:${preset.height}:(ow-iw)/2:(oh-ih)/2`,
1714
- "-r",
1715
- preset.fps.toString(),
1716
- "-c:v",
1717
- "libx264",
1718
- "-b:v",
1719
- preset.videoBitrate,
1720
- "-c:a",
1721
- "aac",
1722
- "-b:a",
1723
- preset.audioBitrate,
1724
- "-movflags",
1725
- "+faststart",
1726
- ];
1727
- if (preset.maxDuration) {
1728
- ffmpegCommand.push("-t", preset.maxDuration.toString());
1729
- }
1730
- ffmpegCommand.push("-y", outputName);
1731
- break;
1102
+ command.push("-c", "copy", "-y", outputName);
1103
+ commandResult = { command, outputExt, tempFiles: [] };
1104
+ }
1105
+ else {
1106
+ const handler = OPERATION_HANDLERS[operation];
1107
+ if (!handler) {
1108
+ throw new Error(`Unknown operation: ${operation}`);
1732
1109
  }
1733
- case "compressToSize": {
1734
- const targetSizeMB = this.getNodeParameter("targetSizeMB", i);
1735
- const compressAudioBitrate = this.getNodeParameter("compressAudioBitrate", i);
1736
- const compressOutputFormat = this.getNodeParameter("compressOutputFormat", i);
1737
- outputExt = (0, helpers_1.normalizeExtension)(compressOutputFormat);
1738
- const outputName = `${outputFilename}${outputExt}`;
1739
- lastLogOutput = "";
1740
- try {
1741
- await ffmpeg.run("-i", inputFilename, "-hide_banner");
1742
- }
1743
- catch {
1744
- }
1745
- const meta = (0, helpers_1.parseMetadataFromLogs)(lastLogOutput);
1746
- const durationSec = meta.durationSeconds || 60;
1747
- const audioBitrateKbps = parseInt(compressAudioBitrate) || 128;
1748
- const targetBitsPerSec = (targetSizeMB * 8 * 1024 * 1024) / durationSec;
1749
- const videoBitrate = Math.max(100, Math.floor(targetBitsPerSec / 1000 - audioBitrateKbps));
1750
- ffmpegCommand = [
1751
- "-i",
1752
- inputFilename,
1753
- "-c:v",
1754
- "libx264",
1755
- "-b:v",
1756
- `${videoBitrate}k`,
1757
- "-maxrate",
1758
- `${Math.floor(videoBitrate * 1.5)}k`,
1759
- "-bufsize",
1760
- `${videoBitrate * 2}k`,
1761
- "-c:a",
1762
- "aac",
1763
- "-b:a",
1764
- compressAudioBitrate,
1765
- "-movflags",
1766
- "+faststart",
1767
- "-y",
1768
- outputName,
1769
- ];
1770
- break;
1110
+ commandResult = await handler(handlerParams);
1111
+ }
1112
+ if (commandResult.outputExt === ".mp4" &&
1113
+ !commandResult.command.includes("-movflags")) {
1114
+ const yIdx = commandResult.command.indexOf("-y");
1115
+ if (yIdx !== -1) {
1116
+ commandResult.command.splice(yIdx, 0, "-movflags", "+faststart");
1771
1117
  }
1772
- default:
1773
- throw new Error(`Unknown operation: ${operation}`);
1774
1118
  }
1775
1119
  const timeoutMs = (additionalOptions.timeout || 300) * 1000;
1776
- lastLogOutput = "";
1120
+ resetLog();
1121
+ let timeoutId;
1777
1122
  await Promise.race([
1778
- ffmpeg.run(...ffmpegCommand),
1779
- new Promise((_, reject) => setTimeout(() => reject(new Error(`FFmpeg timed out after ${additionalOptions.timeout || 300}s`)), timeoutMs)),
1123
+ ffmpeg.run(...commandResult.command),
1124
+ new Promise((_, reject) => {
1125
+ timeoutId = setTimeout(() => reject(new Error(`FFmpeg timed out after ${additionalOptions.timeout || 300}s`)), timeoutMs);
1126
+ }),
1780
1127
  ]);
1781
- const outputName = `${outputFilename}${outputExt}`;
1128
+ clearTimeout(timeoutId);
1129
+ const outputName = `${outputFilename}${commandResult.outputExt}`;
1782
1130
  const outputData = ffmpeg.FS("readFile", outputName);
1783
- const outputMimeType = (0, helpers_1.getMimeTypeFromExtension)(outputExt.replace(".", ""));
1131
+ const outputMimeType = (0, helpers_1.getMimeTypeFromExtension)(commandResult.outputExt.replace(".", ""));
1784
1132
  const outputItem = {
1785
1133
  json: {
1786
1134
  ...items[i].json,
@@ -1800,11 +1148,10 @@ class FFmpegWasm {
1800
1148
  },
1801
1149
  };
1802
1150
  returnData.push(outputItem);
1803
- try {
1804
- ffmpeg.FS("unlink", inputFilename);
1805
- ffmpeg.FS("unlink", outputName);
1806
- }
1807
- catch { }
1151
+ safeUnlink(ffmpeg, inputFilename);
1152
+ safeUnlink(ffmpeg, outputName);
1153
+ for (const f of commandResult.tempFiles)
1154
+ safeUnlink(ffmpeg, f);
1808
1155
  }
1809
1156
  catch (error) {
1810
1157
  if (this.continueOnFail()) {
@@ -1826,9 +1173,40 @@ class FFmpegWasm {
1826
1173
  }
1827
1174
  }
1828
1175
  finally {
1829
- ffmpeg.exit();
1176
+ if (ffmpeg.isLoaded()) {
1177
+ try {
1178
+ ffmpeg.exit();
1179
+ }
1180
+ catch { }
1181
+ }
1830
1182
  }
1831
1183
  return [returnData];
1832
1184
  }
1833
1185
  }
1834
1186
  exports.FFmpegWasm = FFmpegWasm;
1187
+ function safeUnlink(ffmpeg, filename) {
1188
+ try {
1189
+ ffmpeg.FS("unlink", filename);
1190
+ }
1191
+ catch { }
1192
+ }
1193
+ const OPERATION_HANDLERS = {
1194
+ convert: operations_1.handleConvert,
1195
+ extractAudio: operations_1.handleExtractAudio,
1196
+ resize: operations_1.handleResize,
1197
+ thumbnail: operations_1.handleThumbnail,
1198
+ custom: operations_1.handleCustom,
1199
+ merge: operations_1.handleMerge,
1200
+ videoFilters: operations_1.handleVideoFilters,
1201
+ speed: operations_1.handleSpeed,
1202
+ rotate: operations_1.handleRotate,
1203
+ audioMix: operations_1.handleAudioMix,
1204
+ audioFilters: operations_1.handleAudioFilters,
1205
+ audioNormalize: operations_1.handleAudioNormalize,
1206
+ overlay: operations_1.handleOverlay,
1207
+ subtitle: operations_1.handleSubtitle,
1208
+ gif: operations_1.handleGif,
1209
+ remux: operations_1.handleRemux,
1210
+ socialMedia: operations_1.handleSocialMedia,
1211
+ compressToSize: operations_1.handleCompressToSize,
1212
+ };