mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Source image rotation added. Interfaces simplified.
This commit is contained in:
parent
44d817f17f
commit
3fa068e48b
@ -20,6 +20,7 @@ public class SingleJob
|
||||
public bool EstimateOnly { get; set; }
|
||||
public bool ForceFixed { get; set; }
|
||||
public bool SingleThreaded { get; set; }
|
||||
public int? Rotate { get; set; }
|
||||
public Dictionary<string, string> Parameters { get; set; } = [];
|
||||
|
||||
public void Override<T>(ref T member, string name)
|
||||
@ -100,6 +101,18 @@ public sealed class CommandLine
|
||||
{
|
||||
Master.Detect = arg.Substring("--detect=".Length).ToLowerInvariant();
|
||||
}
|
||||
else if (arg =="--rotate")
|
||||
{
|
||||
Master.Rotate = 90;
|
||||
}
|
||||
else if (arg.StartsWith("--rotate="))
|
||||
{
|
||||
var val = arg.Substring("--rotate=".Length);
|
||||
if (int.TryParse(val, out var degrees) && (degrees == 90 || degrees == 180 || degrees == 270))
|
||||
Master.Rotate = degrees;
|
||||
else
|
||||
throw new FormatException($"Invalid --rotate value: {val}");
|
||||
}
|
||||
else if (arg.StartsWith("--crop="))
|
||||
{
|
||||
Master.Crop = ParseCrop(arg.Substring("--crop=".Length));
|
||||
@ -172,6 +185,7 @@ public sealed class CommandLine
|
||||
EstimateOnly = Master.EstimateOnly,
|
||||
ForceFixed = Master.ForceFixed,
|
||||
SingleThreaded = Master.SingleThreaded,
|
||||
Rotate = Master.Rotate,
|
||||
Parameters = new Dictionary<string, string>(Master.Parameters)
|
||||
}).ToArray();
|
||||
}
|
||||
@ -297,6 +311,9 @@ Options:
|
||||
Last segment may be shorter.
|
||||
Default: OFF
|
||||
|
||||
--rotate=<degrees> Rotate video by specified degrees (90, 180, 270).
|
||||
Useful for videos with incorrect orientation metadata.
|
||||
|
||||
--estimate Print calculated segment information and exit.
|
||||
No splitting is performed.
|
||||
|
||||
|
||||
@ -6,5 +6,5 @@ namespace splitter;
|
||||
|
||||
public interface ISegmentProcessor
|
||||
{
|
||||
Task ProcessSegment( string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] ffmpegPassthroughParameters);
|
||||
Task ProcessSegment( SingleTask job );
|
||||
}
|
||||
|
||||
92
ProbeVideo.cs
Normal file
92
ProbeVideo.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
public record VideoInfo(
|
||||
double Duration,
|
||||
int Width,
|
||||
int Height,
|
||||
double Fps,
|
||||
double Bitrate
|
||||
);
|
||||
|
||||
public static class ProbeVideo
|
||||
{
|
||||
public static VideoInfo Probe(string inputFile)
|
||||
{
|
||||
var args =
|
||||
"-v error " +
|
||||
"-select_streams v:0 " +
|
||||
"-show_entries format=duration " +
|
||||
"-show_entries stream=width,height,avg_frame_rate,bit_rate " +
|
||||
"-of default=noprint_wrappers=1:nokey=0 " + // <-- IMPORTANT: include keys
|
||||
$"\"{inputFile}\"";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffprobe",
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var p = new Process { StartInfo = psi };
|
||||
p.Start();
|
||||
|
||||
var duration = -1.0;
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var fps = 0.0;
|
||||
var bitrate = 0.0;
|
||||
|
||||
while (!p.StandardOutput.EndOfStream)
|
||||
{
|
||||
var line = p.StandardOutput.ReadLine()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (line.StartsWith("duration="))
|
||||
{
|
||||
var v = line.Substring("duration=".Length);
|
||||
double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out duration);
|
||||
}
|
||||
else if (line.StartsWith("width="))
|
||||
{
|
||||
var v = line.Substring("width=".Length);
|
||||
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out width);
|
||||
}
|
||||
else if (line.StartsWith("bit_rate="))
|
||||
{
|
||||
var v = line.Substring("bit_rate=".Length);
|
||||
double.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out bitrate);
|
||||
}
|
||||
else if (line.StartsWith("height="))
|
||||
{
|
||||
var v = line.Substring("height=".Length);
|
||||
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out height);
|
||||
}
|
||||
else if (line.StartsWith("avg_frame_rate="))
|
||||
{
|
||||
var v = line.Substring("avg_frame_rate=".Length);
|
||||
var parts = v.Split('/');
|
||||
if (parts.Length == 2 &&
|
||||
double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
|
||||
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
|
||||
den != 0)
|
||||
{
|
||||
fps = num / den;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.WaitForExit();
|
||||
|
||||
return new(duration, width, height, fps, bitrate);
|
||||
}
|
||||
}
|
||||
@ -9,15 +9,41 @@ namespace splitter;
|
||||
|
||||
public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, segmentNo), ISegmentProcessor
|
||||
{
|
||||
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] passthrough)
|
||||
public async Task ProcessSegment(SingleTask job)
|
||||
{
|
||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||
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 args =
|
||||
var pass = ffmpegPassthroughParameters.Length > 0 ? string.Join(" ", ffmpegPassthroughParameters) : "";
|
||||
|
||||
string args;
|
||||
var rotation = GetRotationFilter(job.Job.Rotate);
|
||||
if (rotation == null)
|
||||
{
|
||||
args =
|
||||
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-i \"{inputFile}\" " +
|
||||
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-c copy {pass} \"{outputFile}\" -y";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rotation → must re-encode
|
||||
args =
|
||||
$"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-i \"{inputFile}\" " +
|
||||
$"-t {length.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"-vf \"{rotation}\" " +
|
||||
"-c:v h264_nvenc -preset p4 -b:v 8M -pix_fmt yuv420p " +
|
||||
"-c:a copy " +
|
||||
$"{pass} \"{outputFile}\" -y";
|
||||
}
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
@ -43,6 +69,16 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
|
||||
LogInfo($"Segment {name} processing completed");
|
||||
}
|
||||
|
||||
string? GetRotationFilter(int? degrees) =>
|
||||
degrees switch
|
||||
{
|
||||
90 => "transpose=1",
|
||||
180 => "rotate=PI",
|
||||
270 => "transpose=2",
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
||||
private void ShowFFMpegProgress(double length, Process proc, string name)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
12
SingleTask.cs
Normal file
12
SingleTask.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using splitter;
|
||||
|
||||
public record SingleTask(
|
||||
SingleJob Job,
|
||||
VideoInfo Info,
|
||||
string OutputFileName,
|
||||
int SegmentIndex,
|
||||
int TotalSegments,
|
||||
double SegmentStart,
|
||||
double SegmentLength,
|
||||
Func<int, ISegmentProcessor> ProcessorFactory
|
||||
);
|
||||
@ -11,7 +11,6 @@ namespace splitter;
|
||||
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
{
|
||||
private readonly IObjectDetector _detector;
|
||||
private readonly SingleJob _cmd;
|
||||
|
||||
public TrackingSplitter(
|
||||
int progressLine,
|
||||
@ -21,7 +20,6 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
: base(logger, progressLine)
|
||||
{
|
||||
_detector = detector;
|
||||
_cmd = cmd;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -30,14 +28,18 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
d.Dispose();
|
||||
}
|
||||
|
||||
public async Task ProcessSegment(
|
||||
string inputFile,
|
||||
string outputFile,
|
||||
double start,
|
||||
double length,
|
||||
int videoWidth, int videoHeight, double fps,
|
||||
string[] ffmpegPassthroughParameters)
|
||||
public async Task ProcessSegment(SingleTask job)
|
||||
{
|
||||
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;
|
||||
double bitrate = job.Info.Bitrate;
|
||||
string[] ffmpegPassthroughParameters = job.Job.Passthrough;
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(outputFile);
|
||||
|
||||
// 1) Probe source video
|
||||
@ -47,19 +49,19 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cmd.Crop == null)
|
||||
if (job.Job.Crop == null)
|
||||
{
|
||||
LogError($"{name}: Crop parameters are required");
|
||||
return;
|
||||
}
|
||||
|
||||
var encWidth = _cmd.Debug ? videoWidth : _cmd.Crop.Value.width;
|
||||
var encHeight = _cmd.Debug ? videoHeight : _cmd.Crop.Value.height;
|
||||
var encWidth = job.Job.Debug ? videoWidth : job.Job.Crop.Value.width;
|
||||
var encHeight = job.Job.Debug ? videoHeight : job.Job.Crop.Value.height;
|
||||
|
||||
LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
|
||||
|
||||
// 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
|
||||
var decode = StartFfmpegDecode(inputFile, start, length);
|
||||
var decode = StartFfmpegDecode(inputFile, start, length, job.Job.Rotate, job.Job.PlainText);
|
||||
using var decodeStdout = decode.StandardOutput.BaseStream;
|
||||
|
||||
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
||||
@ -71,7 +73,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
encWidth,
|
||||
encHeight,
|
||||
fps,
|
||||
ffmpegPassthroughParameters);
|
||||
ffmpegPassthroughParameters,
|
||||
job.Job.PlainText);
|
||||
|
||||
using var encodeStdin = encode.StandardInput.BaseStream;
|
||||
|
||||
@ -89,10 +92,10 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
var camera = new CameraController(
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
_cmd.Crop.Value.width,
|
||||
_cmd.Crop.Value.height,
|
||||
job.Job.Crop.Value.width,
|
||||
job.Job.Crop.Value.height,
|
||||
kalman,
|
||||
_cmd);
|
||||
job.Job);
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var totalFrames = (int)Math.Round(length * fps);
|
||||
@ -115,7 +118,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
camera.Update(primary);
|
||||
var roi = camera.Roi;
|
||||
|
||||
if (_cmd.Debug)
|
||||
if (job.Job.Debug)
|
||||
{
|
||||
DrawDebug(frameMat, objects, camera, kalman);
|
||||
frameMat.CopyTo(outMat);
|
||||
@ -165,15 +168,26 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
// ---------- FFmpeg decode / encode ----------
|
||||
|
||||
private Process StartFfmpegDecode(string inputFile, double start, double length)
|
||||
private Process StartFfmpegDecode(string inputFile, double start, double length, int? rotate, bool plainText)
|
||||
{
|
||||
var ss = start.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
var ss = start .ToString("0.###", CultureInfo.InvariantCulture);
|
||||
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
|
||||
var rotateStr = "";
|
||||
if (rotate != null)
|
||||
{
|
||||
switch (rotate.Value)
|
||||
{
|
||||
case 90: rotateStr = ",transpose=1"; break;
|
||||
case 180: rotateStr = ",transpose=PI"; break;
|
||||
case 270: rotateStr = ",transpose=2"; break;
|
||||
}
|
||||
}
|
||||
|
||||
var args =
|
||||
$"-i \"{inputFile}\" -ss {ss} -t {t} " +
|
||||
"-an -sn " +
|
||||
"-vf format=bgr24 " +
|
||||
$"-vf format=bgr24{rotateStr} " +
|
||||
"-f rawvideo -";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
@ -191,20 +205,17 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
var fileName = Path.GetFileName(inputFile);
|
||||
|
||||
if (_cmd.PlainText)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string? line;
|
||||
while ((line = p.StandardError.ReadLine()) != null)
|
||||
if (_cmd.PlainText)
|
||||
if (plainText)
|
||||
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
@ -217,7 +228,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
int width,
|
||||
int height,
|
||||
double fps,
|
||||
string[] passthrough)
|
||||
string[] passthrough,
|
||||
bool plainText)
|
||||
{
|
||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
@ -230,9 +242,10 @@ 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 " +
|
||||
"-c:a aac -b:a 192k " +
|
||||
"-c:a copy " +
|
||||
pass + $" \"{outputFile}\"";
|
||||
|
||||
// "-c:a aac -b:a 192k " +
|
||||
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
@ -257,7 +270,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
string? line;
|
||||
while ((line = p.StandardError.ReadLine()) != null)
|
||||
{
|
||||
if (_cmd.PlainText)
|
||||
if (plainText)
|
||||
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
124
splitter.cs
124
splitter.cs
@ -5,23 +5,10 @@ using System.Text;
|
||||
using Spectre.Console;
|
||||
using splitter;
|
||||
|
||||
static class Program
|
||||
static partial class Program
|
||||
{
|
||||
private static ILogger _logger = null!;
|
||||
|
||||
private record SingleTask(
|
||||
SingleJob Job,
|
||||
string OutputFileName,
|
||||
int SegmentIndex,
|
||||
int TotalSegments,
|
||||
double SegmentStart,
|
||||
double SegmentLength,
|
||||
int VideoWidth,
|
||||
int VideoHeight,
|
||||
double VideoFps,
|
||||
Func<int, ISegmentProcessor> ProcessorFactory
|
||||
);
|
||||
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
Task? uiTask = null;
|
||||
@ -48,6 +35,9 @@ static class Program
|
||||
uiTask = logger.RunAsync(cts.Token);
|
||||
}
|
||||
|
||||
if (cmd.Master.EstimateOnly)
|
||||
LogInfo("=== ESTIMATE MODE ===");
|
||||
|
||||
var allJobs = new List<SingleTask>();
|
||||
foreach ( var job in cmd.Jobs )
|
||||
{
|
||||
@ -89,10 +79,9 @@ static class Program
|
||||
Directory.CreateDirectory(job.OutputFolder);
|
||||
|
||||
job.Mask ??= $"{baseName}_seg%03d.mp4";
|
||||
LogInfo($"{baseName}: Reading duration via ffprobe...");
|
||||
|
||||
(double duration, int width, int height, double fps) = ProbeVideo(job.InputFile);
|
||||
if (duration <= 0)
|
||||
var info = ProbeVideo.Probe(job.InputFile);
|
||||
if (info.Duration <= 0)
|
||||
{
|
||||
LogError($"{baseName}: Could not read duration.");
|
||||
return [];
|
||||
@ -106,25 +95,17 @@ static class Program
|
||||
if (job.ForceFixed)
|
||||
{
|
||||
// Fixed chunk size, last one may be shorter
|
||||
segments = (int)Math.Ceiling(duration / target);
|
||||
segments = (int)Math.Ceiling(info.Duration / target);
|
||||
segmentLength = target;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Equalized segments
|
||||
segments = (int)Math.Ceiling(duration / target);
|
||||
segmentLength = duration / segments;
|
||||
segments = (int)Math.Ceiling(info.Duration / target);
|
||||
segmentLength = info.Duration / segments;
|
||||
}
|
||||
|
||||
if (cmd.Master.EstimateOnly)
|
||||
LogInfo("=== ESTIMATE MODE ===");
|
||||
|
||||
LogInfo($"{baseName}: Duration {duration:F2}s, {width}x{height} @ {fps:F3}fps");
|
||||
LogInfo($"{baseName}: Target duration: {target:F2}s");
|
||||
LogInfo($"{baseName}: Segments: {segments}");
|
||||
LogInfo(job.ForceFixed
|
||||
? $"{baseName}: Fixed segment length: {segmentLength:F2}s (last may be shorter)"
|
||||
: $"{baseName}: Equalized segment length: {segmentLength:F2}s");
|
||||
LogInfo($"{baseName}: Duration {info.Duration:F2}s, {info.Width}x{info.Height} @ {info.Fps:F3}fps {info.Bitrate/1024:F0}kbps, Target duration: {target:F2}s Segments: {segments} segment length: {segmentLength:F2}s {(job.ForceFixed ? " fixed" : "")}" );
|
||||
|
||||
if (cmd.Master.EstimateOnly)
|
||||
return [];
|
||||
@ -152,17 +133,15 @@ static class Program
|
||||
.Select(i => new SingleTask
|
||||
(
|
||||
Job : job,
|
||||
Info: info,
|
||||
OutputFileName : BuildOutputFileName(job.OutputFolder, job.Mask, i),
|
||||
SegmentIndex : i,
|
||||
TotalSegments : segments,
|
||||
SegmentStart : i * segmentLength,
|
||||
SegmentLength : (i == segments - 1)
|
||||
? Math.Max(0.1, duration - i * segmentLength)
|
||||
? Math.Max(0.1, info.Duration - i * segmentLength)
|
||||
: segmentLength,
|
||||
ProcessorFactory : processorFactory,
|
||||
VideoWidth : width,
|
||||
VideoHeight : height,
|
||||
VideoFps : fps
|
||||
ProcessorFactory : processorFactory
|
||||
)
|
||||
)
|
||||
.ToList();
|
||||
@ -273,15 +252,7 @@ static class Program
|
||||
var processor = t.ProcessorFactory(slot);
|
||||
try
|
||||
{
|
||||
await processor.ProcessSegment(
|
||||
t.Job.InputFile,
|
||||
t.OutputFileName,
|
||||
t.SegmentStart,
|
||||
t.SegmentLength,
|
||||
t.VideoWidth,
|
||||
t.VideoHeight,
|
||||
t.VideoFps,
|
||||
t.Job.Passthrough);
|
||||
await processor.ProcessSegment(t);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -317,73 +288,6 @@ static class Program
|
||||
return Path.Combine(folder, fileName);
|
||||
}
|
||||
|
||||
public static (double duration, int width, int height, double fps) ProbeVideo(string inputFile)
|
||||
{
|
||||
var args =
|
||||
"-v error " +
|
||||
"-select_streams v:0 " +
|
||||
"-show_entries format=duration " +
|
||||
"-show_entries stream=width,height,avg_frame_rate " +
|
||||
"-of default=noprint_wrappers=1:nokey=0 " + // <-- IMPORTANT: include keys
|
||||
$"\"{inputFile}\"";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffprobe",
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var p = new Process { StartInfo = psi };
|
||||
p.Start();
|
||||
|
||||
var duration = -1.0;
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var fps = 0.0;
|
||||
|
||||
while (!p.StandardOutput.EndOfStream)
|
||||
{
|
||||
var line = p.StandardOutput.ReadLine()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (line.StartsWith("duration="))
|
||||
{
|
||||
var v = line.Substring("duration=".Length);
|
||||
double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out duration);
|
||||
}
|
||||
else if (line.StartsWith("width="))
|
||||
{
|
||||
var v = line.Substring("width=".Length);
|
||||
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out width);
|
||||
}
|
||||
else if (line.StartsWith("height="))
|
||||
{
|
||||
var v = line.Substring("height=".Length);
|
||||
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out height);
|
||||
}
|
||||
else if (line.StartsWith("avg_frame_rate="))
|
||||
{
|
||||
var v = line.Substring("avg_frame_rate=".Length);
|
||||
var parts = v.Split('/');
|
||||
if (parts.Length == 2 &&
|
||||
double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
|
||||
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
|
||||
den != 0)
|
||||
{
|
||||
fps = num / den;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.WaitForExit();
|
||||
|
||||
return (duration, width, height, fps);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user