Bring the Video tab closer to Shorts with 4K preview stills and crop modes (none, auto, center, plus the visual editor). Settings store a default source folder so Find videos can collect output.avi sequences. Cap the FFmpeg log with auto-scroll, hide repeat spam, and show a Windows notification when a video or short finishes or fails.
114 lines
4.0 KiB
C#
114 lines
4.0 KiB
C#
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
using AmiReel.Models;
|
|
|
|
namespace AmiReel.Services;
|
|
|
|
/// <summary>Shared FFmpeg crop filters used by both Video and Shorts pipelines.</summary>
|
|
internal static class SourceCropFilters
|
|
{
|
|
internal const string CenterFourByThreeFilter =
|
|
"crop='min(iw,ih*4/3)':'min(ih,iw*3/4)':'(iw-ow)/2':'(ih-oh)/2'";
|
|
|
|
private static readonly Regex CropSpecRegex = new(
|
|
@"crop=(\d+):(\d+):(\d+):(\d+)",
|
|
RegexOptions.CultureInvariant);
|
|
|
|
internal static string BuildManualCropFilter(int left, int right, int top, int bottom) =>
|
|
$"crop=iw-{left}-{right}:ih-{top}-{bottom}:{left}:{top}";
|
|
|
|
internal static bool HasManualCrop(int left, int right, int top, int bottom) =>
|
|
left > 0 || right > 0 || top > 0 || bottom > 0;
|
|
|
|
internal static string Resolve(
|
|
ShortSourceCropMode mode,
|
|
int left,
|
|
int right,
|
|
int top,
|
|
int bottom,
|
|
string? autoDetected)
|
|
{
|
|
if (HasManualCrop(left, right, top, bottom))
|
|
return BuildManualCropFilter(left, right, top, bottom);
|
|
|
|
return mode switch
|
|
{
|
|
ShortSourceCropMode.None => "",
|
|
ShortSourceCropMode.Auto when !string.IsNullOrWhiteSpace(autoDetected) => autoDetected,
|
|
ShortSourceCropMode.Auto => CenterFourByThreeFilter,
|
|
_ => CenterFourByThreeFilter,
|
|
};
|
|
}
|
|
|
|
internal static string Prefix(string cropFilter, string nextFilter) =>
|
|
string.IsNullOrWhiteSpace(cropFilter) ? nextFilter : $"{cropFilter},{nextFilter}";
|
|
|
|
internal static List<string> BuildHorizontalCropDetectArguments(string inputFile, string start, double durationSeconds) =>
|
|
[
|
|
"-hide_banner",
|
|
"-ss",
|
|
string.IsNullOrWhiteSpace(start) ? "00:00:00" : start,
|
|
"-t",
|
|
durationSeconds.ToString("0.###", CultureInfo.InvariantCulture),
|
|
"-i",
|
|
inputFile,
|
|
"-an",
|
|
"-vf",
|
|
"crop=iw:trunc(ih*0.70/2)*2:0:trunc(ih*0.15/2)*2,cropdetect=limit=28:round=2:reset=0",
|
|
"-f",
|
|
"null",
|
|
"-",
|
|
];
|
|
|
|
internal static string? SelectMostCommonCropSpec(string ffmpegOutput)
|
|
{
|
|
Dictionary<string, int> counts = new(StringComparer.Ordinal);
|
|
foreach (Match match in CropSpecRegex.Matches(ffmpegOutput))
|
|
{
|
|
string spec = match.Value;
|
|
counts[spec] = counts.GetValueOrDefault(spec) + 1;
|
|
}
|
|
|
|
return counts.Count == 0
|
|
? null
|
|
: counts.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal).First().Key;
|
|
}
|
|
|
|
internal static bool TryParseCropSpec(string spec, out int width, out int height, out int x, out int y)
|
|
{
|
|
Match match = CropSpecRegex.Match(spec);
|
|
if (!match.Success)
|
|
{
|
|
width = height = x = y = 0;
|
|
return false;
|
|
}
|
|
|
|
width = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
height = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture);
|
|
x = int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture);
|
|
y = int.Parse(match.Groups[4].Value, CultureInfo.InvariantCulture);
|
|
return true;
|
|
}
|
|
|
|
internal static bool TryBuildAutoHorizontalCrop(int detectedWidth, int detectedX, out int width, out int x)
|
|
{
|
|
int inset = (int)(detectedWidth * 0.005 / 2) * 2;
|
|
width = ((detectedWidth - (2 * inset)) / 2) * 2;
|
|
x = ((detectedX + inset) / 2) * 2;
|
|
if (width < 16)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
internal static string? TryBuildAutoHorizontalFilter(string ffmpegOutput)
|
|
{
|
|
string? spec = SelectMostCommonCropSpec(ffmpegOutput);
|
|
if (spec is null || !TryParseCropSpec(spec, out int width, out _, out int x, out _))
|
|
return null;
|
|
if (!TryBuildAutoHorizontalCrop(width, x, out int cropWidth, out int cropX))
|
|
return null;
|
|
|
|
return $"crop={cropWidth}:ih:{cropX}:0";
|
|
}
|
|
}
|