Sar/Dar support for all segment processors

This commit is contained in:
Alexander Shabarshov 2026-05-26 08:57:08 +01:00
parent 23bfdc8452
commit 093c7c7803
5 changed files with 150 additions and 55 deletions

View File

@ -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());
}

View File

@ -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)
{

View File

@ -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<int> ReadExact(Stream s, byte[] buffer, int offset, int count, CancellationToken token)
{
var total = 0;

View File

@ -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);
}
}

View File

@ -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);
}
}