diff --git a/ISegmentProcessor.cs b/ISegmentProcessor.cs index 4f2979e..01219b8 100644 --- a/ISegmentProcessor.cs +++ b/ISegmentProcessor.cs @@ -6,5 +6,5 @@ namespace splitter; public interface ISegmentProcessor { - Task ProcessSegment( string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters); + Task ProcessSegment( string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] ffmpegPassthroughParameters); } diff --git a/SimpleSplitter.cs b/SimpleSplitter.cs index 5cbc7e8..ee071d2 100644 --- a/SimpleSplitter.cs +++ b/SimpleSplitter.cs @@ -9,17 +9,7 @@ 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, string[] passthrough) - { - RunFFmpegSegment(inputFile, outputFile, start, length, passthrough); - } - - private void RunFFmpegSegment( - string inputFile, - string outputFile, - double start, - double length, - string[] passthrough) + public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] passthrough) { var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : ""; diff --git a/SpectreConsoleLogger.cs b/SpectreConsoleLogger.cs index 6280f36..ccf376e 100644 --- a/SpectreConsoleLogger.cs +++ b/SpectreConsoleLogger.cs @@ -25,8 +25,10 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable private readonly Dictionary _progress = new(); private readonly CancellationTokenSource _cts = new(); - private Task? _uiTask; - private Task? _inputTask; + private Task? _uiTask; + private Task? _inputTask; + private int _numberOfProcesses = 1; + private const int _maxLogEntries = 500; // Public configuration public string Title { get; set; } = string.Empty; @@ -51,14 +53,6 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable } } - private int _numberOfProcesses = 1; - - private const int MaxLogEntries = 500; - - public SpectreConsoleLogger() - { - NumberOfProcesses = 1; - } // ---- ILogger ---- @@ -93,8 +87,8 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable { lock (_sync) { - if (_logs.Count >= MaxLogEntries) - _logs.RemoveRange(0, _logs.Count - MaxLogEntries + 1); + if (_logs.Count >= _maxLogEntries) + _logs.RemoveRange(0, _logs.Count - _maxLogEntries + 1); _logs.Add(new LogEntry( DateTime.Now, @@ -137,7 +131,7 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable ctx.UpdateTarget(BuildRoot()); await Task.Delay(100, token); } - catch ( Exception ex ) + catch ( Exception ) { break; } diff --git a/TrackingSplitter.cs b/TrackingSplitter.cs index 8aeb71e..caec81f 100644 --- a/TrackingSplitter.cs +++ b/TrackingSplitter.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using OpenCvSharp; @@ -9,21 +10,16 @@ namespace splitter; public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable { - private readonly int _cropWidth; - private readonly int _cropHeight; - private readonly bool _debugOverlay; - private readonly bool _plainText; - private readonly IObjectDetector _detector; - private readonly SingleJob _cmd; + private readonly SingleJob _cmd; - public TrackingSplitter(int segmentNo, int cropWidth, int cropHeight, bool debugOverlay, bool plainText, IObjectDetector detector, SingleJob cmd, ILogger logger) - : base(logger, segmentNo) + public TrackingSplitter( + int progressLine, + IObjectDetector detector, + SingleJob cmd, + ILogger logger) + : base(logger, progressLine) { - _cropWidth = cropWidth; - _cropHeight = cropHeight; - _debugOverlay = debugOverlay; - _plainText = plainText; _detector = detector; _cmd = cmd; } @@ -34,146 +30,287 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable d.Dispose(); } - public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters) - { - using var capture = new VideoCapture(inputFile); - if (!capture.IsOpened()) + public async Task ProcessSegment( + string inputFile, + string outputFile, + double start, + double length, + int videoWidth, int videoHeight, double fps, + string[] ffmpegPassthroughParameters) + { + var name = Path.GetFileNameWithoutExtension(outputFile); + + // 1) Probe source video + if (videoWidth <= 0 || videoHeight <= 0 || fps <= 0) { - LogError($"{Path.GetFileName(inputFile)}: Cannot open video"); + LogError($"{name}: ffprobe failed to get metadata"); return; } - var name = Path.GetFileNameWithoutExtension(outputFile); - var skip = TimeSpan.FromSeconds(start); - var duration = TimeSpan.FromSeconds(length); + if (_cmd.Crop == null) + { + LogError($"{name}: Crop parameters are required"); + return; + } - capture.Set(VideoCaptureProperties.PosMsec, start); + var encWidth = _cmd.Debug ? videoWidth : _cmd.Crop.Value.width; + var encHeight = _cmd.Debug ? videoHeight : _cmd.Crop.Value.height; - var videoWidth = (int)capture.Get(VideoCaptureProperties.FrameWidth); - var videoHeight = (int)capture.Get(VideoCaptureProperties.FrameHeight); - var fps = capture.Get(VideoCaptureProperties.Fps); - var totalFrames = (int)(length * fps); + LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}"); - var originalCropWidth = _cropWidth; - var originalCropHeight = _cropHeight; + // 2) Start FFmpeg decode (video only → raw BGR24 to stdout) + var decode = StartFfmpegDecode(inputFile, start, length); + using var decodeStdout = decode.StandardOutput.BaseStream; - LogInfo($"{Path.GetFileName(outputFile)}:: [TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}"); - - var encWidth = _debugOverlay ? videoWidth : originalCropWidth; - var encHeight = _debugOverlay ? videoHeight : originalCropHeight; - - var ffmpeg = StartFfmpegNvenc( + // 3) Start FFmpeg encode (video from stdin + audio from original) + var encode = StartFfmpegEncode( inputFile, outputFile, + start, + length, encWidth, encHeight, fps, - skip, ffmpegPassthroughParameters); - using var stdin = ffmpeg.StandardInput.BaseStream; + using var encodeStdin = encode.StandardInput.BaseStream; - using var frame = new Mat(); - using var outputBgr = new Mat(encHeight, encWidth, MatType.CV_8UC3); + // Separate input/output sizes and buffers + var inBytes = videoWidth * videoHeight * 3; + var outBytes = encWidth * encHeight * 3; - var frameBytes = encWidth * encHeight * 3; - var videoBuffer = new byte[frameBytes]; + var inBuffer = new byte[inBytes]; + var outBuffer = new byte[outBytes]; + + using var frameMat = new Mat(videoHeight, videoWidth, MatType.CV_8UC3); + using var outMat = new Mat(encHeight, encWidth, MatType.CV_8UC3); var kalman = new KalmanTracker(); - // initial reset is now done inside CameraController - var camera = new CameraController( videoWidth, videoHeight, - originalCropWidth, - originalCropHeight, + _cmd.Crop.Value.width, + _cmd.Crop.Value.height, kalman, - _cmd - ); + _cmd); - var startTime = DateTime.UtcNow; + var startTime = DateTime.UtcNow; + var totalFrames = (int)Math.Round(length * fps); + var frameIndex = 0; - for (var i = 0; i < totalFrames; i++) + while (frameIndex < totalFrames) { - if (!capture.Read(frame) || frame.Empty()) + frameIndex++; + + var read = ReadExact(decodeStdout, inBuffer, 0, inBytes); + if (read != inBytes) break; - Rect? objectBox = null; - Point2f? objectCenter = null; + // input frame → Mat + Marshal.Copy(inBuffer, 0, frameMat.Data, inBytes); - var objects = _detector.DetectAll(frame, videoWidth, videoHeight); + var objects = _detector.DetectAll(frameMat, videoWidth, videoHeight); var primary = SelectTrackedObject(objects, kalman.LastMeasurement); camera.Update(primary); + var roi = camera.Roi; - objectBox = camera.ObjectBox; - objectCenter = camera.ObjectCenter; - - var smoothedCenter = camera.SmoothedCenter; - var cameraCenter = camera.CameraCenter; - var state = camera.State; - var lostFrames = camera.LostFrames; - var roi = camera.Roi; - - if (_debugOverlay) + if (_cmd.Debug) { - if (objectBox.HasValue) - { - var fb = objectBox.Value; - Cv2.Rectangle(frame, - new Rect(fb.X, fb.Y, fb.Width, fb.Height), - Scalar.LimeGreen, 2); - } - - Cv2.Circle(frame, - new Point((int)smoothedCenter.X, (int)smoothedCenter.Y), - 6, Scalar.LimeGreen, -1); - - Cv2.Rectangle(frame, roi, - objectCenter.HasValue ? Scalar.Yellow : Scalar.Red, 3); - - DrawText(frame, $"Faces: {objects.Count}", 20, 40, Scalar.White); - DrawText(frame, $"LostFrames: {lostFrames}", 20, 70, Scalar.White); - DrawText(frame, $"Noise: {kalman.CurrentNoise:F3}", 20, 130, Scalar.White); - DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White); - } - - if (_debugOverlay) - { - frame.CopyTo(outputBgr); - Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes); - stdin.Write(videoBuffer, 0, frameBytes); + DrawDebug(frameMat, objects, camera, kalman); + frameMat.CopyTo(outMat); } else { - using var cropped = new Mat(frame, roi); - cropped.CopyTo(outputBgr); - - Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes); - stdin.Write(videoBuffer, 0, frameBytes); + using var cropped = new Mat(frameMat, roi); + cropped.CopyTo(outMat); } + // output Mat → outBuffer + Marshal.Copy(outMat.Data, outBuffer, 0, outBytes); + encodeStdin.Write(outBuffer, 0, outBytes); + var elapsed = DateTime.UtcNow - startTime; - var progress = (double)i / totalFrames; - var speed = i > 0 ? (i / elapsed.TotalSeconds)/fps : 0.0; - var remainingFrames = totalFrames - i; - var etaSeconds = speed > 0 ? remainingFrames / speed : 0; + var progress = totalFrames > 0 ? (double)frameIndex / totalFrames : 0.0; + var speed = elapsed.TotalSeconds > 0 ? (frameIndex / elapsed.TotalSeconds) / fps : 0.0; + var remainingFrames = Math.Max(totalFrames - frameIndex, 0); + var etaSeconds = speed > 0 ? remainingFrames / speed : 0.0; var eta = TimeSpan.FromSeconds(etaSeconds); DrawProgress(name, progress, eta, speed); } - stdin.Flush(); - stdin.Close(); + encodeStdin.Flush(); - await ffmpeg.WaitForExitAsync(); + // loop finished + + encodeStdin.Flush(); + encodeStdin.Close(); // must happen before waiting encode + + await encode.WaitForExitAsync(); + + // belt-and-braces: if decode is still alive, kill it + try { if (!decode.HasExited) decode.Kill(entireProcessTree: true); } catch { } + try { if (!decode.HasExited) await decode.WaitForExitAsync(); } catch { } ClearProgress(); - if (ffmpeg.ExitCode != 0) - LogError($"{Path.GetFileName(outputFile)}: Segment {name} FFmpeg encoding failed"); + + if (encode.ExitCode != 0) + LogError($"{name}: FFmpeg encoding failed"); else - LogInfo($"{Path.GetFileName(outputFile)}: Segment {name} processing completed"); + LogInfo($"{name}: Segment processing completed"); + } + + + // ---------- FFmpeg decode / encode ---------- + + private Process StartFfmpegDecode(string inputFile, double start, double length) + { + var ss = start.ToString("0.###", CultureInfo.InvariantCulture); + var t = length.ToString("0.###", CultureInfo.InvariantCulture); + + var args = + $"-i \"{inputFile}\" -ss {ss} -t {t} " + + "-an -sn " + + "-vf format=bgr24 " + + "-f rawvideo -"; + + var psi = new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var p = new Process { StartInfo = psi }; + p.Start(); + + var fileName = Path.GetFileName(inputFile); + + if (_cmd.PlainText) + { + _ = Task.Run(() => + { + try + { + string? line; + while ((line = p.StandardError.ReadLine()) != null) + if (_cmd.PlainText) + LogInfo($"[ffmpeg-decode] {fileName}: {line}"); + } + catch { } + }); + } + + return p; + } + + private Process StartFfmpegEncode( + string inputFile, + string outputFile, + double start, + double length, + int width, + int height, + double fps, + string[] passthrough) + { + var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : ""; + var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture); + var ss = start.ToString("0.###", CultureInfo.InvariantCulture); + var t = length.ToString("0.###", CultureInfo.InvariantCulture); + + var args = + "-y " + + $"-f rawvideo -pix_fmt bgr24 -s {width}x{height} -r {fpsStr} -i - " + + $"-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 " + + pass + $" \"{outputFile}\""; + + + + var psi = new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = args, + RedirectStandardInput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var p = new Process { StartInfo = psi }; + p.Start(); + + var fileName = Path.GetFileName(outputFile); + + _ = Task.Run(() => + { + try + { + string? line; + while ((line = p.StandardError.ReadLine()) != null) + { + if (_cmd.PlainText) + LogInfo($"[ffmpeg-encode] {fileName}: {line}"); + } + } + catch { } + }); + + return p; + } + + // ---------- helpers ---------- + + private static int ReadExact(Stream s, byte[] buffer, int offset, int count) + { + var total = 0; + while (total < count) + { + var read = s.Read(buffer, offset + total, count - total); + if (read <= 0) + break; + total += read; + } + return total; + } + + private void DrawDebug( + Mat frame, + System.Collections.Generic.List<(Rect box, Point2f center)> objects, + CameraController camera, + KalmanTracker kalman) + { + if (camera.ObjectBox.HasValue) + { + var fb = camera.ObjectBox.Value; + Cv2.Rectangle(frame, fb, Scalar.LimeGreen, 2); + } + + Cv2.Circle(frame, + new Point((int)camera.SmoothedCenter.X, (int)camera.SmoothedCenter.Y), + 6, Scalar.LimeGreen, -1); + + Cv2.Rectangle(frame, camera.Roi, + camera.ObjectCenter.HasValue ? Scalar.Yellow : Scalar.Red, 3); + + DrawText(frame, $"Faces: {objects.Count}", 20, 40, Scalar.White); + DrawText(frame, $"LostFrames: {camera.LostFrames}", 20, 70, Scalar.White); + DrawText(frame, $"Noise: {kalman.CurrentNoise:F3}", 20, 130, Scalar.White); + DrawText(frame, $"Camera: {camera.CameraCenter.X:F1},{camera.CameraCenter.Y:F1}", 20, 160, Scalar.White); + } + + private static void DrawText(Mat img, string text, int x, int y, Scalar color) + { + Cv2.PutText(img, text, new Point(x, y), + HersheyFonts.HersheySimplex, 0.6, color, 2); } private (Rect box, Point2f center)? SelectTrackedObject( @@ -185,7 +322,6 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable if (!previousCenter.HasValue) { - // Largest area var bestIndex = 0; var bestArea = float.MinValue; @@ -204,8 +340,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable } else { - // Closest to previous center - var prev = previousCenter.Value; + var prev = previousCenter.Value; var bestIndex = 0; var bestDist2 = float.MaxValue; @@ -226,64 +361,4 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable return foundObjects[bestIndex]; } } - - private Process StartFfmpegNvenc( - string srcFileName, - string destFileName, - int width, - int height, - double fps, - TimeSpan skip, - string[] passthrough) - { - var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : ""; - var skipSeconds = skip.TotalSeconds.ToString("0.###", CultureInfo.InvariantCulture); - var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture); - - var args = - "-y " + - $"-f rawvideo -pix_fmt bgr24 -s {width}x{height} -r {fpsStr} -i - " + - $"-ss {skipSeconds} -i \"{srcFileName}\" " + - "-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 " + - pass + $" \"{destFileName}\""; - - var psi = new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = args, - RedirectStandardInput = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - var process = new Process { StartInfo = psi }; - process.Start(); - - _ = Task.Run(() => - { - try - { - string? line; - while ((line = process.StandardError.ReadLine()) != null) - { - if (_plainText) - Console.WriteLine($"[ffmpeg] {line}"); - } - } - catch { } - }); - - return process; - } - - private static void DrawText(Mat img, string text, int x, int y, Scalar color) - { - Cv2.PutText(img, text, new Point(x, y), - HersheyFonts.HersheySimplex, 0.6, color, 2); - } - } diff --git a/splitter.cs b/splitter.cs index 46705c3..176a83f 100644 --- a/splitter.cs +++ b/splitter.cs @@ -16,6 +16,9 @@ static class Program int TotalSegments, double SegmentStart, double SegmentLength, + int VideoWidth, + int VideoHeight, + double VideoFps, Func ProcessorFactory ); @@ -38,7 +41,6 @@ static class Program var logger = new SpectreConsoleLogger { Title = "Splitter", - NumberOfProcesses = cmd.Master.SingleThreaded ? 1 : Math.Max(1, Environment.ProcessorCount / 2) + 1 }; _logger = logger; @@ -89,7 +91,7 @@ static class Program job.Mask ??= $"{baseName}_seg%03d.mp4"; LogInfo($"{baseName}: Reading duration via ffprobe..."); - var duration = GetDuration(job.InputFile); + (double duration, int width, int height, double fps) = ProbeVideo(job.InputFile); if (duration <= 0) { LogError($"{baseName}: Could not read duration."); @@ -115,20 +117,17 @@ static class Program } if (cmd.Master.EstimateOnly) - { LogInfo("=== ESTIMATE MODE ==="); - LogInfo($"{baseName}: Total duration: {duration:F2}s"); - 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"); - return []; - } - LogInfo($"{baseName}: Duration: {duration:F2}s"); + LogInfo($"{baseName}: Duration {duration:F2}s, {width}x{height} @ {fps:F3}fps"); + LogInfo($"{baseName}: Target duration: {target:F2}s"); LogInfo($"{baseName}: Segments: {segments}"); - LogInfo($"{baseName}: Equal segment length: {segmentLength:F3}s"); + LogInfo(job.ForceFixed + ? $"{baseName}: Fixed segment length: {segmentLength:F2}s (last may be shorter)" + : $"{baseName}: Equalized segment length: {segmentLength:F2}s"); + + if (cmd.Master.EstimateOnly) + return []; Func processorFactory; if (job.Crop != null) @@ -141,7 +140,7 @@ static class Program "body" => new YoloOnnxObjectDetector(_logger), _ => throw new InvalidOperationException($"Unknown detector: {job.Detect}") }; - return new TrackingSplitter(i, job.Crop.Value.width, job.Crop.Value.height, cmd.Master.Debug, cmd.Master.PlainText, detector, job, _logger); + return new TrackingSplitter(i, detector, job, _logger); }; } else @@ -160,7 +159,10 @@ static class Program SegmentLength : (i == segments - 1) ? Math.Max(0.1, duration - i * segmentLength) : segmentLength, - ProcessorFactory : processorFactory + ProcessorFactory : processorFactory, + VideoWidth : width, + VideoHeight : height, + VideoFps : fps ) ) .ToList(); @@ -195,28 +197,6 @@ static class Program // ffprobe // ----------------------------- - static double GetDuration(string inputFile) - { - var psi = new ProcessStartInfo - { - FileName = "ffprobe", - Arguments = $"-v error -show_entries format=duration -of csv=p=0 \"{inputFile}\"", - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffprobe."); - var output = proc.StandardOutput.ReadToEnd(); - proc.WaitForExit(); - - if (output != null && - double.TryParse(output, NumberStyles.Any, CultureInfo.InvariantCulture, out var duration)) - return duration; - - return -1; - } - // ----------------------------- // Multi-threaded splitting // ----------------------------- @@ -252,15 +232,7 @@ static class Program while (!freeSlots.TryDequeue(out slot)) await Task.Yield(); - await ProcessSegment( - job.ProcessorFactory, - job.Job.InputFile, - job.OutputFileName, - job.Job.Passthrough, - slot + 1, // <-- slot instead of SegmentIndex (+1 for totals) - job.SegmentStart, - job.SegmentLength - ); + await ProcessSegment(job,slot + 1); var processed = Interlocked.Increment(ref processedSegments); var elapsed = sw.Elapsed; @@ -291,25 +263,25 @@ static class Program { foreach (var job in jobs) { - await ProcessSegment( - job.ProcessorFactory, - job.Job.InputFile, - job.OutputFileName, - job.Job.Passthrough, - job.SegmentIndex, - job.SegmentStart, - job.SegmentLength - ); + await ProcessSegment(job, 0); } } - private static async Task ProcessSegment(Func processorFactory, string inputFile, string outputFileName, string[] passthrough, int index, double start, double length) + private static async Task ProcessSegment(SingleTask t, int slot) { - var processor = processorFactory(index); + var processor = t.ProcessorFactory(slot); try { - await processor.ProcessSegment(inputFile, outputFileName, start, length, passthrough); + await processor.ProcessSegment( + t.Job.InputFile, + t.OutputFileName, + t.SegmentStart, + t.SegmentLength, + t.VideoWidth, + t.VideoHeight, + t.VideoFps, + t.Job.Passthrough); } finally { @@ -345,4 +317,73 @@ 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); + } + + }