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); CalculateCrop(job);
} }
else //else
{ //{
var sampler = new VideoRotationSampler(null); // var sampler = new VideoRotationSampler(null);
job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration, token); // job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration, token);
job.Detect = job.Rotate == 0 ? null : "body"; // job.Detect = job.Rotate == 0 ? null : "body";
} //}
_log.LogInfo(job.ToString()); _log.LogInfo(job.ToString());
} }

View File

@ -11,36 +11,73 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
string outputFile = job.OutputFileName; string outputFile = job.OutputFileName;
double start = job.SegmentStart; double start = job.SegmentStart;
double length = job.SegmentLength; 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) : ""; var rotation = GetRotationFilter(job.Job.Rotate);
string args; string args;
var rotation = GetRotationFilter(job.Job.Rotate);
if (rotation == null) if (rotation == null)
{ {
// Copy path: keep original SAR/DAR exactly as in source
args = args =
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " + $"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
$"-i \"{inputFile}\" " + $"-i \"{inputFile}\" " +
$"-t {length.ToString(CultureInfo.InvariantCulture)} " + $"-t {length.ToString(CultureInfo.InvariantCulture)} " +
$"-c copy {pass} \"{outputFile}\" -y"; $"-c copy {string.Join(" ", job.Job.Passthrough)} " +
$"\"{outputFile}\" -y";
} }
else 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 = args =
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " + $"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
$"-i \"{inputFile}\" " + $"-i \"{inputFile}\" " +
$"-t {length.ToString(CultureInfo.InvariantCulture)} " + $"-t {length.ToString(CultureInfo.InvariantCulture)} " +
$"-vf \"{rotation}\" " + sarArg + darArg +
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " + "-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
"-c:a copy " + "-c:a copy " +
$"{pass} \"{outputFile}\" -y"; $"{string.Join(" ", job.Job.Passthrough)} " +
$"\"{outputFile}\" -y";
} }
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = "ffmpeg", FileName = "ffmpeg",
@ -65,6 +102,7 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
LogInfo($"Segment {name} processing completed"); LogInfo($"Segment {name} processing completed");
} }
string? GetRotationFilter(int? degrees) => string? GetRotationFilter(int? degrees) =>
degrees switch degrees switch
{ {
@ -74,6 +112,20 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
_ => null _ => 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) 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, length,
encWidth, encWidth,
encHeight, encHeight,
fps, job.Info,
ffmpegPassthroughParameters, ffmpegPassthroughParameters,
job.Job.PlainText, job.Job.PlainText,
token); token);
@ -231,17 +231,36 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
string outputFile, string outputFile,
double start, double start,
double length, double length,
int width, int width, int height,
int height, VideoInfo info,
double fps,
string[] passthrough, string[] passthrough,
bool plainText, bool plainText,
CancellationToken token) CancellationToken token)
{ {
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : ""; 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 ss = start.ToString("0.###", CultureInfo.InvariantCulture);
var t = length.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 = var args =
"-y " + "-y " +
@ -249,6 +268,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
$"-ss {ss} -i \"{inputFile}\" " + $"-ss {ss} -i \"{inputFile}\" " +
"-map 0:v:0 -map 1:a:0? -shortest " + "-map 0:v:0 -map 1:a:0? -shortest " +
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " + "-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
sarArg + darArg +
"-c:a copy " + "-c:a copy " +
pass + $" \"{outputFile}\""; pass + $" \"{outputFile}\"";
@ -289,6 +309,26 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
// ---------- helpers ---------- // ---------- 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) private static async Task<int> ReadExact(Stream s, byte[] buffer, int offset, int count, CancellationToken token)
{ {
var total = 0; var total = 0;

View File

@ -87,29 +87,7 @@ public static class ProbeVideo
var bitrate = stream?.Bit_rate ?? 0.0; var bitrate = stream?.Bit_rate ?? 0.0;
var sar = ParseAspectRatio(stream?.Sample_aspect_ratio); return new VideoInfo(duration, width, height, fps, bitrate, stream?.Sample_aspect_ratio, stream?.Display_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);
} }
} }

View File

@ -1,4 +1,6 @@
namespace splitter.probe; using System.Globalization;
namespace splitter.probe;
public record VideoInfo( public record VideoInfo(
double Duration, double Duration,
@ -6,7 +8,30 @@ public record VideoInfo(
int Height, int Height,
double Fps, double Fps,
double Bitrate, double Bitrate,
Point2f Sar, string? SampleAspectRatio,
Point2f Dar, string? DisplayAspectRatio,
int Rotation = 0 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);
}
}