diff --git a/Splitter-UI/Converters/ConsoleColorToBrushConverter.cs b/Splitter-UI/Converters/ConsoleColorToBrushConverter.cs new file mode 100644 index 0000000..1074df1 --- /dev/null +++ b/Splitter-UI/Converters/ConsoleColorToBrushConverter.cs @@ -0,0 +1,40 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; +using System; +using System.Globalization; + +namespace Splitter_UI.Converters; + +public sealed class ConsoleColorToBrushConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is ConsoleColor c) + return new SolidColorBrush(ToColor(c)); + + return Brushes.White; + } + + private static Color ToColor(ConsoleColor c) => + c switch + { + ConsoleColor.Black => Colors.Black, + ConsoleColor.DarkBlue => Colors.DarkBlue, + ConsoleColor.DarkGreen => Colors.DarkGreen, + ConsoleColor.DarkCyan => Colors.DarkCyan, + ConsoleColor.DarkRed => Colors.DarkRed, + ConsoleColor.DarkMagenta => Colors.DarkMagenta, + ConsoleColor.DarkYellow => Colors.Olive, + ConsoleColor.Gray => Colors.Gray, + ConsoleColor.DarkGray => Colors.DarkGray, + ConsoleColor.Blue => Colors.Blue, + ConsoleColor.Green => Colors.Green, + ConsoleColor.Cyan => Colors.Cyan, + ConsoleColor.Red => Colors.Red, + ConsoleColor.Magenta => Colors.Magenta, + ConsoleColor.Yellow => Colors.Yellow, + ConsoleColor.White => Colors.White, + _ => Colors.White + }; + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException(); +} diff --git a/Splitter-UI/Models/ProgressInfo.cs b/Splitter-UI/Models/ProgressInfo.cs deleted file mode 100644 index 02cebc8..0000000 --- a/Splitter-UI/Models/ProgressInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Splitter_UI.Models; - -public class ProgressInfo -{ - public double Percent { get; set; } -} diff --git a/Splitter-UI/Program.cs b/Splitter-UI/Program.cs index ee216ba..684cec3 100644 --- a/Splitter-UI/Program.cs +++ b/Splitter-UI/Program.cs @@ -23,13 +23,15 @@ internal sealed class Program { var services = new ServiceCollection(); + var logPaveVM = new LogPaneViewModel(); // ViewModels services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(logPaveVM); + services.AddSingleton(logPaveVM); // splitter services services.AddSingleton(); @@ -51,8 +53,6 @@ internal sealed class Program services.AddTransient(); services.AddTransient(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); diff --git a/Splitter-UI/Services/AutoDecisionService.cs b/Splitter-UI/Services/AutoDecisionService.cs index c534a40..aed8196 100644 --- a/Splitter-UI/Services/AutoDecisionService.cs +++ b/Splitter-UI/Services/AutoDecisionService.cs @@ -1,6 +1,4 @@ -using OpenCvSharp.Dnn; - -namespace Splitter_UI.Services; +namespace Splitter_UI.Services; public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProbeService _fileProbe, ILogger _log) : IAutoDecisionService { @@ -34,6 +32,8 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration); job.Detect = job.Rotate == 0 ? null : "body"; } + + _log.LogInfo(job.ToString()); } catch (Exception ex) { diff --git a/Splitter-UI/Services/GlobalLogger.cs b/Splitter-UI/Services/GlobalLogger.cs index da5594b..100e3ca 100644 --- a/Splitter-UI/Services/GlobalLogger.cs +++ b/Splitter-UI/Services/GlobalLogger.cs @@ -1,11 +1,20 @@ namespace Splitter_UI.Services; -internal class GlobalLogger(ILogService _logService) : ILogger +internal class GlobalLogger(ILogService _logService, StatusBarViewModel _statusBar) : ILogger { - public void ClearProgress(int progressLevel) { } - public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) { } + public void ClearProgress(string name, int progressLine) + { + if (progressLine == 0) + _statusBar.Percent = 0; + } + public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) + { + if (progressLine == 0) + _statusBar.Percent = progress; + } + public void Log(string prefix, ConsoleColor color, string msg) { - _logService.Write($"[{prefix}] {msg}"); + _logService.Log(prefix, color, msg); } } diff --git a/Splitter-UI/Services/ILogService.cs b/Splitter-UI/Services/ILogService.cs index 048f375..ef8b7f2 100644 --- a/Splitter-UI/Services/ILogService.cs +++ b/Splitter-UI/Services/ILogService.cs @@ -3,7 +3,5 @@ namespace Splitter_UI.Services; public interface ILogService { - event Action? MessageLogged; - - void Write(string message); + void Log(string prefix, ConsoleColor color, string msg); } diff --git a/Splitter-UI/Services/IProcessingService.cs b/Splitter-UI/Services/IProcessingService.cs deleted file mode 100644 index f49b62f..0000000 --- a/Splitter-UI/Services/IProcessingService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Splitter_UI.Services; - -public interface IProcessingService -{ - event Action? ProgressChanged; - - Task ProcessAsync(IEnumerable jobs, CancellationToken token); -} diff --git a/Splitter-UI/Services/LogService.cs b/Splitter-UI/Services/LogService.cs deleted file mode 100644 index e19f1e1..0000000 --- a/Splitter-UI/Services/LogService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Splitter_UI.Services; - -public sealed class LogService : ILogService -{ - public event Action? MessageLogged; - - public void Write(string message) - { - MessageLogged?.Invoke(message); - } -} diff --git a/Splitter-UI/Services/ProcessingService.cs b/Splitter-UI/Services/ProcessingService.cs deleted file mode 100644 index 54e1213..0000000 --- a/Splitter-UI/Services/ProcessingService.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Splitter_UI.Services; - -public sealed class ProcessingService : IProcessingService -{ - public event Action? ProgressChanged; - - public async Task ProcessAsync(IEnumerable jobs, CancellationToken token) - { - foreach (var job in jobs) - { - for (int i = 0; i <= 100; i += 20) - { - if (token.IsCancellationRequested) - return; - - var progress = new ProgressInfo { Percent = i }; - - // Notify UI - ProgressChanged?.Invoke(job.InputFile, progress); - - await Task.Delay(100, token); - } - } - } -} diff --git a/Splitter-UI/ViewModels/JobViewModel.cs b/Splitter-UI/ViewModels/JobViewModel.cs index 51f1e2e..3bd74ab 100644 --- a/Splitter-UI/ViewModels/JobViewModel.cs +++ b/Splitter-UI/ViewModels/JobViewModel.cs @@ -14,7 +14,6 @@ public partial class JobViewModel : ObservableObject [ObservableProperty] private VideoInfo? _probe; [ObservableProperty] private PreviewData? _preview = new(null, [], null, new(0.5f, 0.5f)); - [ObservableProperty] private ProgressInfo? _progress; [ObservableProperty] private Bitmap? _thumbnail; [ObservableProperty] private double _sliderLiveValue; [ObservableProperty] private double _positionSeconds; @@ -36,7 +35,7 @@ public partial class JobViewModel : ObservableObject ? $"{Probe.Width}x{Probe.Height}, {TimeSpan.FromSeconds(Probe.Duration).ToString(@"hh\:mm\:ss")}), FPS: {Probe.Fps:F2}, Bitrate: {Probe.Bitrate/1024/1024:F2} MB/s" : ""; - public override string ToString() => $"{FileName} - {TextDesc}"; + public override string ToString() => $"{FileName}: {TextDesc}"; public ObservableCollection ParametersList { get; } = new(); diff --git a/Splitter-UI/ViewModels/LogPaneViewModel.cs b/Splitter-UI/ViewModels/LogPaneViewModel.cs index e20f396..ef82116 100644 --- a/Splitter-UI/ViewModels/LogPaneViewModel.cs +++ b/Splitter-UI/ViewModels/LogPaneViewModel.cs @@ -1,15 +1,35 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; namespace Splitter_UI.ViewModels; -public partial class LogPaneViewModel : ObservableObject -{ - public ObservableCollection Logs { get; } = []; +public sealed record LogEntry(string Prefix, ConsoleColor Color, string Message); - public void Add(string message) +public partial class LogPaneViewModel : ObservableObject, ILogService +{ + public ObservableCollection Logs { get; } = []; + + public void Log(string prefix, ConsoleColor color, string msg) { - Logs.Add(message); + Add(new LogEntry(prefix.Replace("[", "").Replace("]", ""), color, msg)); + } + + private void Add(LogEntry entry) + { + if (Dispatcher.UIThread.CheckAccess()) + { + AddInternal(entry); + } + else + { + Dispatcher.UIThread.Post(() => AddInternal(entry)); + } + } + + private void AddInternal(LogEntry entry) + { + Logs.Add(entry); if (Logs.Count > 5000) Logs.RemoveAt(0); } diff --git a/Splitter-UI/ViewModels/MainViewModel.cs b/Splitter-UI/ViewModels/MainViewModel.cs index 96ef448..1d9ef5c 100644 --- a/Splitter-UI/ViewModels/MainViewModel.cs +++ b/Splitter-UI/ViewModels/MainViewModel.cs @@ -4,15 +4,26 @@ namespace Splitter_UI.ViewModels; public partial class MainViewModel : ViewModelBase { - public FileListViewModel FileList { get; } - public PreviewPaneViewModel Preview { get; } = new PreviewPaneViewModel(); - public InspectorPaneViewModel Inspector { get; } = new InspectorPaneViewModel(); - public StatusBarViewModel StatusBar { get; } = new StatusBarViewModel(); - public LogPaneViewModel LogPane { get; } = new LogPaneViewModel(); + public FileListViewModel FileList { get; } + public PreviewPaneViewModel Preview { get; } + public InspectorPaneViewModel Inspector { get; } + public StatusBarViewModel StatusBar { get; } + public LogPaneViewModel LogPane { get; } - public MainViewModel(IFileJobFactory fileJobFactory, IAutoDecisionService autoDecisionService) + public MainViewModel( + IFileJobFactory fileJobFactory, + IAutoDecisionService autoDecisionService, + PreviewPaneViewModel ppVM, + InspectorPaneViewModel iVM, + LogPaneViewModel lpVM, + StatusBarViewModel sbVM + ) { - FileList = new FileListViewModel(fileJobFactory, autoDecisionService); + FileList = new FileListViewModel(fileJobFactory, autoDecisionService); + Preview = ppVM; + Inspector = iVM; + LogPane = lpVM; + StatusBar = sbVM; // Wire selection → preview + inspector FileList.SelectedFileChanged += file => { diff --git a/Splitter-UI/Views/FileListView.axaml b/Splitter-UI/Views/FileListView.axaml index a1da9ee..071eb99 100644 --- a/Splitter-UI/Views/FileListView.axaml +++ b/Splitter-UI/Views/FileListView.axaml @@ -67,47 +67,10 @@ - - - - - - - - - - - - - - - - - + + diff --git a/Splitter-UI/Views/JobListItemView.axaml b/Splitter-UI/Views/JobListItemView.axaml new file mode 100644 index 0000000..b339429 --- /dev/null +++ b/Splitter-UI/Views/JobListItemView.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Splitter-UI/Views/JobListItemView.axaml.cs b/Splitter-UI/Views/JobListItemView.axaml.cs new file mode 100644 index 0000000..3a6431a --- /dev/null +++ b/Splitter-UI/Views/JobListItemView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Splitter_UI.Views; + +public partial class JobListItemView : UserControl +{ + public JobListItemView() + { + InitializeComponent(); + } +} + diff --git a/Splitter-UI/Views/LogPane.axaml b/Splitter-UI/Views/LogPane.axaml index b45ce36..d2da202 100644 --- a/Splitter-UI/Views/LogPane.axaml +++ b/Splitter-UI/Views/LogPane.axaml @@ -2,15 +2,30 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Splitter_UI.Views.LogPane" + xmlns:conv="clr-namespace:Splitter_UI.Converters" xmlns:vm="clr-namespace:Splitter_UI.ViewModels" x:DataType="vm:LogPaneViewModel"> + + + + - + - - + + + + + + + diff --git a/Splitter-UI/Views/LogPane.axaml.cs b/Splitter-UI/Views/LogPane.axaml.cs index e65040f..55c1e02 100644 --- a/Splitter-UI/Views/LogPane.axaml.cs +++ b/Splitter-UI/Views/LogPane.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Threading; namespace Splitter_UI.Views; @@ -7,5 +8,24 @@ public partial class LogPane : UserControl public LogPane() { InitializeComponent(); + + // When DataContext changes, subscribe to collection changes + this.DataContextChanged += (_, _) => + { + if (DataContext is LogPaneViewModel vm) + { + vm.Logs.CollectionChanged += (_, _) => ScrollToEnd(); + } + }; + } + + private void ScrollToEnd() + { + // Must run after layout pass + Dispatcher.UIThread.Post(() => + { + if (Scroller != null) + Scroller.ScrollToEnd(); + }, DispatcherPriority.Background); } } diff --git a/splitter-cli/IJobProcessor.cs b/splitter-cli/IJobProcessor.cs new file mode 100644 index 0000000..9e4cf94 --- /dev/null +++ b/splitter-cli/IJobProcessor.cs @@ -0,0 +1,7 @@ +namespace splitter; + +public interface IJobProcessor +{ + Task> GenerateJobs(SingleJob job, bool estimateOnly); + Task ProcessJobs(List tasks, bool singleThreaded); +} \ No newline at end of file diff --git a/splitter-cli/JobProcessor.cs b/splitter-cli/JobProcessor.cs new file mode 100644 index 0000000..f44102d --- /dev/null +++ b/splitter-cli/JobProcessor.cs @@ -0,0 +1,217 @@ +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace splitter; + +public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcessor +{ + public async Task> GenerateJobs(SingleJob job, bool estimateOnly) + { + var baseName = Path.GetFileNameWithoutExtension(job.InputFile); + + if (!File.Exists(job.InputFile)) + { + LogError($"{baseName}: Input file not found."); + return []; + } + + if (!Directory.Exists(job.OutputFolder)) + Directory.CreateDirectory(job.OutputFolder); + + var info = await ProbeVideo.Probe(job.InputFile, job.RotateAuto); + if (info.Duration <= 0) + { + LogError($"{baseName}: Could not read duration."); + return []; + } + + var target = job.OverrideTargetDuration ?? 58.0; + + int segments; + double segmentLength; + + if (job.ForceFixed) + { + // Fixed chunk size, last one may be shorter + segments = (int)Math.Ceiling(info.Duration / target); + segmentLength = target; + } + else + { + // Equalized segments + segments = (int)Math.Ceiling(info.Duration / target); + segmentLength = info.Duration / segments; + } + + 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 (estimateOnly) + return []; + + Func processorFactory; + if (job.Crop != null) + { + processorFactory = i => + { + IObjectDetector detector = job.Detect switch + { + "face" => new UltraFaceDetector(_logger), + "body" => new YoloOnnxObjectDetector(_logger), + _ => throw new InvalidOperationException($"Unknown detector: {job.Detect}") + }; + return new TrackingSplitter(i, detector, job, _logger); + }; + } + else + { + processorFactory = i => new SimpleSplitter(i, _logger); + } + + var jobs = Enumerable.Range(0, segments) + .Select(i => new SingleTask + ( + Job : job, + Info: info, + OutputFileName : BuildOutputFileName(job, i), + SegmentIndex : i, + TotalSegments : segments, + SegmentStart : i * segmentLength, + SegmentLength : (i == segments - 1) + ? Math.Max(0.1, info.Duration - i * segmentLength) + : segmentLength, + ProcessorFactory : processorFactory + ) + ) + .ToList(); + + return jobs; + } + + public async Task ProcessJobs(List tasks, bool singleThreaded) + { + + if (singleThreaded) + { + LogInfo("Starting single-threaded splitting..."); + await RunSingleThreaded(tasks); + } + else + { + LogInfo("Starting multi-threaded splitting..."); + await RunMultiThreaded(tasks); + } + + LogInfo("Done."); + return true; + } + + private void LogProgress(double progress, TimeSpan eta, double speed) => _logger.DrawProgress("Total", 0, progress, eta, speed); + + // ----------------------------- + // ffprobe + // ----------------------------- + + // ----------------------------- + // Multi-threaded splitting + // ----------------------------- + + private async Task RunMultiThreaded(List jobs) + { + LogProgress(0.0, TimeSpan.Zero, 0.0); + + var maxDegree = Math.Max(1, Environment.ProcessorCount / 2); + + using var sem = new SemaphoreSlim(maxDegree); + var tasks = new List(); + + // Slot pool: 0..maxDegree-1 + var freeSlots = new ConcurrentQueue(Enumerable.Range(0, maxDegree)); + + var totalSegments = jobs.Count; + var processedSegments = 0; + var totalDuration = jobs.Sum(j => j.SegmentLength); + var sw = Stopwatch.StartNew(); + + foreach (var job in jobs) + { + await sem.WaitAsync(); + + tasks.Add(Task.Run(async () => + { + int slot = -1; + + try + { + // Acquire a slot ID + while (!freeSlots.TryDequeue(out slot)) + await Task.Yield(); + + await ProcessSegment(job, slot + 1); + + var processed = Interlocked.Increment(ref processedSegments); + var elapsed = sw.Elapsed; + var eta = TimeSpan.FromTicks(elapsed.Ticks * (totalSegments - processed) / processed); + var speed = (processed * totalDuration) / elapsed.TotalSeconds; + LogProgress((double)processed / totalSegments, eta, speed); + } + finally + { + // Return slot to pool + if (slot >= 0) + freeSlots.Enqueue(slot); + + sem.Release(); + } + })); + } + + await Task.WhenAll(tasks); + } + + + // ----------------------------- + // Single-threaded splitting + // ----------------------------- + + private async Task RunSingleThreaded(List jobs) + { + foreach (var job in jobs) + { + await ProcessSegment(job, 0); + } + + } + + private async Task ProcessSegment(SingleTask t, int slot) + { + var processor = t.ProcessorFactory(slot); + try + { + await processor.ProcessSegment(t); + } + finally + { + if (processor is IDisposable disposable) + disposable.Dispose(); + } + } + + private static string BuildOutputFileName(SingleJob job, int index) + { + string fileName; + + fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]") + .Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile)) + .Replace("[N]", index.ToString()) + .Replace("[NN]", index.ToString("00")) + .Replace("[NNN]", index.ToString("000")) + .Replace("[NNNN]", index.ToString("0000")) + .Replace("[EXT]", Path.GetExtension(job.InputFile).TrimStart('.')) + ; + + return Path.Combine(job.OutputFolder, fileName); + } + + +} diff --git a/splitter-cli/SimpleSplitter.cs b/splitter-cli/SimpleSplitter.cs index 21d9da5..3986ea1 100644 --- a/splitter-cli/SimpleSplitter.cs +++ b/splitter-cli/SimpleSplitter.cs @@ -57,7 +57,7 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger, proc.WaitForExit(); - ClearProgress(); + ClearProgress(name); if (proc.ExitCode != 0) LogError($"Segment {name} FFmpeg encoding failed"); diff --git a/splitter-cli/TrackingSplitter.cs b/splitter-cli/TrackingSplitter.cs index 9d4b752..167ffaf 100644 --- a/splitter-cli/TrackingSplitter.cs +++ b/splitter-cli/TrackingSplitter.cs @@ -152,7 +152,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable try { if (!decode.HasExited) decode.Kill(entireProcessTree: true); } catch { } try { if (!decode.HasExited) await decode.WaitForExitAsync(); } catch { } - ClearProgress(); + ClearProgress(name); if (encode.ExitCode != 0) diff --git a/splitter-cli/splitter.cs b/splitter-cli/splitter.cs index c4760eb..72a279f 100644 --- a/splitter-cli/splitter.cs +++ b/splitter-cli/splitter.cs @@ -1,6 +1,3 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using Spectre.Console; using splitter; static partial class Program @@ -33,24 +30,26 @@ static partial class Program uiTask = logger.RunAsync(cts.Token); } + var processor = new JobProcessor(_logger); + if (cmd.Master.EstimateOnly) - LogInfo("=== ESTIMATE MODE ==="); + _logger.LogInfo("=== ESTIMATE MODE ==="); var allJobs = new List(); foreach ( var job in cmd.Jobs ) { - var jobs = await GenerateJobs(cmd, job); + var jobs = await processor.GenerateJobs(job, cmd.Master.EstimateOnly); allJobs.AddRange(jobs); } if ( allJobs.Count == 0) { if ( !cmd.Master.EstimateOnly) - LogWarn("No valid jobs to process."); + _logger.LogWarn("No valid jobs to process."); return 0; } - var success = await ProcessJobs(cmd, allJobs); + var success = await processor.ProcessJobs(allJobs, cmd.Master.SingleThreaded); if (uiTask != null) { if ( cts != null ) @@ -63,217 +62,4 @@ static partial class Program return success ? 1 : 0; } - private static async Task> GenerateJobs(CommandLine cmd, SingleJob job) - { - var baseName = Path.GetFileNameWithoutExtension(job.InputFile); - - if (!File.Exists(job.InputFile)) - { - LogError($"{baseName}: Input file not found."); - return []; - } - - if (!Directory.Exists(job.OutputFolder)) - Directory.CreateDirectory(job.OutputFolder); - - var info = await ProbeVideo.Probe(job.InputFile, job.RotateAuto); - if (info.Duration <= 0) - { - LogError($"{baseName}: Could not read duration."); - return []; - } - - var target = job.OverrideTargetDuration ?? 58.0; - - int segments; - double segmentLength; - - if (job.ForceFixed) - { - // Fixed chunk size, last one may be shorter - segments = (int)Math.Ceiling(info.Duration / target); - segmentLength = target; - } - else - { - // Equalized segments - segments = (int)Math.Ceiling(info.Duration / target); - segmentLength = info.Duration / segments; - } - - 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 []; - - Func processorFactory; - if (job.Crop != null) - { - processorFactory = i => - { - IObjectDetector detector = job.Detect switch - { - "face" => new UltraFaceDetector(_logger), - "body" => new YoloOnnxObjectDetector(_logger), - _ => throw new InvalidOperationException($"Unknown detector: {job.Detect}") - }; - return new TrackingSplitter(i, detector, job, _logger); - }; - } - else - { - processorFactory = i => new SimpleSplitter(i, _logger); - } - - var jobs = Enumerable.Range(0, segments) - .Select(i => new SingleTask - ( - Job : job, - Info: info, - OutputFileName : BuildOutputFileName(job, i), - SegmentIndex : i, - TotalSegments : segments, - SegmentStart : i * segmentLength, - SegmentLength : (i == segments - 1) - ? Math.Max(0.1, info.Duration - i * segmentLength) - : segmentLength, - ProcessorFactory : processorFactory - ) - ) - .ToList(); - - return jobs; - } - - private static async Task ProcessJobs(CommandLine cmd, List tasks) - { - - if (cmd.Master.SingleThreaded) - { - LogInfo("Starting single-threaded splitting..."); - await RunSingleThreaded(tasks); - } - else - { - LogInfo("Starting multi-threaded splitting..."); - await RunMultiThreaded(tasks); - } - - LogInfo("Done."); - return true; - } - - private static void LogInfo(string message) => _logger.LogInfo(message); - private static void LogWarn(string message) => _logger.LogWarn(message); - private static void LogError(string message) => _logger.LogError(message); - private static void LogProgress(double progress, TimeSpan eta, double speed) => _logger.DrawProgress("Total", 0, progress, eta, speed); - - // ----------------------------- - // ffprobe - // ----------------------------- - - // ----------------------------- - // Multi-threaded splitting - // ----------------------------- - - static async Task RunMultiThreaded(List jobs) - { - LogProgress(0.0, TimeSpan.Zero, 0.0); - - var maxDegree = Math.Max(1, Environment.ProcessorCount / 2); - - using var sem = new SemaphoreSlim(maxDegree); - var tasks = new List(); - - // Slot pool: 0..maxDegree-1 - var freeSlots = new ConcurrentQueue(Enumerable.Range(0, maxDegree)); - - var totalSegments = jobs.Count; - var processedSegments = 0; - var totalDuration = jobs.Sum(j => j.SegmentLength); - var sw = Stopwatch.StartNew(); - - foreach (var job in jobs) - { - await sem.WaitAsync(); - - tasks.Add(Task.Run(async () => - { - int slot = -1; - - try - { - // Acquire a slot ID - while (!freeSlots.TryDequeue(out slot)) - await Task.Yield(); - - await ProcessSegment(job,slot + 1); - - var processed = Interlocked.Increment(ref processedSegments); - var elapsed = sw.Elapsed; - var eta = TimeSpan.FromTicks(elapsed.Ticks * (totalSegments - processed) / processed); - var speed = (processed * totalDuration) / elapsed.TotalSeconds; - LogProgress((double)processed / totalSegments, eta, speed); - } - finally - { - // Return slot to pool - if (slot >= 0) - freeSlots.Enqueue(slot); - - sem.Release(); - } - })); - } - - await Task.WhenAll(tasks); - } - - - // ----------------------------- - // Single-threaded splitting - // ----------------------------- - - static async Task RunSingleThreaded(List jobs) - { - foreach (var job in jobs) - { - await ProcessSegment(job, 0); - } - - } - - private static async Task ProcessSegment(SingleTask t, int slot) - { - var processor = t.ProcessorFactory(slot); - try - { - await processor.ProcessSegment(t); - } - finally - { - if (processor is IDisposable disposable) - disposable.Dispose(); - } - } - - static string BuildOutputFileName(SingleJob job, int index) - { - string fileName; - - fileName = Path.GetFileName(job.Mask ?? "[NAME]_seg[NN].[EXT]") - .Replace("[NAME]", Path.GetFileNameWithoutExtension(job.InputFile)) - .Replace("[N]" , index.ToString()) - .Replace("[NN]" , index.ToString("00")) - .Replace("[NNN]" , index.ToString("000")) - .Replace("[NNNN]", index.ToString("0000")) - .Replace("[EXT]" , Path.GetExtension(job.InputFile).TrimStart('.')) - ; - - return Path.Combine(job.OutputFolder, fileName); - } - - - } diff --git a/splitter-cli/tui/ILogger.cs b/splitter-cli/tui/ILogger.cs index a546c6e..0a46192 100644 --- a/splitter-cli/tui/ILogger.cs +++ b/splitter-cli/tui/ILogger.cs @@ -2,7 +2,7 @@ public interface ILogger { - void ClearProgress(int progressLevel); + void ClearProgress(string name, int progressLine); void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed); void Log(string prefix, ConsoleColor color, string msg); diff --git a/splitter-cli/tui/LoggingBase.cs b/splitter-cli/tui/LoggingBase.cs index 0c230f4..ab68952 100644 --- a/splitter-cli/tui/LoggingBase.cs +++ b/splitter-cli/tui/LoggingBase.cs @@ -1,7 +1,11 @@ namespace splitter.tui; -public abstract class LoggingBase(ILogger _logger, int _progressLine) +public abstract class LoggingBase(ILogger logger, int _progressLine) { +#pragma warning disable IDE1006 // Naming Styles + protected ILogger _logger = logger; +#pragma warning restore IDE1006 // Naming Styles + protected void Log(string level, ConsoleColor color, string message) => _logger.Log(level, color, message); @@ -17,6 +21,6 @@ public abstract class LoggingBase(ILogger _logger, int _progressLine) protected void DrawProgress(string name, double percent, TimeSpan eta, double fps) => _logger.DrawProgress(name, _progressLine, percent, eta, fps); - protected void ClearProgress() - => _logger.ClearProgress(_progressLine); + protected void ClearProgress(string name) + => _logger.ClearProgress(name,_progressLine); } diff --git a/splitter-cli/tui/SpectreConsoleLogger.cs b/splitter-cli/tui/SpectreConsoleLogger.cs index ca9a535..779315d 100644 --- a/splitter-cli/tui/SpectreConsoleLogger.cs +++ b/splitter-cli/tui/SpectreConsoleLogger.cs @@ -51,11 +51,11 @@ public sealed class SpectreConsoleLogger : ILogger, IDisposable // ---- ILogger ---- - public void ClearProgress(int progressLevel) + public void ClearProgress(string name, int progressLine) { lock (_sync) { - _progress[progressLevel] = ProgressEntry.Empty; + _progress[progressLine] = ProgressEntry.Empty; } } diff --git a/splitter-cli/tui/TextLogger.cs b/splitter-cli/tui/TextLogger.cs index 4cbe09d..80f2d13 100644 --- a/splitter-cli/tui/TextLogger.cs +++ b/splitter-cli/tui/TextLogger.cs @@ -13,6 +13,6 @@ public class TextLogger() : ILogger } public void DrawProgress(string name, int progressLine, double progress, TimeSpan eta, double speed) {} - public void ClearProgress(int progressLevel){} + public void ClearProgress(string name, int progressLine) {} }