From 093c7c780325608f75726fc92e2ee9971a45a4d7 Mon Sep 17 00:00:00 2001 From: unclshura Date: Tue, 26 May 2026 08:57:08 +0100 Subject: [PATCH] Sar/Dar support for all segment processors --- Splitter-UI/Services/AutoDecisionService.cs | 12 +-- splitter-cli/SimpleSplitter.cs | 86 +++++++++++++++++---- splitter-cli/TrackingSplitter.cs | 50 ++++++++++-- splitter-cli/probe/ProbeVideo.cs | 24 +----- splitter-cli/probe/VideoInfo.cs | 33 +++++++- 5 files changed, 150 insertions(+), 55 deletions(-) diff --git a/Splitter-UI/Services/AutoDecisionService.cs b/Splitter-UI/Services/AutoDecisionService.cs index c7c3e64..37de4d5 100644 --- a/Splitter-UI/Services/AutoDecisionService.cs +++ b/Splitter-UI/Services/AutoDecisionService.cs @@ -26,12 +26,12 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb CalculateCrop(job); } - else - { - var sampler = new VideoRotationSampler(null); - job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration, token); - job.Detect = job.Rotate == 0 ? null : "body"; - } + //else + //{ + // var sampler = new VideoRotationSampler(null); + // job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration, token); + // job.Detect = job.Rotate == 0 ? null : "body"; + //} _log.LogInfo(job.ToString()); } diff --git a/splitter-cli/SimpleSplitter.cs b/splitter-cli/SimpleSplitter.cs index 9bee0c3..48845bf 100644 --- a/splitter-cli/SimpleSplitter.cs +++ b/splitter-cli/SimpleSplitter.cs @@ -7,40 +7,77 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, { public async Task ProcessSegment(SingleTask job, CancellationToken token) { - string inputFile = job.Job.InputFile; - string outputFile = job.OutputFileName; - double start = job.SegmentStart; - double length = job.SegmentLength; - int videoWidth = job.Info.Width; - int videoHeight = job.Info.Height; - double fps = job.Info.Fps; - string[] ffmpegPassthroughParameters = job.Job.Passthrough; - - var pass = ffmpegPassthroughParameters.Length > 0 ? string.Join(" ", ffmpegPassthroughParameters) : ""; + string inputFile = job.Job.InputFile; + string outputFile = job.OutputFileName; + double start = job.SegmentStart; + double length = job.SegmentLength; + + var rotation = GetRotationFilter(job.Job.Rotate); string args; - var rotation = GetRotationFilter(job.Job.Rotate); + if (rotation == null) { + // Copy path: keep original SAR/DAR exactly as in source args = $"-ss {start.ToString(CultureInfo.InvariantCulture)} " + $"-i \"{inputFile}\" " + $"-t {length.ToString(CultureInfo.InvariantCulture)} " + - $"-c copy {pass} \"{outputFile}\" -y"; + $"-c copy {string.Join(" ", job.Job.Passthrough)} " + + $"\"{outputFile}\" -y"; } else { - // Rotation → must re-encode + var sarArg = ""; + var darArg = ""; + + var sar = job.Info.SampleAspectRatio; // e.g. "4:3" + if (sar != null) + { + // Rotation path: must re-encode and recompute DAR + + long sarNum = Convert.ToInt64(job.Info.Sar.X); + long sarDen = Convert.ToInt64(job.Info.Sar.Y); + + // After rotation, width/height swap + int w = job.Info.Width; + int h = job.Info.Height; + + if (job.Job.Rotate == 90 || job.Job.Rotate == 270) + { + (w, h) = (h, w); + } + + // Compute DAR = (w * sarNum) : (h * sarDen) + var darNum = w * sarNum; + var darDen = h * sarDen; + + // Reduce fraction + long Gcd(long a, long b) + { + while (b != 0) (a, b) = (b, a % b); + return a; + } + var g = Gcd(darNum, darDen); + darNum /= g; + darDen /= g; + + sarArg = $"-vf \"{rotation},setsar={sarNum}:{sarDen}\" "; + darArg = $"-aspect {darNum}:{darDen} "; + } + else + sarArg = $"-vf \"{rotation}\" "; + args = $"-ss {start.ToString(CultureInfo.InvariantCulture)} " + $"-i \"{inputFile}\" " + $"-t {length.ToString(CultureInfo.InvariantCulture)} " + - $"-vf \"{rotation}\" " + + sarArg + darArg + "-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " + "-c:a copy " + - $"{pass} \"{outputFile}\" -y"; + $"{string.Join(" ", job.Job.Passthrough)} " + + $"\"{outputFile}\" -y"; } - var psi = new ProcessStartInfo { FileName = "ffmpeg", @@ -56,7 +93,7 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, await ShowFFMpegProgress(length, proc, name, token); await proc.WaitForExitAsync(token); - + ClearProgress(name); if (proc.ExitCode != 0) @@ -65,6 +102,7 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, LogInfo($"Segment {name} processing completed"); } + string? GetRotationFilter(int? degrees) => degrees switch { @@ -74,6 +112,20 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, _ => null }; + private static long Gcd(long a, long b) + { + a = Math.Abs(a); + b = Math.Abs(b); + + while (b != 0) + { + long t = b; + b = a % b; + a = t; + } + + return a; + } private async Task ShowFFMpegProgress(double length, Process proc, string name, CancellationToken token) { diff --git a/splitter-cli/TrackingSplitter.cs b/splitter-cli/TrackingSplitter.cs index 2ae3a9a..6410783 100644 --- a/splitter-cli/TrackingSplitter.cs +++ b/splitter-cli/TrackingSplitter.cs @@ -68,7 +68,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable length, encWidth, encHeight, - fps, + job.Info, ffmpegPassthroughParameters, job.Job.PlainText, token); @@ -231,17 +231,36 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable string outputFile, double start, double length, - int width, - int height, - double fps, + int width, int height, + VideoInfo info, string[] passthrough, bool plainText, CancellationToken token) { var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : ""; - var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture); + var fpsStr = info.Fps.ToString("0.###", CultureInfo.InvariantCulture); var ss = start.ToString("0.###", CultureInfo.InvariantCulture); var t = length.ToString("0.###", CultureInfo.InvariantCulture); + var sarArg = !string.IsNullOrWhiteSpace(info.SampleAspectRatio) + ? $"-vf setsar={info.SampleAspectRatio} " + : ""; + + string darArg = ""; + + if (info.Sar is { } s) + { + // compute DAR from output size and SAR + var darNum = width * s.X; + var darDen = height * s.Y; + + // clamp to int and reduce + int dn = (int)Math.Min(int.MaxValue, Math.Max(int.MinValue, darNum)); + int dd = (int)Math.Min(int.MaxValue, Math.Max(int.MinValue, darDen)); + ReduceFraction(ref dn, ref dd); + + if (dn > 0 && dd > 0) + darArg = $"-aspect {dn}:{dd} "; + } var args = "-y " + @@ -249,6 +268,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable $"-ss {ss} -i \"{inputFile}\" " + "-map 0:v:0 -map 1:a:0? -shortest " + "-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " + + sarArg + darArg + "-c:a copy " + pass + $" \"{outputFile}\""; @@ -289,6 +309,26 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable // ---------- helpers ---------- + private static void ReduceFraction(ref int num, ref int den) + { + int Gcd(int a, int b) + { + while (b != 0) + { + var t = b; + b = a % b; + a = t; + } + return a; + } + + var g = Gcd(Math.Abs(num), Math.Abs(den)); + if (g > 1) + { + num /= g; + den /= g; + } + } private static async Task ReadExact(Stream s, byte[] buffer, int offset, int count, CancellationToken token) { var total = 0; diff --git a/splitter-cli/probe/ProbeVideo.cs b/splitter-cli/probe/ProbeVideo.cs index 5dd578b..9bfcf6f 100644 --- a/splitter-cli/probe/ProbeVideo.cs +++ b/splitter-cli/probe/ProbeVideo.cs @@ -87,29 +87,7 @@ public static class ProbeVideo var bitrate = stream?.Bit_rate ?? 0.0; - var sar = ParseAspectRatio(stream?.Sample_aspect_ratio); - var dar = ParseAspectRatio(stream?.Display_aspect_ratio); - - return new VideoInfo(duration, width, height, fps, bitrate, sar, dar); - } - - private static Point2f ParseAspectRatio(string? sar) - { - if (string.IsNullOrWhiteSpace(sar)) - return new Point2f(1.0f, 1.0f); - - var parts = sar.Split(':'); - if (parts.Length != 2) - return new(1.0f, 1.0f); - - if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) && - float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) && - den != 0) - { - return new(num, den); - } - - return new(1.0f, 1.0f); + return new VideoInfo(duration, width, height, fps, bitrate, stream?.Sample_aspect_ratio, stream?.Display_aspect_ratio); } } diff --git a/splitter-cli/probe/VideoInfo.cs b/splitter-cli/probe/VideoInfo.cs index 8521ee6..b00de4a 100644 --- a/splitter-cli/probe/VideoInfo.cs +++ b/splitter-cli/probe/VideoInfo.cs @@ -1,4 +1,6 @@ -namespace splitter.probe; +using System.Globalization; + +namespace splitter.probe; public record VideoInfo( double Duration, @@ -6,7 +8,30 @@ public record VideoInfo( int Height, double Fps, double Bitrate, - Point2f Sar, - Point2f Dar, + string? SampleAspectRatio, + string? DisplayAspectRatio, int Rotation = 0 -); +) +{ + public Point2f Sar => ParseAspectRatio(SampleAspectRatio); + public Point2f Dar => ParseAspectRatio(DisplayAspectRatio); + + private static Point2f ParseAspectRatio(string? sar) + { + if (string.IsNullOrWhiteSpace(sar)) + return new Point2f(1.0f, 1.0f); + + var parts = sar.Split(':'); + if (parts.Length != 2) + return new(1.0f, 1.0f); + + if (float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) && + float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) && + den != 0) + { + return new(num, den); + } + + return new(1.0f, 1.0f); + } +}