diff --git a/CommandLine.cs b/CommandLine.cs
index 50a7720..6bf3758 100644
--- a/CommandLine.cs
+++ b/CommandLine.cs
@@ -18,6 +18,7 @@ public sealed class CommandLine
public bool PlainText { get; private init; }
public bool EstimateOnly { get; private init; }
public bool ForceFixed { get; private init; }
+ public bool SingleThreaded { get; private init; }
public bool IsValid => !string.IsNullOrEmpty(InputFile) && !string.IsNullOrEmpty(OutputFolder);
@@ -79,6 +80,10 @@ public sealed class CommandLine
{
Debug = true;
}
+ else if (arg == "--single-thread")
+ {
+ SingleThreaded = true;
+ }
else if (arg.StartsWith("--duration="))
{
var dur = arg.Substring("--duration=".Length);
@@ -198,6 +203,9 @@ Options:
--text Display log in plain text.
+ --single-thread Run in single-threaded mode (no parallel ffmpeg processes).
+ Useful for debugging or if system is resource-constrained.
+
--debug Show debug overlay during face tracking.
Passthrough:
diff --git a/ISegmentProcessor.cs b/ISegmentProcessor.cs
new file mode 100644
index 0000000..4f2979e
--- /dev/null
+++ b/ISegmentProcessor.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace splitter;
+
+public interface ISegmentProcessor
+{
+ Task ProcessSegment( string inputFile, string outputFile, double start, double length, string[] ffmpegPassthroughParameters);
+}
diff --git a/Logger.cs b/Logger.cs
new file mode 100644
index 0000000..e6673a5
--- /dev/null
+++ b/Logger.cs
@@ -0,0 +1,68 @@
+namespace splitter;
+
+public static class Logger
+{
+ static int _logLines = 0;
+ static readonly object _consoleLock = new();
+
+ public static bool PlainText { get; set; }
+
+ public static void Log(string prefix, ConsoleColor color, string msg)
+ {
+ lock (_consoleLock)
+ {
+ if (PlainText)
+ {
+ Console.WriteLine($"{prefix} {msg}");
+ }
+ else
+ {
+ Console.ForegroundColor = color;
+ Console.WriteLine($"{prefix} {msg}");
+ Console.ResetColor();
+ _logLines++;
+ }
+ }
+ }
+
+ public static void LogInfo(string msg) => Log("[INFO]", ConsoleColor.Cyan, msg);
+ public static void LogSuccess(string msg) => Log("[ OK ]", ConsoleColor.Green, msg);
+ public static void LogWarn(string msg) => Log("[WARN]", ConsoleColor.Yellow, msg);
+ public static void LogError(string msg) => Log("[ERR ]", ConsoleColor.Red, msg);
+
+ public static void DrawProgress(int progressLevel, double progress, TimeSpan eta, double speed)
+ {
+ if (PlainText || progressLevel < 0)
+ return;
+
+ lock (_consoleLock)
+ {
+ var width = Math.Max(20, Console.WindowWidth - 20);
+ var filled = (int)(progress * width);
+ if (filled < 0) filled = 0;
+ if (filled > width) filled = width;
+
+ var barLine = _logLines + 1 + progressLevel*2;
+ var infoLine = _logLines + 2 + progressLevel*2;
+
+ // Progress bar with 24-bit color (green)
+ Console.SetCursorPosition(0, barLine);
+ Console.Write("\u001b[38;2;0;255;0m[");
+ Console.Write(new string('#', filled));
+ Console.Write(new string('-', width - filled));
+ Console.Write("]\u001b[0m");
+
+ // Info line: percentage, ETA, speed
+ Console.SetCursorPosition(0, infoLine);
+ var etaStr = eta.TotalSeconds < 0 || double.IsInfinity(eta.TotalSeconds)
+ ? "ETA: --:--"
+ : $"ETA: {eta:mm\\:ss}";
+ var speedStr = double.IsNaN(speed) || double.IsInfinity(speed)
+ ? "Speed: -.-x"
+ : $"Speed: {speed:F2}x";
+
+ var info = $"{progress * 100:0.0}% {etaStr} {speedStr} ";
+ Console.Write("\u001b[38;2;180;180;180m" + info.PadRight(Console.WindowWidth - 1) + "\u001b[0m");
+ }
+ }
+}
diff --git a/LoggingBase.cs b/LoggingBase.cs
index 04641e8..1edb6b9 100644
--- a/LoggingBase.cs
+++ b/LoggingBase.cs
@@ -1,20 +1,20 @@
using System;
-using System.Collections.Generic;
-using System.Text;
-
namespace splitter;
-public class LoggingBase(
- Action log,
- Action drawProgress
- )
+public abstract class LoggingBase(int progressLine)
{
- protected Action Log = log;
- protected Action DrawProgress = drawProgress;
+ protected void Log(string level, ConsoleColor color, string message)
+ => Logger.Log(level, color, message);
- protected void LogInfo(string msg) => Log("[INFO]", ConsoleColor.Cyan, msg);
- protected void LogSuccess(string msg) => Log("[ OK ]", ConsoleColor.Green, msg);
- protected void LogWarn(string msg) => Log("[WARN]", ConsoleColor.Yellow, msg);
- protected void LogError(string msg) => Log("[ERR ]", ConsoleColor.Red, msg);
+ protected void LogInfo(string message)
+ => Logger.LogInfo(message);
+ protected void LogWarn(string message)
+ => Logger.LogWarn(message);
+
+ protected void LogError(string message)
+ => Logger.LogError(message);
+
+ protected void DrawProgress(double percent, TimeSpan eta, double fps)
+ => Logger.DrawProgress(progressLine, percent, eta, fps);
}
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
index 5ffc2d4..0be7d17 100644
--- a/Properties/launchSettings.json
+++ b/Properties/launchSettings.json
@@ -10,7 +10,7 @@
},
"Debug": {
"commandName": "Project",
- "commandLineArgs": "\"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\20260426_212004.mp4\" \"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\Shorts\" --crop --detect=body --debug --text"
+ "commandLineArgs": "\"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\20260426_212004.mp4\" \"C:\\Users\\uncls\\Pictures\\2026\\2026 - Secret Rule\\Shorts\" --crop --detect=body --debug --single-thread --text"
}
}
}
\ No newline at end of file
diff --git a/SimpleSplitter.cs b/SimpleSplitter.cs
new file mode 100644
index 0000000..a589259
--- /dev/null
+++ b/SimpleSplitter.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+
+namespace splitter;
+
+public class SimpleSplitter(int segmentNo) : LoggingBase(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)
+ {
+ var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
+
+ var args =
+ $"-ss {start.ToString(CultureInfo.InvariantCulture)} " +
+ $"-i \"{inputFile}\" " +
+ $"-t {length.ToString(CultureInfo.InvariantCulture)} " +
+ $"-c copy {pass} \"{outputFile}\" -y";
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "ffmpeg",
+ Arguments = args,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
+
+ ShowFFMpegProgress(length, proc);
+
+ proc.WaitForExit();
+ }
+
+ private void ShowFFMpegProgress(double length, Process proc)
+ {
+ var sw = Stopwatch.StartNew();
+
+ string? line;
+ while ((line = proc.StandardError.ReadLine()) != null)
+ {
+ // Look for "time=00:00:03.52"
+ var idx = line.IndexOf("time=", StringComparison.OrdinalIgnoreCase);
+ if (idx < 0)
+ continue;
+
+ var timeStr = ExtractTimestamp(line, idx + 5);
+ if (timeStr == null)
+ continue;
+
+ if (!TryParseFfmpegTime(timeStr, out var current))
+ continue;
+
+ var progress = current.TotalSeconds / length;
+ if (progress < 0) progress = 0;
+ if (progress > 1) progress = 1;
+
+ var elapsed = sw.Elapsed;
+ var speed = current.TotalSeconds > 0
+ ? current.TotalSeconds / elapsed.TotalSeconds
+ : 0;
+
+ var remaining = length - current.TotalSeconds;
+ var etaSeconds = speed > 0 ? remaining / speed : remaining;
+ var eta = TimeSpan.FromSeconds(etaSeconds);
+
+ DrawProgress(progress, eta, speed);
+ }
+ }
+
+ private static string? ExtractTimestamp(string line, int startIndex)
+ {
+ // FFmpeg formats: HH:MM:SS.xx
+ // We read until whitespace
+ int end = startIndex;
+ while (end < line.Length && !char.IsWhiteSpace(line[end]))
+ end++;
+
+ if (end <= startIndex)
+ return null;
+
+ return line[startIndex..end];
+ }
+
+ private static bool TryParseFfmpegTime(string s, out TimeSpan ts)
+ {
+ // FFmpeg uses "00:00:03.52"
+ return TimeSpan.TryParseExact(
+ s,
+ @"hh\:mm\:ss\.ff",
+ CultureInfo.InvariantCulture,
+ out ts);
+ }
+
+}
diff --git a/TrackingSplitter.cs b/TrackingSplitter.cs
index 81d6814..3030c24 100644
--- a/TrackingSplitter.cs
+++ b/TrackingSplitter.cs
@@ -7,49 +7,65 @@ using OpenCvSharp;
namespace splitter;
-public class TrackingSplitter(
- Action log,
- Action drawProgress
- ) : LoggingBase(log, drawProgress)
+public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
{
- public async Task TrackAndExtract(
- string srcFileName,
- string destFileName,
- IObjectDetector detector,
- TimeSpan skip,
- TimeSpan duration,
- int cropWidth,
- int cropHeight,
- string[] passthrough,
- bool debugOverlay)
+ private readonly int _segmentNo;
+ private readonly int _cropWidth;
+ private readonly int _cropHeight;
+ private readonly bool _debugOverlay;
+ private readonly bool _plainText;
+
+ private readonly IObjectDetector _detector;
+
+ public TrackingSplitter(int segmentNo, int cropWidth, int cropHeight, bool debugOverlay, bool plainText, IObjectDetector detector)
+ : base(segmentNo)
{
- using var capture = new VideoCapture(srcFileName);
+ _segmentNo = segmentNo;
+ _cropWidth = cropWidth;
+ _cropHeight = cropHeight;
+ _debugOverlay = debugOverlay;
+ _plainText = plainText;
+ _detector = detector;
+ }
+
+ public void Dispose()
+ {
+ if (_detector is IDisposable d)
+ 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())
throw new Exception("Cannot open video");
- capture.Set(VideoCaptureProperties.PosMsec, skip.TotalMilliseconds);
+ var skip = TimeSpan.FromSeconds(start);
+ var duration = TimeSpan.FromSeconds(length);
+
+ capture.Set(VideoCaptureProperties.PosMsec, start);
var videoWidth = (int)capture.Get(VideoCaptureProperties.FrameWidth);
var videoHeight = (int)capture.Get(VideoCaptureProperties.FrameHeight);
var fps = capture.Get(VideoCaptureProperties.Fps);
- var totalFrames = (int)(duration.TotalSeconds * fps);
+ var totalFrames = (int)(length * fps);
- var originalCropWidth = cropWidth;
- var originalCropHeight = cropHeight;
+ var originalCropWidth = _cropWidth;
+ var originalCropHeight = _cropHeight;
Console.WriteLine($"[TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}");
- var encWidth = debugOverlay ? videoWidth : originalCropWidth;
- var encHeight = debugOverlay ? videoHeight : originalCropHeight;
+ var encWidth = _debugOverlay ? videoWidth : originalCropWidth;
+ var encHeight = _debugOverlay ? videoHeight : originalCropHeight;
var ffmpeg = StartFfmpegNvenc(
- srcFileName,
- destFileName,
- encWidth,
- encHeight,
- fps,
- skip,
- passthrough);
+ inputFile,
+ outputFile,
+ encWidth,
+ encHeight,
+ fps,
+ skip,
+ ffmpegPassthroughParameters);
using var stdin = ffmpeg.StandardInput.BaseStream;
@@ -63,12 +79,12 @@ public class TrackingSplitter(
// initial reset is now done inside CameraController
var camera = new CameraController(
- videoWidth,
- videoHeight,
- originalCropWidth,
- originalCropHeight,
- kalman
- );
+ videoWidth,
+ videoHeight,
+ originalCropWidth,
+ originalCropHeight,
+ kalman
+ );
var startTime = DateTime.UtcNow;
@@ -80,7 +96,7 @@ public class TrackingSplitter(
Rect? objectBox = null;
Point2f? objectCenter = null;
- var objects = detector.DetectAll(frame, videoWidth, videoHeight);
+ var objects = _detector.DetectAll(frame, videoWidth, videoHeight);
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
camera.Update(primary);
@@ -94,7 +110,7 @@ public class TrackingSplitter(
var lostFrames = camera.LostFrames;
var roi = camera.Roi;
- if (debugOverlay)
+ if (_debugOverlay)
{
if (objectBox.HasValue)
{
@@ -117,7 +133,7 @@ public class TrackingSplitter(
DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White);
}
- if (debugOverlay)
+ if (_debugOverlay)
{
frame.CopyTo(outputBgr);
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
@@ -134,7 +150,7 @@ public class TrackingSplitter(
var elapsed = DateTime.UtcNow - startTime;
var progress = (double)i / totalFrames;
- var speed = i > 0 ? i / elapsed.TotalSeconds : 0.0;
+ var speed = i > 0 ? (i / elapsed.TotalSeconds)/fps : 0.0;
var remainingFrames = totalFrames - i;
var etaSeconds = speed > 0 ? remainingFrames / speed : 0;
var eta = TimeSpan.FromSeconds(etaSeconds);
@@ -147,7 +163,9 @@ public class TrackingSplitter(
await ffmpeg.WaitForExitAsync();
if (ffmpeg.ExitCode != 0)
- throw new Exception("FFmpeg NVENC encoding failed");
+ LogError($"Segment {_segmentNo} FFmpeg encoding failed");
+ else
+ LogInfo($"Segment {_segmentNo} processing completed");
}
private (Rect box, Point2f center)? SelectTrackedObject(
@@ -243,7 +261,10 @@ public class TrackingSplitter(
{
string? line;
while ((line = process.StandardError.ReadLine()) != null)
- Console.WriteLine($"[ffmpeg] {line}");
+ {
+ if (_plainText)
+ Console.WriteLine($"[ffmpeg] {line}");
+ }
}
catch { }
});
@@ -256,4 +277,5 @@ public class TrackingSplitter(
Cv2.PutText(img, text, new Point(x, y),
HersheyFonts.HersheySimplex, 0.6, color, 2);
}
+
}
diff --git a/UltraFaceDetector.cs b/UltraFaceDetector.cs
index 3f4420e..e3a1a8f 100644
--- a/UltraFaceDetector.cs
+++ b/UltraFaceDetector.cs
@@ -9,10 +9,7 @@ public sealed class UltraFaceDetector: LoggingBase, IDisposable, IObjectDetector
{
private readonly UltraFace _ultraFace;
- public UltraFaceDetector(
- Action log,
- Action drawProgress
- ) : base(log, drawProgress)
+ public UltraFaceDetector() : base(-1)
{
var basePath = AppDomain.CurrentDomain.BaseDirectory;
var param = new UltraFaceParameter
diff --git a/YoloOnnxObjectDetector.cs b/YoloOnnxObjectDetector.cs
index 577e4fa..a8553d4 100644
--- a/YoloOnnxObjectDetector.cs
+++ b/YoloOnnxObjectDetector.cs
@@ -51,18 +51,15 @@ public sealed class YoloOnnxObjectDetector : LoggingBase, IObjectDetector, IDisp
public Detection(float x, float y, float w, float h, float score)
{
- X = x;
- Y = y;
- Width = w;
+ X = x;
+ Y = y;
+ Width = w;
Height = h;
- Score = score;
+ Score = score;
}
}
- public YoloOnnxObjectDetector(
- Action log,
- Action drawProgress
- ) : base(log, drawProgress)
+ public YoloOnnxObjectDetector() : base(-1)
{
var options = new SessionOptions();
options.AppendExecutionProvider_DML();
@@ -75,8 +72,8 @@ public sealed class YoloOnnxObjectDetector : LoggingBase, IObjectDetector, IDisp
_inputName = _session.InputMetadata.Keys.First();
_outputName = _session.OutputMetadata.Keys.First();
- foreach (var kv in _session.OutputMetadata)
- LogInfo($"[YoloOnnx] {kv.Key}: {string.Join(",", kv.Value.Dimensions)} {kv.Value.ElementType}");
+ //foreach (var kv in _session.OutputMetadata)
+ // LogInfo($"[YoloOnnx] {kv.Key}: {string.Join(",", kv.Value.Dimensions)} {kv.Value.ElementType}");
// Preallocate tensor buffer (fixed size for lifetime)
_inputBuffer = new float[1 * 3 * _inputHeight * _inputWidth];
diff --git a/splitter.cs b/splitter.cs
index 50e21a3..8760bfa 100644
--- a/splitter.cs
+++ b/splitter.cs
@@ -5,12 +5,7 @@ using splitter;
static class Program
{
- static int _logLines = 0;
- static bool _plainText = false;
- static readonly object _consoleLock = new();
- static bool _progressRunning = true;
-
- static void Main(string[] args)
+ static async Task Main(string[] args)
{
var cmd = new CommandLine(args);
@@ -24,7 +19,7 @@ static class Program
var debug = cmd.Debug;
string? detect = cmd.Detect;
double? overrideTargetDuration = cmd.OverrideTargetDuration;
- _plainText = cmd.PlainText;
+ Logger.PlainText = cmd.PlainText;
if (!File.Exists(inputFile))
{
@@ -81,90 +76,46 @@ static class Program
LogInfo($"Segments: {segments}");
LogInfo($"Equal segment length: {segmentLength:F3}s");
+ Func processorFactory;
if (crop != null)
{
- LogInfo("Starting multi-threaded face tracking crop and splitting...");
- RunMultiThreadedCrop(inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough, crop.Value.width, crop.Value.height, debug, detect);
+ processorFactory = i =>
+ {
+ IObjectDetector detector = detect switch
+ {
+ "face" => new UltraFaceDetector(),
+ "body" => new YoloOnnxObjectDetector(),
+ _ => throw new InvalidOperationException($"Unknown detector: {detect}")
+ };
+ return new TrackingSplitter(i, crop.Value.width, crop.Value.height, debug, cmd.PlainText, detector);
+ };
}
else
{
- LogInfo("Starting multi-threaded ffmpeg splitting...");
- RunMultiThreadedSplit(inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough);
+ processorFactory = i => new SimpleSplitter(i);
+ }
+ if (cmd.SingleThreaded)
+ {
+ LogInfo("Starting single-threaded splitting...");
+ await RunSingleThreaded(processorFactory, inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough);
+ }
+ else
+ {
+ LogInfo("Starting multi-threaded splitting...");
+ await RunMultiThreaded(processorFactory, inputFile, outputFolder, outputMask, duration, segments, segmentLength, passthrough);
}
- LogSuccess("Done.");
- _progressRunning = false;
- // Move cursor below progress area
- lock (_consoleLock)
- {
- Console.SetCursorPosition(0, _logLines + 4);
- Console.WriteLine();
- }
+ LogInfo("Done.");
}
+ private static void LogInfo(string message)
+ => Logger.LogInfo(message);
- // -----------------------------
- // Logging + Progress UI
- // -----------------------------
+ private static void LogWarn(string message)
+ => Logger.LogWarn(message);
- static void Log(string prefix, ConsoleColor color, string msg)
- {
- lock (_consoleLock)
- {
- if (_plainText)
- {
- Console.WriteLine($"{prefix} {msg}");
- }
- else
- {
- Console.ForegroundColor = color;
- Console.WriteLine($"{prefix} {msg}");
- Console.ResetColor();
- _logLines++;
- }
- }
- }
-
- static void LogInfo(string msg) => Log("[INFO]", ConsoleColor.Cyan, msg);
- static void LogSuccess(string msg) => Log("[ OK ]", ConsoleColor.Green, msg);
- static void LogWarn(string msg) => Log("[WARN]", ConsoleColor.Yellow, msg);
- static void LogError(string msg) => Log("[ERR ]", ConsoleColor.Red, msg);
-
- static void DrawProgress(double progress, TimeSpan eta, double speed)
- {
- if ( _plainText )
- return;
-
- lock (_consoleLock)
- {
- var width = Math.Max(20, Console.WindowWidth - 20);
- var filled = (int)(progress * width);
- if (filled < 0) filled = 0;
- if (filled > width) filled = width;
-
- var barLine = _logLines + 1;
- var infoLine = _logLines + 2;
-
- // Progress bar with 24-bit color (green)
- Console.SetCursorPosition(0, barLine);
- Console.Write("\u001b[38;2;0;255;0m[");
- Console.Write(new string('#', filled));
- Console.Write(new string('-', width - filled));
- Console.Write("]\u001b[0m");
-
- // Info line: percentage, ETA, speed
- Console.SetCursorPosition(0, infoLine);
- var etaStr = eta.TotalSeconds < 0 || double.IsInfinity(eta.TotalSeconds)
- ? "ETA: --:--"
- : $"ETA: {eta:mm\\:ss}";
- var speedStr = double.IsNaN(speed) || double.IsInfinity(speed)
- ? "Speed: -.-x"
- : $"Speed: {speed:F2}x";
-
- var info = $"{progress * 100:0.0}% {etaStr} {speedStr} ";
- Console.Write("\u001b[38;2;180;180;180m" + info.PadRight(Console.WindowWidth - 1) + "\u001b[0m");
- }
- }
+ private static void LogError(string message)
+ => Logger.LogError(message);
// -----------------------------
// ffprobe
@@ -196,7 +147,8 @@ static class Program
// Multi-threaded splitting
// -----------------------------
- static void RunMultiThreadedSplit(
+ static async Task RunMultiThreaded(
+ Func processorFactory,
string inputFile,
string outputFolder,
string mask,
@@ -216,52 +168,32 @@ static class Program
})
.ToList();
- var completed = 0;
- var sw = Stopwatch.StartNew();
-
- // Progress thread
- var progressThread = new Thread(() =>
- {
- while (_progressRunning)
- {
- var progress = segments == 0 ? 0 : (double)completed / segments;
- var processedSeconds = completed * segmentLength;
- var speed = sw.Elapsed.TotalSeconds > 0
- ? processedSeconds / sw.Elapsed.TotalSeconds
- : 0;
-
- var remainingSeconds = (totalDuration - processedSeconds) / Math.Max(speed, 0.0001);
- if (remainingSeconds < 0) remainingSeconds = 0;
- var eta = TimeSpan.FromSeconds(remainingSeconds);
-
- DrawProgress(progress, eta, speed);
- Thread.Sleep(200);
- }
- })
- {
- IsBackground = true
- };
- progressThread.Start();
-
var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
+ using var sem = new SemaphoreSlim(maxDegree);
+ var tasks = new List();
- Parallel.ForEach(
- jobs,
- new ParallelOptions { MaxDegreeOfParallelism = maxDegree },
- job =>
+ foreach (var job in jobs)
+ {
+ await sem.WaitAsync();
+
+ tasks.Add(Task.Run(async () =>
{
- var outputFile = BuildOutputFileName(outputFolder, mask, job.Index);
- RunFFmpegSegment(inputFile, outputFile, job.Start, job.Length, passthrough);
- Interlocked.Increment(ref completed);
- });
+ try
+ {
+ await ProcessSegment(processorFactory, inputFile, outputFolder, mask, passthrough, job.Index, job.Start, job.Length);
+ }
+ finally
+ {
+ sem.Release();
+ }
+ }));
+ }
- sw.Stop();
- _progressRunning = false;
- progressThread.Join();
- DrawProgress(1.0, TimeSpan.Zero, totalDuration / Math.Max(sw.Elapsed.TotalSeconds, 0.0001));
+ await Task.WhenAll(tasks);
}
- static void RunSingleThreadedSplit(
+ static async Task RunSingleThreaded(
+ Func processorFactory,
string inputFile,
string outputFolder,
string mask,
@@ -271,157 +203,38 @@ static class Program
string[] passthrough)
{
var jobs = Enumerable.Range(0, segments)
- .Select(i => new
- {
- Index = i,
- Start = i * segmentLength,
- Length = (i == segments - 1)
- ? Math.Max(0.1, totalDuration - i * segmentLength)
- : segmentLength
- })
- .ToList();
-
- var completed = 0;
- var sw = Stopwatch.StartNew();
-
- // Progress thread
- var progressThread = new Thread(() =>
- {
- while (_progressRunning)
+ .Select(i => new
{
- var progress = segments == 0 ? 0 : (double)completed / segments;
- var processedSeconds = completed * segmentLength;
- var speed = sw.Elapsed.TotalSeconds > 0
- ? processedSeconds / sw.Elapsed.TotalSeconds
- : 0;
+ Index = i,
+ Start = i * segmentLength,
+ Length = (i == segments - 1)
+ ? Math.Max(0.1, totalDuration - i * segmentLength)
+ : segmentLength
+ })
+ .ToList();
- var remainingSeconds = (totalDuration - processedSeconds) / Math.Max(speed, 0.0001);
- if (remainingSeconds < 0) remainingSeconds = 0;
- var eta = TimeSpan.FromSeconds(remainingSeconds);
-
- DrawProgress(progress, eta, speed);
- Thread.Sleep(200);
- }
- })
- {
- IsBackground = true
- };
- progressThread.Start();
-
- // --- SINGLE THREADED LOOP ---
foreach (var job in jobs)
{
- var outputFile = BuildOutputFileName(outputFolder, mask, job.Index);
- RunFFmpegSegment(inputFile, outputFile, job.Start, job.Length, passthrough);
- completed++;
+ await ProcessSegment(processorFactory, inputFile, outputFolder, mask, passthrough, job.Index, job.Start, job.Length);
}
- sw.Stop();
- _progressRunning = false;
- progressThread.Join();
- DrawProgress(1.0, TimeSpan.Zero, totalDuration / Math.Max(sw.Elapsed.TotalSeconds, 0.0001));
}
- // -----------------------------
- // Multi-threaded cropping
- // -----------------------------
- private static void RunMultiThreadedCrop(
- string inputFile,
- string outputFolder,
- string outputMask,
- double duration,
- int segments,
- double segmentLength,
- string[] passthrough,
- int width,
- int height,
- bool showDebugOverlay,
- string? detect)
+ private static async Task ProcessSegment(Func processorFactory, string inputFile, string outputFolder, string mask, string[] passthrough, int index, double start, double length)
{
- var tracker = new TrackingSplitter(Log, DrawProgress);
-
- var jobs = Enumerable.Range(0, segments)
- .Select(i => new
+ var outputFile = BuildOutputFileName(outputFolder, mask, index);
+ var processor = processorFactory(index);
+ try
{
- Index = i,
- Start = i * segmentLength,
- Length = (i == segments - 1)
- ? Math.Max(0.1, duration - i * segmentLength)
- : segmentLength
- })
- .ToList();
-
- var completed = 0;
- var sw = Stopwatch.StartNew();
- _progressRunning = true;
-
- // --- PROGRESS THREAD ---
- var progressThread = new Thread(() =>
+ await processor.ProcessSegment(inputFile, outputFile, start, length, passthrough);
+ }
+ finally
{
- while (_progressRunning)
- {
- var progress = segments == 0 ? 0 : (double)completed / segments;
- var processedSeconds = completed * segmentLength;
-
- var speed = sw.Elapsed.TotalSeconds > 0
- ? processedSeconds / sw.Elapsed.TotalSeconds
- : 0;
-
- var remainingSeconds = (duration - processedSeconds) / Math.Max(speed, 0.0001);
- if (remainingSeconds < 0) remainingSeconds = 0;
-
- var eta = TimeSpan.FromSeconds(remainingSeconds);
-
- DrawProgress(progress, eta, speed);
- Thread.Sleep(200);
- }
- })
- {
- IsBackground = true
- };
- progressThread.Start();
-
- // --- PARALLEL EXECUTION ---
- var maxDegree = Math.Max(1, Environment.ProcessorCount / 2);
-
- Parallel.ForEach(
- jobs,
- new ParallelOptions { MaxDegreeOfParallelism = maxDegree },
- async job =>
- {
- var outputFile = BuildOutputFileName(outputFolder, outputMask, job.Index);
- using IDisposable detector = detect switch
- {
- "face" => new UltraFaceDetector(Log, DrawProgress),
- "body" => new YoloOnnxObjectDetector(Log, DrawProgress),
- _ => throw new InvalidOperationException($"Unknown detector: {detect}")
- };
-
- // Run the face-tracking cropper
- await tracker.TrackAndExtract(
- inputFile,
- outputFile,
- (IObjectDetector)detector,
- TimeSpan.FromSeconds(job.Start),
- TimeSpan.FromSeconds(job.Length),
- width,
- height,
- passthrough,
- showDebugOverlay);
-
- Interlocked.Increment(ref completed);
- });
-
- // --- CLEANUP ---
- sw.Stop();
- _progressRunning = false;
- progressThread.Join();
-
- var finalSpeed = duration / Math.Max(sw.Elapsed.TotalSeconds, 0.0001);
- DrawProgress(1.0, TimeSpan.Zero, finalSpeed);
+ if (processor is IDisposable disposable)
+ disposable.Dispose();
+ }
}
-
static string BuildOutputFileName(string folder, string mask, int index)
{
string fileName;
@@ -445,24 +258,4 @@ static class Program
return Path.Combine(folder, fileName);
}
- static void RunFFmpegSegment(string inputFile, string outputFile, double start, double length, string[] passthrough)
- {
- var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
-
- var args =
- $"-ss {start.ToString(CultureInfo.InvariantCulture)} -i \"{inputFile}\" -t {length.ToString(CultureInfo.InvariantCulture)} -c copy {pass} \"{outputFile}\" -y";
-
- var psi = new ProcessStartInfo
- {
- FileName = "ffmpeg",
- Arguments = args,
- RedirectStandardError = true,
- UseShellExecute = false,
- CreateNoWindow = true
- };
-
- using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
- proc.StandardError.ReadToEnd(); // swallow output
- proc.WaitForExit();
- }
}