diff --git a/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderAspectRatio.cs b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderAspectRatio.cs
new file mode 100644
index 00000000..0ea481f7
--- /dev/null
+++ b/VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderAspectRatio.cs
@@ -0,0 +1,229 @@
+namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
+
+///
+/// A node that forces the aspect ratio of a video using FFmpeg.
+/// Provides options to stretch, add black bars (letterbox), crop, or use custom dimensions.
+///
+public class FfmpegBuilderAspectRatio : FfmpegBuilderNode
+{
+ ///
+ public override int Outputs => 2;
+ ///
+ public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/aspect-ratio";
+ ///
+ public override string Icon => "far fa-percent";
+
+ ///
+ /// The desired aspect ratio for the video. Supports standard options like 16:9, 4:3, 21:9, or custom dimensions.
+ ///
+ [Select(nameof(AspectRatioOptions), 1)]
+ public string AspectRatio { get; set; }
+
+ ///
+ /// The mode of adjustment to enforce the aspect ratio. Options include stretching, adding black bars, cropping, or using custom dimensions.
+ ///
+ [Select(nameof(AdjustmentModeOptions), 2)]
+ public string AdjustmentMode { get; set; } = "Stretch";
+
+ ///
+ /// Custom width when using the custom aspect ratio option.
+ /// Used if is set to "Custom".
+ ///
+ [Range(1, int.MaxValue)]
+ [ConditionEquals(nameof(AspectRatio), "Custom")]
+ public int CustomWidth { get; set; }
+
+ ///
+ /// Custom height when using the custom aspect ratio option.
+ /// Used if is set to "Custom".
+ ///
+ [Range(1, int.MaxValue)]
+ [ConditionEquals(nameof(AspectRatio), "Custom")]
+ public int CustomHeight { get; set; }
+
+ ///
+ /// A list of standard aspect ratio options (16:9, 4:3, 21:9, or Custom).
+ ///
+ private static List _AspectRatioOptions;
+ public static List AspectRatioOptions
+ {
+ get
+ {
+ if (_AspectRatioOptions == null)
+ {
+ _AspectRatioOptions = new List
+ {
+ new() { Value = "16:9", Label = "16:9" },
+ new() { Value = "4:3", Label = "4:3" },
+ new() { Value = "21:9", Label = "21:9" },
+ new() { Value = "Custom", Label = "Custom" }
+ };
+ }
+ return _AspectRatioOptions;
+ }
+ }
+
+ ///
+ /// A list of adjustment modes: Stretch, Add Black Bars (Letterbox), Crop, or Custom.
+ ///
+ private static List _AdjustmentModeOptions;
+ public static List AdjustmentModeOptions
+ {
+ get
+ {
+ if (_AdjustmentModeOptions == null)
+ {
+ _AdjustmentModeOptions = new List
+ {
+ new() { Value = "Stretch", Label = "Stretch to Fit" },
+ new() { Value = "AddBlackBars", Label = "Add Black Bars (Letterbox)" },
+ new() { Value = "Crop", Label = "Crop to Fit" },
+ };
+ }
+ return _AdjustmentModeOptions;
+ }
+ }
+
+ ///
+ /// Executes the FFmpeg aspect ratio adjustment based on the selected aspect ratio and adjustment mode.
+ ///
+ /// The node parameters passed to the FFmpeg builder.
+ ///
+ /// 1 if the aspect ratio was changed successfully,
+ /// 2 if the video was already in the desired aspect ratio and no change was needed,
+ /// -1 if there was an error.
+ ///
+ public override int Execute(NodeParameters args)
+ {
+ var videoInfo = GetVideoInfo(args);
+ if (videoInfo == null || videoInfo.VideoStreams?.Any() != true)
+ {
+ args.FailureReason = "No valid video stream found.";
+ args.Logger?.ELog(args.FailureReason);
+ return -1;
+ }
+
+ int videoWidth = videoInfo.VideoStreams[0].Width;
+ int videoHeight = videoInfo.VideoStreams[0].Height;
+ double currentAspectRatio = (double)videoWidth / videoHeight;
+
+ args.Logger?.ILog($"Current video dimensions: {videoWidth}x{videoHeight}, Aspect Ratio: {currentAspectRatio:F2}");
+
+ // Check for custom aspect ratio
+ double targetAspectRatio;
+ if (AspectRatio == "Custom")
+ {
+ targetAspectRatio = (double)CustomWidth / CustomHeight;
+ args.Logger?.ILog($"Target custom aspect ratio: {CustomWidth}:{CustomHeight} = {targetAspectRatio:F2}");
+
+ if (IsAlreadyDesiredAspectRatio(currentAspectRatio, targetAspectRatio))
+ {
+ args.Logger?.ILog("The video is already in the desired custom aspect ratio.");
+ return 2;
+ }
+ }
+ else
+ {
+ string[] aspectParts = AspectRatio.Split(':');
+ targetAspectRatio = (double)int.Parse(aspectParts[0]) / int.Parse(aspectParts[1]);
+ args.Logger?.ILog($"Target aspect ratio: {AspectRatio} = {targetAspectRatio:F2}");
+
+ // Check if the video is already in the desired aspect ratio
+ if (IsAlreadyDesiredAspectRatio(currentAspectRatio, targetAspectRatio))
+ {
+ args.Logger?.ILog("The video is already in the desired aspect ratio.");
+ return 2;
+ }
+ }
+
+ // Generate the filter based on adjustment mode
+ string filter = AdjustmentMode switch
+ {
+ "Stretch" => $"scale={videoWidth}:{(int)(videoWidth / targetAspectRatio)}:flags=lanczos",
+ "AddBlackBars" => AddBlackBars(videoWidth, videoHeight, targetAspectRatio),
+ "Crop" => CropToFit(videoWidth, videoHeight, targetAspectRatio),
+ _ => null
+ };
+
+ if (filter == null)
+ {
+ args.FailureReason = "Failed to generate a filter based on the adjustment mode.";
+ args.Logger?.ELog(args.FailureReason);
+ return -1;
+ }
+
+ args.Logger?.ILog($"Applying filter: {filter}");
+ Model.VideoStreams[0].Filter.AddRange(new[] { filter });
+ return 1;
+ }
+
+ ///
+ /// Determines if the video is already in the desired aspect ratio within a small tolerance.
+ ///
+ /// The current aspect ratio of the video.
+ /// The desired target aspect ratio.
+ /// True if the video is already within the target aspect ratio tolerance.
+ private bool IsAlreadyDesiredAspectRatio(double currentAspectRatio, double targetAspectRatio)
+ {
+ const double tolerance = 0.01; // 1% tolerance for aspect ratio matching
+ bool result = Math.Abs(currentAspectRatio - targetAspectRatio) < tolerance;
+ Args?.Logger?.ILog($"Checking aspect ratio: current={currentAspectRatio:F2}, target={targetAspectRatio:F2}, result={result}");
+ return result;
+ }
+
+ ///
+ /// Adds black bars to the video (letterbox) to fit the target aspect ratio.
+ ///
+ /// The width of the original video.
+ /// The height of the original video.
+ /// The desired aspect ratio.
+ /// A string representing the FFmpeg filter to add black bars.
+ private string AddBlackBars(int width, int height, double targetAspectRatio)
+ {
+ double currentAspectRatio = (double)width / height;
+ Args?.Logger?.ILog($"Adding black bars: width={width}, height={height}, targetAspectRatio={targetAspectRatio:F2}");
+
+ if (currentAspectRatio > targetAspectRatio)
+ {
+ // Add vertical black bars
+ int paddedHeight = (int)(width / targetAspectRatio);
+ Args?.Logger?.ILog($"Adding vertical black bars, padded height={paddedHeight}");
+ return $"scale={width}:-2, pad={width}:{paddedHeight}:(ow-iw)/2:(oh-ih)/2";
+ }
+ else
+ {
+ // Add horizontal black bars
+ int paddedWidth = (int)(height * targetAspectRatio);
+ Args?.Logger?.ILog($"Adding horizontal black bars, padded width={paddedWidth}");
+ return $"scale=-2:{height}, pad={paddedWidth}:{height}:(ow-iw)/2:(oh-ih)/2";
+ }
+ }
+
+ ///
+ /// Crops the video to fit the desired aspect ratio, discarding part of the frame.
+ ///
+ /// The width of the original video.
+ /// The height of the original video.
+ /// The desired aspect ratio.
+ /// A string representing the FFmpeg filter to crop the video.
+ private string CropToFit(int width, int height, double targetAspectRatio)
+ {
+ double currentAspectRatio = (double)width / height;
+ Args?.Logger?.ILog($"Cropping video: width={width}, height={height}, targetAspectRatio={targetAspectRatio:F2}");
+
+ if (currentAspectRatio > targetAspectRatio)
+ {
+ // Crop width
+ int croppedWidth = (int)(height * targetAspectRatio);
+ Args?.Logger?.ILog($"Cropping width, new width={croppedWidth}");
+ return $"crop={croppedWidth}:{height}";
+ }
+ else
+ {
+ // Crop height
+ int croppedHeight = (int)(width / targetAspectRatio);
+ Args?.Logger?.ILog($"Cropping height, new height={croppedHeight}");
+ return $"crop={width}:{croppedHeight}";
+ }
+ }
+}
diff --git a/VideoNodes/Tests/FfmpegBuilderTests/FFmpegBuilder_AspectRatioTests.cs b/VideoNodes/Tests/FfmpegBuilderTests/FFmpegBuilder_AspectRatioTests.cs
new file mode 100644
index 00000000..d5a24f28
--- /dev/null
+++ b/VideoNodes/Tests/FfmpegBuilderTests/FFmpegBuilder_AspectRatioTests.cs
@@ -0,0 +1,146 @@
+using FileFlows.VideoNodes.FfmpegBuilderNodes;
+using FileFlows.VideoNodes.FfmpegBuilderNodes.Models;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VideoNodes.Tests;
+
+namespace FileFlows.VideoNodes.Tests.FfmpegBuilderTests;
+
+///
+/// Tests for FFmpeg Builder: Aspect Ratio
+///
+[TestClass]
+[TestCategory("Slow")]
+public class FFmpegBuilder_AspectRatioTests : VideoTestBase
+{
+ NodeParameters args;
+
+ ///
+ /// Sets up the test environment before each test.
+ /// Initializes video parameters and executes the video file setup.
+ ///
+ private void InitVideo(string file)
+ {
+ args = GetVideoNodeParameters(file);
+ VideoFile vf = new VideoFile();
+ vf.PreExecute(args);
+ vf.Execute(args);
+
+ FfmpegBuilderStart ffStart = new();
+ ffStart.PreExecute(args);
+ Assert.AreEqual(1, ffStart.Execute(args));
+ }
+
+ ///
+ /// Checks if the video aspect ratio matches the provided width and height ratio.
+ /// Logs the results and asserts conditions.
+ ///
+ /// The expected width ratio.
+ /// The expected height ratio.
+ private void CheckAspectRatio(int expectedWidth, int expectedHeight)
+ {
+ var videoInfo = (VideoInfo)args.Parameters[VideoNode.VIDEO_INFO];
+ Assert.IsNotNull(videoInfo, "Video info is null.");
+
+ var videoStream = videoInfo.VideoStreams[0];
+ int width = videoStream.Width;
+ int height = videoStream.Height;
+
+ double actualAspectRatio = (double)width / height;
+ double expectedAspectRatio = (double)expectedWidth / expectedHeight;
+ Logger.ILog($"Current dimensions: {width}x{height}");
+
+ Logger.ILog($"Current aspect ratio: {actualAspectRatio:F4}, Expected: {expectedAspectRatio:F4}");
+ Assert.AreEqual(expectedAspectRatio, actualAspectRatio, 0.01, "Aspect ratio does not match the expected value.");
+ }
+
+ ///
+ /// Executes the aspect ratio test with the specified parameters.
+ ///
+ /// The desired aspect ratio (e.g., "4:3", "16:9").
+ /// The adjustment mode to use (e.g., "Crop").
+ /// The expected width ratio.
+ /// The expected height ratio.
+ /// The video file to test.
+ private void ExecuteAspectRatioTest(string aspectRatio, string adjustmentMode, int expectedWidth, int expectedHeight, string? file = null)
+ {
+ file ??= Video4by7;
+ InitVideo(file);
+
+ var ffAspectRatio = new FfmpegBuilderAspectRatio
+ {
+ AspectRatio = aspectRatio,
+ AdjustmentMode = adjustmentMode
+ };
+
+ if (aspectRatio == "Custom")
+ {
+ ffAspectRatio.CustomWidth = expectedWidth; // Set custom width
+ ffAspectRatio.CustomHeight = expectedHeight; // Set custom height
+ }
+
+ ffAspectRatio.PreExecute(args);
+ ffAspectRatio.Execute(args);
+
+ var ffExecutor = new FfmpegBuilderExecutor();
+ ffExecutor.PreExecute(args);
+ int result = ffExecutor.Execute(args);
+
+ Assert.AreEqual(1, result);
+ CheckAspectRatio(expectedWidth, expectedHeight);
+ }
+
+ ///
+ /// Test to verify the video scaler with a 4:3 aspect ratio using cropping.
+ ///
+ [TestMethod]
+ public void FfmpegBuilder_AspectRatio_4by3_Crop()
+ {
+ ExecuteAspectRatioTest("4:3", "Crop", 4, 3);
+ }
+
+ ///
+ /// Test to verify the video scaler with a 16:9 aspect ratio using cropping.
+ ///
+ [TestMethod]
+ public void FfmpegBuilder_AspectRatio_16by9_Crop()
+ {
+ ExecuteAspectRatioTest("16:9", "Crop", 16, 9);
+ }
+
+ ///
+ /// Test to verify the video scaler with a 16:9 aspect ratio using stretching.
+ ///
+ [TestMethod]
+ public void FfmpegBuilder_AspectRatio_16by9_Stretch()
+ {
+ ExecuteAspectRatioTest("16:9", "Stretch", 16, 9, file: Video4by3);
+ }
+
+ ///
+ /// Test to verify the video scaler with a 16:9 aspect ratio using black bars.
+ ///
+ [TestMethod]
+ public void FfmpegBuilder_AspectRatio_16by9_AddBlackBars()
+ {
+ ExecuteAspectRatioTest("16:9", "AddBlackBars", 16, 9, file: Video4by3);
+ }
+
+ ///
+ /// Test to verify the video scaler with a 21:9 aspect ratio using cropping.
+ ///
+ [TestMethod]
+ public void FfmpegBuilder_AspectRatio_21by9_Crop()
+ {
+ ExecuteAspectRatioTest("21:9", "Crop", 21, 9);
+ }
+
+ ///
+ /// Test to verify the video scaler with a custom aspect ratio using cropping.
+ /// Modify as needed to fit the custom requirements.
+ ///
+ [TestMethod]
+ public void FfmpegBuilder_AspectRatio_CustomAspectRatio_Crop()
+ {
+ ExecuteAspectRatioTest("Custom", "Crop", 4, 5);
+ }
+}
\ No newline at end of file
diff --git a/VideoNodes/Tests/Resources/video4by3.mp4 b/VideoNodes/Tests/Resources/video4by3.mp4
new file mode 100644
index 00000000..26fb8b24
Binary files /dev/null and b/VideoNodes/Tests/Resources/video4by3.mp4 differ
diff --git a/VideoNodes/Tests/Resources/video4by7.mkv b/VideoNodes/Tests/Resources/video4by7.mkv
new file mode 100644
index 00000000..80d30c56
Binary files /dev/null and b/VideoNodes/Tests/Resources/video4by7.mkv differ
diff --git a/VideoNodes/Tests/_VideoTestBase.cs b/VideoNodes/Tests/_VideoTestBase.cs
index 602b2afe..1e43e7fb 100644
--- a/VideoNodes/Tests/_VideoTestBase.cs
+++ b/VideoNodes/Tests/_VideoTestBase.cs
@@ -31,6 +31,16 @@ public abstract class VideoTestBase : TestBase
///
protected static readonly string VideoMkvHevc = ResourcesTestFilesDir + "/hevc.mkv";
+ ///
+ /// Video 4:7 MKV file
+ ///
+ protected static readonly string Video4by7 = ResourcesTestFilesDir + "/video4by7.mkv";
+
+ ///
+ /// Video 4:3 mp4 file
+ ///
+ protected static readonly string Video4by3 = ResourcesTestFilesDir + "/video4by3.mp4";
+
///
/// Video Corrupt file
///
diff --git a/VideoNodes/i18n/de.json b/VideoNodes/i18n/de.json
index ecc31939..898a3c0a 100644
--- a/VideoNodes/i18n/de.json
+++ b/VideoNodes/i18n/de.json
@@ -118,6 +118,23 @@
"Strictness-Help": "Ermöglicht es Ihnen, die Strenge von FFmpeg einzustellen. Für die meisten Benutzer sollte diese Einstellung auf 'Experimentell' belassen werden."
}
},
+ "FfmpegBuilderAspectRatio": {
+ "Label": "FFMPEG Builder: Seitenverhältnis",
+ "Outputs": {
+ "1": "Seitenverhältnis wurde erfolgreich geändert",
+ "2": "Video war bereits im gewünschten Seitenverhältnis"
+ },
+ "Fields": {
+ "AspectRatio": "Seitenverhältnis",
+ "AspectRatio-Help": "Wählen Sie das gewünschte Seitenverhältnis für das Video aus. Verfügbare Optionen sind Standard-Seitenverhältnisse (16:9, 4:3, 21:9) oder ein benutzerdefiniertes Verhältnis.",
+ "CustomWidth": "Benutzerdefinierte Breite",
+ "CustomWidth-Help": "Geben Sie die Breite an, die verwendet werden soll, wenn die Option für ein benutzerdefiniertes Seitenverhältnis ausgewählt ist.",
+ "CustomHeight": "Benutzerdefinierte Höhe",
+ "CustomHeight-Help": "Geben Sie die Höhe an, die verwendet werden soll, wenn die Option für ein benutzerdefiniertes Seitenverhältnis ausgewählt ist.",
+ "AdjustmentMode": "Anpassungsmodus",
+ "AdjustmentMode-Help": "Wählen Sie, wie das Seitenverhältnis erzwungen werden soll. Optionen umfassen das Strecken des Videos, das Hinzufügen von schwarzen Balken (Letterbox) oder das Zuschneiden des Videos, um das Seitenverhältnis anzupassen."
+ }
+ },
"FfmpegBuilderAddInputFile": {
"Label": "FFMPEG-Builder: Eingabedatei hinzufügen",
"Outputs": {
diff --git a/VideoNodes/i18n/en.json b/VideoNodes/i18n/en.json
index 5b59b04b..79dfca1e 100644
--- a/VideoNodes/i18n/en.json
+++ b/VideoNodes/i18n/en.json
@@ -157,6 +157,23 @@
"UseSourceDirectory-Help": "If checked the original source directory will be searched, otherwise the working directory will be used."
}
},
+ "FfmpegBuilderAspectRatio": {
+ "Label": "FFMPEG Builder: Aspect Ratio",
+ "Outputs": {
+ "1": "Aspect ratio was changed successfully",
+ "2": "Video was already in the desired aspect ratio"
+ },
+ "Fields": {
+ "AspectRatio": "Aspect Ratio",
+ "AspectRatio-Help": "Select the desired aspect ratio for the video. Available options include standard aspect ratios (16:9, 4:3, 21:9) or a custom ratio.",
+ "CustomWidth": "Custom Width",
+ "CustomWidth-Help": "Specify the width to use when selecting the custom aspect ratio option.",
+ "CustomHeight": "Custom Height",
+ "CustomHeight-Help": "Specify the height to use when selecting the custom aspect ratio option.",
+ "AdjustmentMode": "Adjustment Mode",
+ "AdjustmentMode-Help": "Choose how the aspect ratio should be enforced. Options include stretching the video, adding black bars (letterbox), or cropping the video to fit the aspect ratio."
+ }
+ },
"FfmpegBuilderAudioAddTrack": {
"Label": "FFMPEG Builder: Audio Add Track",
"Outputs": {