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