FF-1795: Added Aspect Ratio flow element

This commit is contained in:
John Andrews
2024-09-22 18:58:28 +12:00
parent e9e0434be3
commit f06dc435a0
7 changed files with 419 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
namespace FileFlows.VideoNodes.FfmpegBuilderNodes;
/// <summary>
/// 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.
/// </summary>
public class FfmpegBuilderAspectRatio : FfmpegBuilderNode
{
/// <inheritdoc />
public override int Outputs => 2;
/// <inheritdoc />
public override string HelpUrl => "https://fileflows.com/docs/plugins/video-nodes/ffmpeg-builder/aspect-ratio";
/// <inheritdoc />
public override string Icon => "far fa-percent";
/// <summary>
/// The desired aspect ratio for the video. Supports standard options like 16:9, 4:3, 21:9, or custom dimensions.
/// </summary>
[Select(nameof(AspectRatioOptions), 1)]
public string AspectRatio { get; set; }
/// <summary>
/// The mode of adjustment to enforce the aspect ratio. Options include stretching, adding black bars, cropping, or using custom dimensions.
/// </summary>
[Select(nameof(AdjustmentModeOptions), 2)]
public string AdjustmentMode { get; set; } = "Stretch";
/// <summary>
/// Custom width when using the custom aspect ratio option.
/// Used if <see cref="AspectRatio"/> is set to "Custom".
/// </summary>
[Range(1, int.MaxValue)]
[ConditionEquals(nameof(AspectRatio), "Custom")]
public int CustomWidth { get; set; }
/// <summary>
/// Custom height when using the custom aspect ratio option.
/// Used if <see cref="AspectRatio"/> is set to "Custom".
/// </summary>
[Range(1, int.MaxValue)]
[ConditionEquals(nameof(AspectRatio), "Custom")]
public int CustomHeight { get; set; }
/// <summary>
/// A list of standard aspect ratio options (16:9, 4:3, 21:9, or Custom).
/// </summary>
private static List<ListOption> _AspectRatioOptions;
public static List<ListOption> AspectRatioOptions
{
get
{
if (_AspectRatioOptions == null)
{
_AspectRatioOptions = new List<ListOption>
{
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;
}
}
/// <summary>
/// A list of adjustment modes: Stretch, Add Black Bars (Letterbox), Crop, or Custom.
/// </summary>
private static List<ListOption> _AdjustmentModeOptions;
public static List<ListOption> AdjustmentModeOptions
{
get
{
if (_AdjustmentModeOptions == null)
{
_AdjustmentModeOptions = new List<ListOption>
{
new() { Value = "Stretch", Label = "Stretch to Fit" },
new() { Value = "AddBlackBars", Label = "Add Black Bars (Letterbox)" },
new() { Value = "Crop", Label = "Crop to Fit" },
};
}
return _AdjustmentModeOptions;
}
}
/// <summary>
/// Executes the FFmpeg aspect ratio adjustment based on the selected aspect ratio and adjustment mode.
/// </summary>
/// <param name="args">The node parameters passed to the FFmpeg builder.</param>
/// <returns>
/// 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.
/// </returns>
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;
}
/// <summary>
/// Determines if the video is already in the desired aspect ratio within a small tolerance.
/// </summary>
/// <param name="currentAspectRatio">The current aspect ratio of the video.</param>
/// <param name="targetAspectRatio">The desired target aspect ratio.</param>
/// <returns>True if the video is already within the target aspect ratio tolerance.</returns>
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;
}
/// <summary>
/// Adds black bars to the video (letterbox) to fit the target aspect ratio.
/// </summary>
/// <param name="width">The width of the original video.</param>
/// <param name="height">The height of the original video.</param>
/// <param name="targetAspectRatio">The desired aspect ratio.</param>
/// <returns>A string representing the FFmpeg filter to add black bars.</returns>
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";
}
}
/// <summary>
/// Crops the video to fit the desired aspect ratio, discarding part of the frame.
/// </summary>
/// <param name="width">The width of the original video.</param>
/// <param name="height">The height of the original video.</param>
/// <param name="targetAspectRatio">The desired aspect ratio.</param>
/// <returns>A string representing the FFmpeg filter to crop the video.</returns>
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}";
}
}
}

