Turn the Shorts tab into a step-by-step wizard and port amigadb-short-fx-v4.sh: Video background style, auto/manual crop, source fade-in, and blur/opacity/margin settings.
393 lines
13 KiB
C#
393 lines
13 KiB
C#
using AmiReel.Models;
|
|
using AmiReel.Services;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
namespace AmiReel.Tests.Services;
|
|
|
|
[TestClass]
|
|
public class ShortsPipelineTests
|
|
{
|
|
private readonly List<string> _tempDirectories = [];
|
|
|
|
[TestCleanup]
|
|
public void Cleanup()
|
|
{
|
|
foreach (string directory in _tempDirectories)
|
|
{
|
|
try { Directory.Delete(directory, true); } catch { }
|
|
}
|
|
}
|
|
|
|
private string CreateTempDirectory()
|
|
{
|
|
string directory = Path.Combine(Path.GetTempPath(), "AmiReelTests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(directory);
|
|
_tempDirectories.Add(directory);
|
|
return directory;
|
|
}
|
|
|
|
private static ShortSettings ValidSettings(string input, string font, ShortStyle style = ShortStyle.Crop, string background = "") => new()
|
|
{
|
|
InputFile = input,
|
|
OutputPath = Path.Combine(Path.GetDirectoryName(input) ?? "", "out.mp4"),
|
|
Title = "Test Short",
|
|
Hook = "THIS RAN ON AN AMIGA",
|
|
Website = "AMIGADB.NET",
|
|
Start = "00:00:00",
|
|
DurationSeconds = 30,
|
|
Style = style,
|
|
BackgroundImage = background,
|
|
FontFile = font,
|
|
FfmpegPath = "",
|
|
FfprobePath = "",
|
|
};
|
|
|
|
[TestMethod]
|
|
public void EscapeFfmpegPath_WithBackslashesColonsAndQuotes_EscapesAllThree()
|
|
{
|
|
// Act
|
|
string result = ShortsPipeline.EscapeFfmpegPath(@"C:\Fonts\font's.ttf");
|
|
|
|
// Assert
|
|
Assert.AreEqual(@"C\:\\Fonts\\font\'s.ttf", result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void EscapeFfmpegPath_EscapesBackslashesBeforeIntroducingNewOnes()
|
|
{
|
|
// Act: if colon/quote escaping ran before backslash escaping, the backslashes
|
|
// they introduce would themselves get doubled, which must not happen.
|
|
string result = ShortsPipeline.EscapeFfmpegPath("C:\\a");
|
|
|
|
// Assert
|
|
Assert.AreEqual("C\\:\\\\a", result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void BuildMeta_WithAllFieldsPopulated_JoinsWithMiddleDot()
|
|
{
|
|
// Arrange
|
|
ShortSettings settings = ValidSettings("in.mp4", "font.ttf") with { Group = "Pirates", Year = "1991", Type = "Cracktro" };
|
|
|
|
// Act
|
|
string meta = ShortsPipeline.BuildMeta(settings);
|
|
|
|
// Assert
|
|
Assert.AreEqual("Pirates · 1991 · Cracktro", meta);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void BuildMeta_WithSomeFieldsBlank_SkipsBlankFields()
|
|
{
|
|
// Arrange
|
|
ShortSettings settings = ValidSettings("in.mp4", "font.ttf") with { Group = "Pirates", Year = "", Type = " " };
|
|
|
|
// Act
|
|
string meta = ShortsPipeline.BuildMeta(settings);
|
|
|
|
// Assert
|
|
Assert.AreEqual("Pirates", meta);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void BuildMeta_WithAllFieldsBlank_ReturnsEmptyString()
|
|
{
|
|
// Arrange
|
|
ShortSettings settings = ValidSettings("in.mp4", "font.ttf");
|
|
|
|
// Act
|
|
string meta = ShortsPipeline.BuildMeta(settings);
|
|
|
|
// Assert
|
|
Assert.AreEqual("", meta);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_WithMissingInputFile_ThrowsFileNotFoundException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
ShortSettings settings = ValidSettings(Path.Combine(directory, "missing.mp4"), "font.ttf");
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_WithBlankTitle_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
ShortSettings settings = ValidSettings(input, font) with { Title = "" };
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_WithZeroDuration_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
ShortSettings settings = ValidSettings(input, font) with { DurationSeconds = 0 };
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_WithMissingFontFile_ThrowsFileNotFoundException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
ShortSettings settings = ValidSettings(input, Path.Combine(directory, "missing-font.ttf"));
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_BrandStyleWithoutBackgroundImage_ThrowsFileNotFoundException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
ShortSettings settings = ValidSettings(input, font, ShortStyle.Brand);
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_NonBrandStyleWithoutBackgroundImage_DoesNotThrow()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
ShortSettings settings = ValidSettings(input, font, ShortStyle.Crop);
|
|
|
|
// Act & Assert (no exception)
|
|
ShortsPipeline.Validate(settings);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_VideoStyleWithoutBackgroundVideo_ThrowsFileNotFoundException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
ShortSettings settings = ValidSettings(input, font, ShortStyle.Video);
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_VideoStyleWithBackgroundVideo_DoesNotThrow()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
string background = Path.Combine(directory, "bg.mp4");
|
|
File.WriteAllText(background, "data");
|
|
ShortSettings settings = ValidSettings(input, font, ShortStyle.Video) with { BackgroundVideo = background };
|
|
|
|
// Act & Assert (no exception)
|
|
ShortsPipeline.Validate(settings);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_VideoOpacityAboveOne_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
string background = Path.Combine(directory, "bg.mp4");
|
|
File.WriteAllText(background, "data");
|
|
ShortSettings settings = ValidSettings(input, font, ShortStyle.Video) with
|
|
{
|
|
BackgroundVideo = background,
|
|
VideoOpacity = 1.2,
|
|
};
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Validate_SourceMarginTooWide_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
string directory = CreateTempDirectory();
|
|
string input = Path.Combine(directory, "clip.mp4");
|
|
File.WriteAllText(input, "data");
|
|
string font = Path.Combine(directory, "font.ttf");
|
|
File.WriteAllText(font, "data");
|
|
ShortSettings settings = ValidSettings(input, font) with { SourceMarginX = 540 };
|
|
|
|
// Act & Assert
|
|
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void GetBaseFilter_ForEveryStyle_ReturnsAFilterEndingInBaseTag()
|
|
{
|
|
foreach (ShortStyle style in Enum.GetValues<ShortStyle>())
|
|
{
|
|
// Act
|
|
string filter = ShortsPipeline.GetBaseFilter(style);
|
|
|
|
// Assert
|
|
Assert.IsFalse(string.IsNullOrWhiteSpace(filter), $"{style} produced an empty filter.");
|
|
StringAssert.Contains(filter, "[base]", $"{style} filter does not label an output as [base].");
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void BuildFilterComplex_IncludesAllFourTextOverlaysAndTheirTimingGates()
|
|
{
|
|
// Act
|
|
string result = ShortsPipeline.BuildFilterComplex(
|
|
"[0:v]copy[base]",
|
|
fontEsc: "C\\:\\\\font.ttf",
|
|
hookEsc: "hook.txt", titleEsc: "title.txt", metaEsc: "meta.txt", websiteEsc: "website.txt",
|
|
hookEnd: 2.7, infoStart: 23, ctaStart: 26.5);
|
|
|
|
// Assert
|
|
StringAssert.Contains(result, "textfile='hook.txt'");
|
|
StringAssert.Contains(result, "textfile='title.txt'");
|
|
StringAssert.Contains(result, "textfile='meta.txt'");
|
|
StringAssert.Contains(result, "textfile='website.txt'");
|
|
StringAssert.Contains(result, "enable='between(t,0,2.7)'");
|
|
StringAssert.Contains(result, "enable='gte(t,23)'");
|
|
StringAssert.Contains(result, "enable='gte(t,26.5)'");
|
|
StringAssert.Contains(result, "[vout]");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void GetBaseFilter_VideoStyle_UsesBackgroundVideoInput()
|
|
{
|
|
// Act
|
|
string filter = ShortsPipeline.GetBaseFilter(ShortStyle.Video);
|
|
|
|
// Assert
|
|
StringAssert.Contains(filter, "[1:v]");
|
|
StringAssert.Contains(filter, "colorchannelmixer=aa=1");
|
|
StringAssert.Contains(filter, "[base]");
|
|
Assert.IsFalse(filter.Contains("gblur=", StringComparison.Ordinal));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void BuildVideoBaseFilter_WithBlur_InsertsGblur()
|
|
{
|
|
// Act
|
|
string filter = ShortsPipeline.BuildVideoBaseFilter(
|
|
"crop=iw:ih:0:0", "scale=1000:-2:flags=lanczos", blur: 8, opacity: 0.65);
|
|
|
|
// Assert
|
|
StringAssert.Contains(filter, "gblur=sigma=8");
|
|
StringAssert.Contains(filter, "colorchannelmixer=aa=0.65");
|
|
StringAssert.Contains(filter, "crop=iw:ih:0:0,scale=1000:-2:flags=lanczos");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ApplySourceFade_WhenFadeIsZero_ReturnsOriginalFilter()
|
|
{
|
|
// Arrange
|
|
const string baseFilter = "[0:v]split=2[bg][fg]";
|
|
|
|
// Act
|
|
string result = ShortsPipeline.ApplySourceFade(baseFilter, 0);
|
|
|
|
// Assert
|
|
Assert.AreEqual(baseFilter, result);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ApplySourceFade_WhenFadeIsPositive_RewritesSourceLabel()
|
|
{
|
|
// Act
|
|
string result = ShortsPipeline.ApplySourceFade("[0:v]split=2[bg][fg];[0:a]anull[a]", 1.5);
|
|
|
|
// Assert
|
|
StringAssert.StartsWith(result, "[0:v]format=rgba,fade=t=in:st=0:d=1.5:alpha=1[srcfade];");
|
|
StringAssert.Contains(result, "[srcfade]split=2[bg][fg]");
|
|
StringAssert.Contains(result, "[0:a]anull[a]");
|
|
Assert.IsFalse(result.Contains("[0:v]split", StringComparison.Ordinal));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void SelectMostCommonCropSpec_PicksTheMostFrequentSpec()
|
|
{
|
|
// Arrange
|
|
const string output =
|
|
"crop=800:480:100:0\n" +
|
|
"crop=780:480:120:0\n" +
|
|
"crop=780:480:120:0\n" +
|
|
"crop=760:480:140:0";
|
|
|
|
// Act
|
|
string? spec = ShortsPipeline.SelectMostCommonCropSpec(output);
|
|
|
|
// Assert
|
|
Assert.AreEqual("crop=780:480:120:0", spec);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void TryBuildAutoHorizontalCrop_AppliesEvenSafetyInset()
|
|
{
|
|
// Act
|
|
bool ok = ShortsPipeline.TryBuildAutoHorizontalCrop(800, 100, out int width, out int x);
|
|
|
|
// Assert
|
|
Assert.IsTrue(ok);
|
|
Assert.AreEqual(792, width);
|
|
Assert.AreEqual(104, x);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void BuildManualCropFilter_UsesEdgeInsets()
|
|
{
|
|
// Act
|
|
string filter = ShortsPipeline.BuildManualCropFilter(120, 80, 10, 20);
|
|
|
|
// Assert
|
|
Assert.AreEqual("crop=iw-120-80:ih-10-20:120:10", filter);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void SourceTargetWidth_SubtractsBothMargins()
|
|
{
|
|
// Act
|
|
int width = ShortsPipeline.SourceTargetWidth(40);
|
|
|
|
// Assert
|
|
Assert.AreEqual(1000, width);
|
|
}
|
|
}
|