mirror of
https://github.com/revenz/FileFlowsPlugins.git
synced 2025-12-30 16:59:31 -06:00
FF-1795: Added Aspect Ratio flow element
This commit is contained in:
229
VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderAspectRatio.cs
Normal file
229
VideoNodes/FfmpegBuilderNodes/Video/FfmpegBuilderAspectRatio.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
BIN
VideoNodes/Tests/Resources/video4by3.mp4
Normal file
BIN
VideoNodes/Tests/Resources/video4by3.mp4
Normal file
Binary file not shown.
BIN
VideoNodes/Tests/Resources/video4by7.mkv
Normal file
BIN
VideoNodes/Tests/Resources/video4by7.mkv
Normal file
Binary file not shown.
@@ -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>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user