View File

@@ -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;
/// <summary>
/// Tests for FFmpeg Builder: Aspect Ratio
/// </summary>
[TestClass]
[TestCategory("Slow")]
public class FFmpegBuilder_AspectRatioTests : VideoTestBase
{
NodeParameters args;
/// <summary>
/// Sets up the test environment before each test.
/// Initializes video parameters and executes the video file setup.
/// </summary>
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));
}
/// <summary>
/// Checks if the video aspect ratio matches the provided width and height ratio.
/// Logs the results and asserts conditions.
/// </summary>
/// <param name="expectedWidth">The expected width ratio.</param>
/// <param name="expectedHeight">The expected height ratio.</param>
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.");
}
/// <summary>
/// Executes the aspect ratio test with the specified parameters.
/// </summary>
/// <param name="aspectRatio">The desired aspect ratio (e.g., "4:3", "16:9").</param>
/// <param name="adjustmentMode">The adjustment mode to use (e.g., "Crop").</param>
/// <param name="expectedWidth">The expected width ratio.</param>
/// <param name="expectedHeight">The expected height ratio.</param>
/// <param name="file">The video file to test.</param>
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);
}
/// <summary>
/// Test to verify the video scaler with a 4:3 aspect ratio using cropping.
/// </summary>
[TestMethod]
public void FfmpegBuilder_AspectRatio_4by3_Crop()
{
ExecuteAspectRatioTest("4:3", "Crop", 4, 3);
}
/// <summary>
/// Test to verify the video scaler with a 16:9 aspect ratio using cropping.
/// </summary>
[TestMethod]
public void FfmpegBuilder_AspectRatio_16by9_Crop()
{
ExecuteAspectRatioTest("16:9", "Crop", 16, 9);
}
/// <summary>
/// Test to verify the video scaler with a 16:9 aspect ratio using stretching.
/// </summary>
[TestMethod]
public void FfmpegBuilder_AspectRatio_16by9_Stretch()
{
ExecuteAspectRatioTest("16:9", "Stretch", 16, 9, file: Video4by3);
}
/// <summary>
/// Test to verify the video scaler with a 16:9 aspect ratio using black bars.
/// </summary>
[TestMethod]
public void FfmpegBuilder_AspectRatio_16by9_AddBlackBars()
{
ExecuteAspectRatioTest("16:9", "AddBlackBars", 16, 9, file: Video4by3);
}
/// <summary>
/// Test to verify the video scaler with a 21:9 aspect ratio using cropping.
/// </summary>
[TestMethod]
public void FfmpegBuilder_AspectRatio_21by9_Crop()
{
ExecuteAspectRatioTest("21:9", "Crop", 21, 9);
}
/// <summary>
/// Test to verify the video scaler with a custom aspect ratio using cropping.
/// Modify as needed to fit the custom requirements.
/// </summary>
[TestMethod]
public void FfmpegBuilder_AspectRatio_CustomAspectRatio_Crop()
{
ExecuteAspectRatioTest("Custom", "Crop", 4, 5);
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -31,6 +31,16 @@ public abstract class VideoTestBase : TestBase
/// </summary>
protected static readonly string VideoMkvHevc = ResourcesTestFilesDir + "/hevc.mkv";
/// <summary>
/// Video 4:7 MKV file
/// </summary>
protected static readonly string Video4by7 = ResourcesTestFilesDir + "/video4by7.mkv";
/// <summary>
/// Video 4:3 mp4 file
/// </summary>
protected static readonly string Video4by3 = ResourcesTestFilesDir + "/video4by3.mp4";
/// <summary>
/// Video Corrupt file
/// </summary>

View File

@@ -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": {

View File

@@ -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": {