mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Cancellation support added
This commit is contained in:
parent
af363ebb9a
commit
23bfdc8452
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProbeService _fileProbe, ILogger _log) : IAutoDecisionService
|
public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProbeService _fileProbe, ILogger _log) : IAutoDecisionService
|
||||||
{
|
{
|
||||||
public void ApplyAutoDecisions(JobViewModel job)
|
public void ApplyAutoDecisions(JobViewModel job, CancellationToken token)
|
||||||
{
|
{
|
||||||
Task.Run(() => Detect(job));
|
Task.Run(() => Detect(job, token));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Detect(JobViewModel job)
|
private async Task Detect(JobViewModel job, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -16,7 +16,7 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb
|
|||||||
job.Mask = "[NAME]_seg[NN].[EXT]";
|
job.Mask = "[NAME]_seg[NN].[EXT]";
|
||||||
job.OutputFolder = Path.Combine(Path.GetDirectoryName(job.InputFile)!, "splitter");
|
job.OutputFolder = Path.Combine(Path.GetDirectoryName(job.InputFile)!, "splitter");
|
||||||
|
|
||||||
job.Probe = await _fileProbe.ProbeAsync(job.InputFile);
|
job.Probe = await _fileProbe.ProbeAsync(job.InputFile, token);
|
||||||
job.Thumbnail = await _thumbnails.CreateThumbnailAsync(job.InputFile, job.Probe, rotateDegree: job.Rotate);
|
job.Thumbnail = await _thumbnails.CreateThumbnailAsync(job.InputFile, job.Probe, rotateDegree: job.Rotate);
|
||||||
|
|
||||||
if (job.Probe.Width > job.Probe.Height)
|
if (job.Probe.Width > job.Probe.Height)
|
||||||
@ -29,7 +29,7 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var sampler = new VideoRotationSampler(null);
|
var sampler = new VideoRotationSampler(null);
|
||||||
job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration);
|
job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration, token);
|
||||||
job.Detect = job.Rotate == 0 ? null : "body";
|
job.Detect = job.Rotate == 0 ? null : "body";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
public sealed class FileProbeService : IFileProbeService
|
public sealed class FileProbeService : IFileProbeService
|
||||||
{
|
{
|
||||||
public async Task<VideoInfo> ProbeAsync(string inputFile)
|
public async Task<VideoInfo> ProbeAsync(string inputFile, CancellationToken token)
|
||||||
{
|
{
|
||||||
var res = await Task.Run(() => ProbeVideo.Probe(inputFile, false));
|
var res = await Task.Run(() => ProbeVideo.Probe(inputFile, false, token), token);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public interface IAutoDecisionService
|
public interface IAutoDecisionService
|
||||||
{
|
{
|
||||||
void ApplyAutoDecisions(JobViewModel job);
|
void ApplyAutoDecisions(JobViewModel job, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public interface IFileProbeService
|
public interface IFileProbeService
|
||||||
{
|
{
|
||||||
Task<VideoInfo> ProbeAsync(string inputFile);
|
Task<VideoInfo> ProbeAsync(string inputFile, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public partial class FileListViewModel : ObservableObject
|
|||||||
var job = new SingleJob { InputFile = path };
|
var job = new SingleJob { InputFile = path };
|
||||||
var vm = _factory.Create(job);
|
var vm = _factory.Create(job);
|
||||||
Files.Add(vm);
|
Files.Add(vm);
|
||||||
_autoDecisionService.ApplyAutoDecisions(vm);
|
_autoDecisionService.ApplyAutoDecisions(vm, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Selected = Files.LastOrDefault();
|
Selected = Files.LastOrDefault();
|
||||||
|
|||||||
@ -15,6 +15,8 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _transformMode = false;
|
[ObservableProperty] private bool _transformMode = false;
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
|
||||||
public MainViewModel(
|
public MainViewModel(
|
||||||
FileListViewModel fileListVM,
|
FileListViewModel fileListVM,
|
||||||
PreviewPaneViewModel ppVM,
|
PreviewPaneViewModel ppVM,
|
||||||
@ -42,12 +44,19 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
Inspector.Selected = file;
|
Inspector.Selected = file;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Progress.SetMain(this);
|
||||||
Inspector.SetMain(this);
|
Inspector.SetMain(this);
|
||||||
Inspector.Files = FileList.Files;
|
Inspector.Files = FileList.Files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
StatusBar.StatusText = "Processing…";
|
StatusBar.StatusText = "Processing…";
|
||||||
@ -59,23 +68,26 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
var fileJobs = await _processor.GenerateJobs(file.GetJob(), false);
|
var fileJobs = await _processor.GenerateJobs(file.GetJob(), false, _cancellationTokenSource.Token);
|
||||||
jobs.AddRange(fileJobs);
|
jobs.AddRange(fileJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _processor.ProcessJobs(jobs, false);
|
await _processor.ProcessJobs(jobs, false, _cancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Handle exception
|
// Handle exception
|
||||||
StatusBar.StatusText = "Error occurred…";
|
StatusBar.StatusText = "Error occurred…";
|
||||||
_logger.LogError($"Error: {ex.Message}");
|
_logger.LogError($"Error: {ex.Message}");
|
||||||
|
_cancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
StatusBar.StatusText = "Ready…";
|
StatusBar.StatusText = "Ready…";
|
||||||
StatusBar.Percent = 0;
|
StatusBar.Percent = 0;
|
||||||
TransformMode = false;
|
TransformMode = false;
|
||||||
|
|
||||||
|
_cancellationTokenSource?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Splitter_UI.Views;
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
|
|
||||||
@ -11,6 +13,20 @@ public partial class ProgressViewModel : ObservableObject
|
|||||||
public ObservableCollection<ProgressInfo> Processes { get; } = [];
|
public ObservableCollection<ProgressInfo> Processes { get; } = [];
|
||||||
|
|
||||||
private Lock _lock = new();
|
private Lock _lock = new();
|
||||||
|
|
||||||
|
private MainViewModel _mainModel = null!;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
_mainModel.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMain(MainViewModel mainModel)
|
||||||
|
{
|
||||||
|
_mainModel = mainModel;
|
||||||
|
}
|
||||||
|
|
||||||
public void ClearProgress(string name, int progressLine)
|
public void ClearProgress(string name, int progressLine)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
|
|||||||
@ -5,20 +5,20 @@
|
|||||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||||
x:DataType="vm:ProgressViewModel">
|
x:DataType="vm:ProgressViewModel">
|
||||||
|
|
||||||
<Border Background="#111" Padding="8">
|
<Grid RowDefinitions="*,Auto" Background="#111">
|
||||||
<ItemsControl ItemsSource="{Binding Processes}">
|
|
||||||
|
<!-- Processes list -->
|
||||||
|
<ItemsControl Grid.Row="0" ItemsSource="{Binding Processes}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ProgressInfo">
|
<DataTemplate x:DataType="vm:ProgressInfo">
|
||||||
<Grid ColumnDefinitions="2*,3*,Auto,Auto"
|
<Grid ColumnDefinitions="2*,3*,Auto,Auto"
|
||||||
Margin="0,2">
|
Margin="0,2">
|
||||||
|
|
||||||
<!-- Name -->
|
|
||||||
<TextBlock Grid.Column="0"
|
<TextBlock Grid.Column="0"
|
||||||
Text="{Binding Name}"
|
Text="{Binding Name}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="12"/>
|
FontSize="12"/>
|
||||||
|
|
||||||
<!-- Progress bar -->
|
|
||||||
<ProgressBar Grid.Column="1"
|
<ProgressBar Grid.Column="1"
|
||||||
Height="12"
|
Height="12"
|
||||||
Minimum="0"
|
Minimum="0"
|
||||||
@ -26,7 +26,6 @@
|
|||||||
Value="{Binding Progress}"
|
Value="{Binding Progress}"
|
||||||
Margin="8,0"/>
|
Margin="8,0"/>
|
||||||
|
|
||||||
<!-- ETA -->
|
|
||||||
<TextBlock Grid.Column="2"
|
<TextBlock Grid.Column="2"
|
||||||
Width="70"
|
Width="70"
|
||||||
Text="{Binding Eta, StringFormat={}{0:hh\\:mm\\:ss}}"
|
Text="{Binding Eta, StringFormat={}{0:hh\\:mm\\:ss}}"
|
||||||
@ -34,7 +33,6 @@
|
|||||||
Margin="12,0"
|
Margin="12,0"
|
||||||
FontSize="12"/>
|
FontSize="12"/>
|
||||||
|
|
||||||
<!-- Speed -->
|
|
||||||
<TextBlock Grid.Column="3"
|
<TextBlock Grid.Column="3"
|
||||||
Width="70"
|
Width="70"
|
||||||
Text="{Binding Speed, StringFormat={}{0:0.00}}"
|
Text="{Binding Speed, StringFormat={}{0:0.00}}"
|
||||||
@ -46,5 +44,20 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</Border>
|
|
||||||
|
<!-- Bottom-right Cancel button -->
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
|
||||||
|
<Button Content="Cancel"
|
||||||
|
Background="#700000"
|
||||||
|
Foreground="White"
|
||||||
|
Padding="12,6"
|
||||||
|
Margin="0,0,10,10"
|
||||||
|
Command="{Binding CancelCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
public interface IJobProcessor
|
public interface IJobProcessor
|
||||||
{
|
{
|
||||||
Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly);
|
Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly, CancellationToken token);
|
||||||
Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded);
|
Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded, CancellationToken token);
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ namespace splitter;
|
|||||||
|
|
||||||
public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcessor
|
public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcessor
|
||||||
{
|
{
|
||||||
public async Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly)
|
public async Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly, CancellationToken token)
|
||||||
{
|
{
|
||||||
var baseName = Path.GetFileNameWithoutExtension(job.InputFile);
|
var baseName = Path.GetFileNameWithoutExtension(job.InputFile);
|
||||||
|
|
||||||
@ -18,7 +18,14 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
|||||||
if (!Directory.Exists(job.OutputFolder))
|
if (!Directory.Exists(job.OutputFolder))
|
||||||
Directory.CreateDirectory(job.OutputFolder);
|
Directory.CreateDirectory(job.OutputFolder);
|
||||||
|
|
||||||
var info = await ProbeVideo.Probe(job.InputFile, job.RotateAuto);
|
if (token.IsCancellationRequested)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var info = await ProbeVideo.Probe(job.InputFile, job.RotateAuto, token);
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return [];
|
||||||
|
|
||||||
if (info.Duration <= 0)
|
if (info.Duration <= 0)
|
||||||
{
|
{
|
||||||
LogError($"{baseName}: Could not read duration.");
|
LogError($"{baseName}: Could not read duration.");
|
||||||
@ -88,18 +95,18 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
|||||||
return jobs;
|
return jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded)
|
public async Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (singleThreaded)
|
if (singleThreaded)
|
||||||
{
|
{
|
||||||
LogInfo("Starting single-threaded splitting...");
|
LogInfo("Starting single-threaded splitting...");
|
||||||
await RunSingleThreaded(tasks);
|
await RunSingleThreaded(tasks, token);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogInfo("Starting multi-threaded splitting...");
|
LogInfo("Starting multi-threaded splitting...");
|
||||||
await RunMultiThreaded(tasks);
|
await RunMultiThreaded(tasks, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogInfo("Done.");
|
LogInfo("Done.");
|
||||||
@ -116,7 +123,7 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
|||||||
// Multi-threaded splitting
|
// Multi-threaded splitting
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
private async Task RunMultiThreaded(List<SingleTask> jobs)
|
private async Task RunMultiThreaded(List<SingleTask> jobs, CancellationToken token)
|
||||||
{
|
{
|
||||||
LogProgress(0.0, TimeSpan.Zero, 0.0);
|
LogProgress(0.0, TimeSpan.Zero, 0.0);
|
||||||
|
|
||||||
@ -135,7 +142,7 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
|||||||
|
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
{
|
{
|
||||||
await sem.WaitAsync();
|
await sem.WaitAsync(token);
|
||||||
|
|
||||||
tasks.Add(Task.Run(async () =>
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@ -145,9 +152,12 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
|||||||
{
|
{
|
||||||
// Acquire a slot ID
|
// Acquire a slot ID
|
||||||
while (!freeSlots.TryDequeue(out slot))
|
while (!freeSlots.TryDequeue(out slot))
|
||||||
|
{
|
||||||
|
if ( token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
}
|
||||||
await ProcessSegment(job, slot + 1);
|
await ProcessSegment(job, slot + 1, token);
|
||||||
|
|
||||||
var processed = Interlocked.Increment(ref processedSegments);
|
var processed = Interlocked.Increment(ref processedSegments);
|
||||||
var elapsed = sw.Elapsed;
|
var elapsed = sw.Elapsed;
|
||||||
@ -174,21 +184,21 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
|||||||
// Single-threaded splitting
|
// Single-threaded splitting
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
private async Task RunSingleThreaded(List<SingleTask> jobs)
|
private async Task RunSingleThreaded(List<SingleTask> jobs, CancellationToken token)
|
||||||
{
|
{
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
{
|
{
|
||||||
await ProcessSegment(job, 0);
|
await ProcessSegment(job, 0, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessSegment(SingleTask t, int slot)
|
private async Task ProcessSegment(SingleTask t, int slot, CancellationToken token)
|
||||||
{
|
{
|
||||||
var processor = t.ProcessorFactory(slot);
|
var processor = t.ProcessorFactory(slot);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await processor.ProcessSegment(t);
|
await processor.ProcessSegment(t, token);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,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(SingleTask job)
|
public async Task ProcessSegment(SingleTask job, CancellationToken token)
|
||||||
{
|
{
|
||||||
string inputFile = job.Job.InputFile;
|
string inputFile = job.Job.InputFile;
|
||||||
string outputFile = job.OutputFileName;
|
string outputFile = job.OutputFileName;
|
||||||
@ -53,9 +53,9 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
|
|||||||
using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
|
using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(outputFile);
|
var name = Path.GetFileNameWithoutExtension(outputFile);
|
||||||
ShowFFMpegProgress(length, proc, name);
|
await ShowFFMpegProgress(length, proc, name, token);
|
||||||
|
|
||||||
proc.WaitForExit();
|
await proc.WaitForExitAsync(token);
|
||||||
|
|
||||||
ClearProgress(name);
|
ClearProgress(name);
|
||||||
|
|
||||||
@ -75,12 +75,12 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
private void ShowFFMpegProgress(double length, Process proc, string name)
|
private async Task ShowFFMpegProgress(double length, Process proc, string name, CancellationToken token)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
string? line;
|
string? line;
|
||||||
while ((line = proc.StandardError.ReadLine()) != null)
|
while ((line = await proc.StandardError.ReadLineAsync(token)) != null)
|
||||||
{
|
{
|
||||||
// Look for "time=00:00:03.52"
|
// Look for "time=00:00:03.52"
|
||||||
var idx = line.IndexOf("time=", StringComparison.OrdinalIgnoreCase);
|
var idx = line.IndexOf("time=", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
d.Dispose();
|
d.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessSegment(SingleTask job)
|
public async Task ProcessSegment(SingleTask job, CancellationToken token)
|
||||||
{
|
{
|
||||||
string inputFile = job.Job.InputFile;
|
string inputFile = job.Job.InputFile;
|
||||||
string outputFile = job.OutputFileName;
|
string outputFile = job.OutputFileName;
|
||||||
@ -57,11 +57,11 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
|
LogInfo($"{name}: src={videoWidth}x{videoHeight} @ {fps:F3}fps, seg=[{start:F3},{length:F3}] enc={encWidth}x{encHeight}");
|
||||||
|
|
||||||
// 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
|
// 2) Start FFmpeg decode (video only → raw BGR24 to stdout)
|
||||||
var decode = StartFfmpegDecode(inputFile, start, length, job.Job.Rotate, job.Job.PlainText);
|
var decode = await StartFfmpegDecode(inputFile, start, length, job.Job.Rotate, job.Job.PlainText, token);
|
||||||
using var decodeStdout = decode.StandardOutput.BaseStream;
|
using var decodeStdout = decode.StandardOutput.BaseStream;
|
||||||
|
|
||||||
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
||||||
var encode = StartFfmpegEncode(
|
var encode = await StartFfmpegEncode(
|
||||||
inputFile,
|
inputFile,
|
||||||
outputFile,
|
outputFile,
|
||||||
start,
|
start,
|
||||||
@ -70,7 +70,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
encHeight,
|
encHeight,
|
||||||
fps,
|
fps,
|
||||||
ffmpegPassthroughParameters,
|
ffmpegPassthroughParameters,
|
||||||
job.Job.PlainText);
|
job.Job.PlainText,
|
||||||
|
token);
|
||||||
|
|
||||||
using var encodeStdin = encode.StandardInput.BaseStream;
|
using var encodeStdin = encode.StandardInput.BaseStream;
|
||||||
|
|
||||||
@ -99,9 +100,11 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
while (frameIndex < totalFrames)
|
while (frameIndex < totalFrames)
|
||||||
{
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
frameIndex++;
|
frameIndex++;
|
||||||
|
|
||||||
var read = ReadExact(decodeStdout, inBuffer, 0, inBytes);
|
var read = await ReadExact(decodeStdout, inBuffer, 0, inBytes, token);
|
||||||
if (read != inBytes)
|
if (read != inBytes)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -164,7 +167,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
// ---------- FFmpeg decode / encode ----------
|
// ---------- FFmpeg decode / encode ----------
|
||||||
|
|
||||||
private Process StartFfmpegDecode(string inputFile, double start, double length, int? rotate, bool plainText)
|
private async Task<Process> StartFfmpegDecode(string inputFile, double start, double length, int? rotate, bool plainText, CancellationToken token)
|
||||||
{
|
{
|
||||||
var ss = start .ToString("0.###", CultureInfo.InvariantCulture);
|
var ss = start .ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
var t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
@ -192,12 +195,12 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
var fileName = Path.GetFileName(inputFile);
|
var fileName = Path.GetFileName(inputFile);
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string? line;
|
string? line;
|
||||||
while ((line = p.StandardError.ReadLine()) != null)
|
while ((line = await p.StandardError.ReadLineAsync(token)) != null)
|
||||||
if (plainText)
|
if (plainText)
|
||||||
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
||||||
}
|
}
|
||||||
@ -223,7 +226,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
return rotateStr;
|
return rotateStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Process StartFfmpegEncode(
|
private async Task<Process> StartFfmpegEncode(
|
||||||
string inputFile,
|
string inputFile,
|
||||||
string outputFile,
|
string outputFile,
|
||||||
double start,
|
double start,
|
||||||
@ -232,7 +235,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
int height,
|
int height,
|
||||||
double fps,
|
double fps,
|
||||||
string[] passthrough,
|
string[] passthrough,
|
||||||
bool plainText)
|
bool plainText,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||||
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
@ -266,12 +270,12 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
var fileName = Path.GetFileName(outputFile);
|
var fileName = Path.GetFileName(outputFile);
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string? line;
|
string? line;
|
||||||
while ((line = p.StandardError.ReadLine()) != null)
|
while ((line = await p.StandardError.ReadLineAsync(token)) != null)
|
||||||
{
|
{
|
||||||
if (plainText)
|
if (plainText)
|
||||||
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
||||||
@ -285,12 +289,12 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
|||||||
|
|
||||||
// ---------- helpers ----------
|
// ---------- helpers ----------
|
||||||
|
|
||||||
private static int ReadExact(Stream s, byte[] buffer, int offset, int count)
|
private static async Task<int> ReadExact(Stream s, byte[] buffer, int offset, int count, CancellationToken token)
|
||||||
{
|
{
|
||||||
var total = 0;
|
var total = 0;
|
||||||
while (total < count)
|
while (total < count)
|
||||||
{
|
{
|
||||||
var read = s.Read(buffer, offset + total, count - total);
|
var read = await s.ReadAsync(buffer, offset + total, count - total, token);
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
break;
|
break;
|
||||||
total += read;
|
total += read;
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public interface ISegmentProcessor
|
public interface ISegmentProcessor
|
||||||
{
|
{
|
||||||
Task ProcessSegment( SingleTask job );
|
Task ProcessSegment( SingleTask job, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,20 +14,20 @@ public static class ProbeVideo
|
|||||||
_ffprobeJsonOptions.Converters.Add(new FlexibleLongConverter());
|
_ffprobeJsonOptions.Converters.Add(new FlexibleLongConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<VideoInfo> Probe(string inputFile, bool detectRotation)
|
public static async Task<VideoInfo> Probe(string inputFile, bool detectRotation, CancellationToken token)
|
||||||
{
|
{
|
||||||
var info = ProbeSize(inputFile);
|
var info = await ProbeSize(inputFile, token);
|
||||||
if (detectRotation)
|
if (detectRotation)
|
||||||
{
|
{
|
||||||
var rotation = await ProbeRotation(inputFile, info.Duration);
|
var rotation = await ProbeRotation(inputFile, info.Duration, token);
|
||||||
info = info with { Rotation = rotation };
|
info = info with { Rotation = rotation };
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<int> ProbeRotation(string inputFile, double duration)
|
private static async Task<int> ProbeRotation(string inputFile, double duration, CancellationToken token)
|
||||||
=> await new VideoRotationSampler(null).DetectRotationAsync(inputFile, duration);
|
=> await new VideoRotationSampler(null).DetectRotationAsync(inputFile, duration, token);
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _ffprobeJsonOptions =
|
private static readonly JsonSerializerOptions _ffprobeJsonOptions =
|
||||||
new ()
|
new ()
|
||||||
@ -39,7 +39,7 @@ public static class ProbeVideo
|
|||||||
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement
|
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement
|
||||||
};
|
};
|
||||||
|
|
||||||
private static VideoInfo ProbeSize(string inputFile)
|
private static async Task<VideoInfo> ProbeSize(string inputFile, CancellationToken token)
|
||||||
{
|
{
|
||||||
var args =
|
var args =
|
||||||
"-v error " +
|
"-v error " +
|
||||||
@ -61,8 +61,8 @@ public static class ProbeVideo
|
|||||||
using var p = new Process { StartInfo = psi };
|
using var p = new Process { StartInfo = psi };
|
||||||
p.Start();
|
p.Start();
|
||||||
|
|
||||||
var json = p.StandardOutput.ReadToEnd();
|
var json = await p.StandardOutput.ReadToEndAsync(token);
|
||||||
p.WaitForExit();
|
await p.WaitForExitAsync(token);
|
||||||
|
|
||||||
var result = JsonSerializer.Deserialize<FfprobeResult>(json, _ffprobeJsonOptions);
|
var result = JsonSerializer.Deserialize<FfprobeResult>(json, _ffprobeJsonOptions);
|
||||||
var stream = result?.Streams?.FirstOrDefault();
|
var stream = result?.Streams?.FirstOrDefault();
|
||||||
|
|||||||
@ -6,7 +6,7 @@ public sealed class VideoRotationSampler
|
|||||||
{
|
{
|
||||||
private readonly FrameRotationDetector _detector = new FrameRotationDetector();
|
private readonly FrameRotationDetector _detector = new FrameRotationDetector();
|
||||||
|
|
||||||
public static int RotationDetectorSampleCount = 20;
|
public static int RotationDetectorSampleCount = 10;
|
||||||
public static double RotationDetectorSampleLength = 0.15; // seconds to decode per probe
|
public static double RotationDetectorSampleLength = 0.15; // seconds to decode per probe
|
||||||
public static int RotationDetectorFrameWidth = 320;
|
public static int RotationDetectorFrameWidth = 320;
|
||||||
public static int RotationDetectorFrameHeight = 180;
|
public static int RotationDetectorFrameHeight = 180;
|
||||||
@ -38,7 +38,8 @@ public sealed class VideoRotationSampler
|
|||||||
|
|
||||||
public async Task<int> DetectRotationAsync(
|
public async Task<int> DetectRotationAsync(
|
||||||
string inputFile,
|
string inputFile,
|
||||||
double videoLengthSeconds)
|
double videoLengthSeconds,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
if (videoLengthSeconds <= 0)
|
if (videoLengthSeconds <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
@ -54,7 +55,8 @@ public sealed class VideoRotationSampler
|
|||||||
t,
|
t,
|
||||||
RotationDetectorSampleLength,
|
RotationDetectorSampleLength,
|
||||||
RotationDetectorFrameWidth,
|
RotationDetectorFrameWidth,
|
||||||
RotationDetectorFrameHeight);
|
RotationDetectorFrameHeight,
|
||||||
|
token);
|
||||||
|
|
||||||
if (frame != null && !frame.Empty())
|
if (frame != null && !frame.Empty())
|
||||||
{
|
{
|
||||||
@ -98,18 +100,21 @@ public sealed class VideoRotationSampler
|
|||||||
double start,
|
double start,
|
||||||
double length,
|
double length,
|
||||||
int width,
|
int width,
|
||||||
int height)
|
int height,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var p = StartFfmpegDecode(inputFile, start, length, rotate: null, plainText: false);
|
var p = StartFfmpegDecode(inputFile, start, length, rotate: null, plainText: false);
|
||||||
|
|
||||||
int needed = _buffer.Length;
|
var needed = _buffer.Length;
|
||||||
int read = 0;
|
var read = 0;
|
||||||
|
|
||||||
using var stdout = p.StandardOutput.BaseStream;
|
using var stdout = p.StandardOutput.BaseStream;
|
||||||
|
|
||||||
while (read < needed)
|
while (read < needed)
|
||||||
{
|
{
|
||||||
int r = await stdout.ReadAsync(_buffer, read, needed - read);
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var r = await stdout.ReadAsync(_buffer, read, needed - read, token);
|
||||||
if (r == 0)
|
if (r == 0)
|
||||||
return null;
|
return null;
|
||||||
read += r;
|
read += r;
|
||||||
|
|||||||
@ -38,7 +38,7 @@ static partial class Program
|
|||||||
var allJobs = new List<SingleTask>();
|
var allJobs = new List<SingleTask>();
|
||||||
foreach ( var job in cmd.Jobs )
|
foreach ( var job in cmd.Jobs )
|
||||||
{
|
{
|
||||||
var jobs = await processor.GenerateJobs(job, cmd.Master.EstimateOnly);
|
var jobs = await processor.GenerateJobs(job, cmd.Master.EstimateOnly, CancellationToken.None);
|
||||||
allJobs.AddRange(jobs);
|
allJobs.AddRange(jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ static partial class Program
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await processor.ProcessJobs(allJobs, cmd.Master.SingleThreaded);
|
var success = await processor.ProcessJobs(allJobs, cmd.Master.SingleThreaded, CancellationToken.None);
|
||||||
if (uiTask != null)
|
if (uiTask != null)
|
||||||
{
|
{
|
||||||
if ( cts != null )
|
if ( cts != null )
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user