Add video preview, source crop, find-videos, and render toasts
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.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user