Fixed audio/video sync. Single ffprobe call.

This commit is contained in:
Alexander Shabarshov 2026-05-13 23:40:19 +01:00
parent 1dac08d21a
commit 44d817f17f
5 changed files with 348 additions and 248 deletions

View File

@ -6,5 +6,5 @@ namespace splitter;
public interface ISegmentProcessor 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);
} }

View File

@ -9,17 +9,7 @@ namespace splitter;
public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, segmentNo), ISegmentProcessor 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) public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, int videoWidth, int videoHeight, double fps, string[] passthrough)
{
RunFFmpegSegment(inputFile, outputFile, start, length, passthrough);
}
private void RunFFmpegSegment(
string inputFile,
string outputFile,
double start,
double length,
string[] passthrough)
{ {
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : ""; var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";

View File

@ -27,6 +27,8 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
private Task? _uiTask; private Task? _uiTask;
private Task? _inputTask; private Task? _inputTask;
private int _numberOfProcesses = 1;
private const int _maxLogEntries = 500;
// Public configuration // Public configuration
public string Title { get; set; } = string.Empty; 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 ---- // ---- ILogger ----
@ -93,8 +87,8 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
{ {
lock (_sync) lock (_sync)
{ {
if (_logs.Count >= MaxLogEntries) if (_logs.Count >= _maxLogEntries)
_logs.RemoveRange(0, _logs.Count - MaxLogEntries + 1); _logs.RemoveRange(0, _logs.Count - _maxLogEntries + 1);
_logs.Add(new LogEntry( _logs.Add(new LogEntry(
DateTime.Now, DateTime.Now,
@ -137,7 +131,7 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable
ctx.UpdateTarget(BuildRoot()); ctx.UpdateTarget(BuildRoot());
await Task.Delay(100, token); await Task.Delay(100, token);
} }
catch ( Exception ex ) catch ( Exception )
{ {
break; break;
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenCvSharp; using OpenCvSharp;
@ -9,21 +10,16 @@ namespace splitter;
public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable 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 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) public TrackingSplitter(
: base(logger, segmentNo) int progressLine,
IObjectDetector detector,
SingleJob cmd,
ILogger logger)
: base(logger, progressLine)
{ {
_cropWidth = cropWidth;
_cropHeight = cropHeight;
_debugOverlay = debugOverlay;
_plainText = plainText;
_detector = detector; _detector = detector;
_cmd = cmd; _cmd = cmd;
} }
@ -34,146 +30,287 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
d.Dispose(); d.Dispose();
} }
public async Task ProcessSegment(string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters) public async Task ProcessSegment(
string inputFile,
string outputFile,
double start,
double length,
int videoWidth, int videoHeight, double fps,
string[] ffmpegPassthroughParameters)
{ {
using var capture = new VideoCapture(inputFile); var name = Path.GetFileNameWithoutExtension(outputFile);
if (!capture.IsOpened())
// 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; return;
} }
var name = Path.GetFileNameWithoutExtension(outputFile); if (_cmd.Crop == null)
var skip = TimeSpan.FromSeconds(start); {
var duration = TimeSpan.FromSeconds(length); 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); LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
var videoHeight = (int)capture.Get(VideoCaptureProperties.FrameHeight);
var fps = capture.Get(VideoCaptureProperties.Fps);
var totalFrames = (int)(length * fps);
var originalCropWidth = _cropWidth; // 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
var originalCropHeight = _cropHeight; 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}"); // 3) Start FFmpeg encode (video from stdin + audio from original)
var encode = StartFfmpegEncode(
var encWidth = _debugOverlay ? videoWidth : originalCropWidth;
var encHeight = _debugOverlay ? videoHeight : originalCropHeight;
var ffmpeg = StartFfmpegNvenc(
inputFile, inputFile,
outputFile, outputFile,
start,
length,
encWidth, encWidth,
encHeight, encHeight,
fps, fps,
skip,
ffmpegPassthroughParameters); ffmpegPassthroughParameters);
using var stdin = ffmpeg.StandardInput.BaseStream; using var encodeStdin = encode.StandardInput.BaseStream;
using var frame = new Mat(); // Separate input/output sizes and buffers
using var outputBgr = new Mat(encHeight, encWidth, MatType.CV_8UC3); var inBytes = videoWidth * videoHeight * 3;
var outBytes = encWidth * encHeight * 3;
var frameBytes = encWidth * encHeight * 3; var inBuffer = new byte[inBytes];
var videoBuffer = new byte[frameBytes]; 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(); var kalman = new KalmanTracker();
// initial reset is now done inside CameraController
var camera = new CameraController( var camera = new CameraController(
videoWidth, videoWidth,
videoHeight, videoHeight,
originalCropWidth, _cmd.Crop.Value.width,
originalCropHeight, _cmd.Crop.Value.height,
kalman, 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; break;
Rect? objectBox = null; // input frame → Mat
Point2f? objectCenter = null; 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); var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
camera.Update(primary); camera.Update(primary);
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; var roi = camera.Roi;
if (_debugOverlay) if (_cmd.Debug)
{ {
if (objectBox.HasValue) DrawDebug(frameMat, objects, camera, kalman);
{ frameMat.CopyTo(outMat);
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);
} }
else else
{ {
using var cropped = new Mat(frame, roi); using var cropped = new Mat(frameMat, roi);
cropped.CopyTo(outputBgr); cropped.CopyTo(outMat);
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
stdin.Write(videoBuffer, 0, frameBytes);
} }
// output Mat → outBuffer
Marshal.Copy(outMat.Data, outBuffer, 0, outBytes);
encodeStdin.Write(outBuffer, 0, outBytes);
var elapsed = DateTime.UtcNow - startTime; var elapsed = DateTime.UtcNow - startTime;
var progress = (double)i / totalFrames; var progress = totalFrames > 0 ? (double)frameIndex / totalFrames : 0.0;
var speed = i > 0 ? (i / elapsed.TotalSeconds)/fps : 0.0; var speed = elapsed.TotalSeconds > 0 ? (frameIndex / elapsed.TotalSeconds) / fps : 0.0;
var remainingFrames = totalFrames - i; var remainingFrames = Math.Max(totalFrames - frameIndex, 0);
var etaSeconds = speed > 0 ? remainingFrames / speed : 0; var etaSeconds = speed > 0 ? remainingFrames / speed : 0.0;
var eta = TimeSpan.FromSeconds(etaSeconds); var eta = TimeSpan.FromSeconds(etaSeconds);
DrawProgress(name, progress, eta, speed); DrawProgress(name, progress, eta, speed);
} }
stdin.Flush(); encodeStdin.Flush();
stdin.Close();
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(); ClearProgress();
if (ffmpeg.ExitCode != 0)
LogError($"{Path.GetFileName(outputFile)}: Segment {name} FFmpeg encoding failed"); if (encode.ExitCode != 0)
LogError($"{name}: FFmpeg encoding failed");
else 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( private (Rect box, Point2f center)? SelectTrackedObject(
@ -185,7 +322,6 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
if (!previousCenter.HasValue) if (!previousCenter.HasValue)
{ {
// Largest area
var bestIndex = 0; var bestIndex = 0;
var bestArea = float.MinValue; var bestArea = float.MinValue;
@ -204,7 +340,6 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
} }
else else
{ {
// Closest to previous center
var prev = previousCenter.Value; var prev = previousCenter.Value;
var bestIndex = 0; var bestIndex = 0;
var bestDist2 = float.MaxValue; var bestDist2 = float.MaxValue;
@ -226,64 +361,4 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
return foundObjects[bestIndex]; 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);
}
} }

View File

@ -16,6 +16,9 @@ static class Program
int TotalSegments, int TotalSegments,
double SegmentStart, double SegmentStart,
double SegmentLength, double SegmentLength,
int VideoWidth,
int VideoHeight,
double VideoFps,
Func<int, ISegmentProcessor> ProcessorFactory Func<int, ISegmentProcessor> ProcessorFactory
); );
@ -38,7 +41,6 @@ static class Program
var logger = new SpectreConsoleLogger var logger = new SpectreConsoleLogger
{ {
Title = "Splitter", Title = "Splitter",
NumberOfProcesses = cmd.Master.SingleThreaded ? 1 : Math.Max(1, Environment.ProcessorCount / 2) + 1
}; };
_logger = logger; _logger = logger;
@ -89,7 +91,7 @@ static class Program
job.Mask ??= $"{baseName}_seg%03d.mp4"; job.Mask ??= $"{baseName}_seg%03d.mp4";
LogInfo($"{baseName}: Reading duration via ffprobe..."); 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) if (duration <= 0)
{ {
LogError($"{baseName}: Could not read duration."); LogError($"{baseName}: Could not read duration.");
@ -115,20 +117,17 @@ static class Program
} }
if (cmd.Master.EstimateOnly) if (cmd.Master.EstimateOnly)
{
LogInfo("=== ESTIMATE MODE ==="); LogInfo("=== ESTIMATE MODE ===");
LogInfo($"{baseName}: Total 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}: Target duration: {target:F2}s");
LogInfo($"{baseName}: Segments: {segments}"); LogInfo($"{baseName}: Segments: {segments}");
LogInfo(job.ForceFixed LogInfo(job.ForceFixed
? $"{baseName}: Fixed segment length: {segmentLength:F2}s (last may be shorter)" ? $"{baseName}: Fixed segment length: {segmentLength:F2}s (last may be shorter)"
: $"{baseName}: Equalized segment length: {segmentLength:F2}s"); : $"{baseName}: Equalized segment length: {segmentLength:F2}s");
return [];
}
LogInfo($"{baseName}: Duration: {duration:F2}s"); if (cmd.Master.EstimateOnly)
LogInfo($"{baseName}: Segments: {segments}"); return [];
LogInfo($"{baseName}: Equal segment length: {segmentLength:F3}s");
Func<int, ISegmentProcessor> processorFactory; Func<int, ISegmentProcessor> processorFactory;
if (job.Crop != null) if (job.Crop != null)
@ -141,7 +140,7 @@ static class Program
"body" => new YoloOnnxObjectDetector(_logger), "body" => new YoloOnnxObjectDetector(_logger),
_ => throw new InvalidOperationException($"Unknown detector: {job.Detect}") _ => 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 else
@ -160,7 +159,10 @@ static class Program
SegmentLength : (i == segments - 1) SegmentLength : (i == segments - 1)
? Math.Max(0.1, duration - i * segmentLength) ? Math.Max(0.1, duration - i * segmentLength)
: segmentLength, : segmentLength,
ProcessorFactory : processorFactory ProcessorFactory : processorFactory,
VideoWidth : width,
VideoHeight : height,
VideoFps : fps
) )
) )
.ToList(); .ToList();
@ -195,28 +197,6 @@ static class Program
// ffprobe // 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 // Multi-threaded splitting
// ----------------------------- // -----------------------------
@ -252,15 +232,7 @@ static class Program
while (!freeSlots.TryDequeue(out slot)) while (!freeSlots.TryDequeue(out slot))
await Task.Yield(); await Task.Yield();
await ProcessSegment( await ProcessSegment(job,slot + 1);
job.ProcessorFactory,
job.Job.InputFile,
job.OutputFileName,
job.Job.Passthrough,
slot + 1, // <-- slot instead of SegmentIndex (+1 for totals)
job.SegmentStart,
job.SegmentLength
);
var processed = Interlocked.Increment(ref processedSegments); var processed = Interlocked.Increment(ref processedSegments);
var elapsed = sw.Elapsed; var elapsed = sw.Elapsed;
@ -291,25 +263,25 @@ static class Program
{ {
foreach (var job in jobs) foreach (var job in jobs)
{ {
await ProcessSegment( await ProcessSegment(job, 0);
job.ProcessorFactory,
job.Job.InputFile,
job.OutputFileName,
job.Job.Passthrough,
job.SegmentIndex,
job.SegmentStart,
job.SegmentLength
);
} }
} }
private static async Task ProcessSegment(Func<int, ISegmentProcessor> 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 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 finally
{ {
@ -345,4 +317,73 @@ static class Program
return Path.Combine(folder, fileName); 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);
}
} }