mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12: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 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
|
||||
{
|
||||
@ -16,7 +16,7 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb
|
||||
job.Mask = "[NAME]_seg[NN].[EXT]";
|
||||
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);
|
||||
|
||||
if (job.Probe.Width > job.Probe.Height)
|
||||
@ -29,7 +29,7 @@ public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProb
|
||||
else
|
||||
{
|
||||
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";
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
public interface IAutoDecisionService
|
||||
{
|
||||
void ApplyAutoDecisions(JobViewModel job);
|
||||
void ApplyAutoDecisions(JobViewModel job, CancellationToken token);
|
||||
}
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
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 vm = _factory.Create(job);
|
||||
Files.Add(vm);
|
||||
_autoDecisionService.ApplyAutoDecisions(vm);
|
||||
_autoDecisionService.ApplyAutoDecisions(vm, CancellationToken.None);
|
||||
}
|
||||
|
||||
Selected = Files.LastOrDefault();
|
||||
|
||||
@ -15,6 +15,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
[ObservableProperty] private bool _transformMode = false;
|
||||
private ILogger _logger;
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
public MainViewModel(
|
||||
FileListViewModel fileListVM,
|
||||
PreviewPaneViewModel ppVM,
|
||||
@ -42,12 +44,19 @@ public partial class MainViewModel : ViewModelBase
|
||||
Inspector.Selected = file;
|
||||
};
|
||||
|
||||
Progress.SetMain(this);
|
||||
Inspector.SetMain(this);
|
||||
Inspector.Files = FileList.Files;
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
try
|
||||
{
|
||||
StatusBar.StatusText = "Processing…";
|
||||
@ -59,23 +68,26 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await _processor.ProcessJobs(jobs, false);
|
||||
await _processor.ProcessJobs(jobs, false, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exception
|
||||
StatusBar.StatusText = "Error occurred…";
|
||||
_logger.LogError($"Error: {ex.Message}");
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
StatusBar.StatusText = "Ready…";
|
||||
StatusBar.Percent = 0;
|
||||
TransformMode = false;
|
||||
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Splitter_UI.Views;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
@ -11,6 +13,20 @@ public partial class ProgressViewModel : ObservableObject
|
||||
public ObservableCollection<ProgressInfo> Processes { get; } = [];
|
||||
|
||||
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)
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
@ -5,20 +5,20 @@
|
||||
xmlns:vm="clr-namespace:Splitter_UI.ViewModels"
|
||||
x:DataType="vm:ProgressViewModel">
|
||||
|
||||
<Border Background="#111" Padding="8">
|
||||
<ItemsControl ItemsSource="{Binding Processes}">
|
||||
<Grid RowDefinitions="*,Auto" Background="#111">
|
||||
|
||||
<!-- Processes list -->
|
||||
<ItemsControl Grid.Row="0" ItemsSource="{Binding Processes}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ProgressInfo">
|
||||
<Grid ColumnDefinitions="2*,3*,Auto,Auto"
|
||||
Margin="0,2">
|
||||
|
||||
<!-- Name -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding Name}"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"/>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<ProgressBar Grid.Column="1"
|
||||
Height="12"
|
||||
Minimum="0"
|
||||
@ -26,7 +26,6 @@
|
||||
Value="{Binding Progress}"
|
||||
Margin="8,0"/>
|
||||
|
||||
<!-- ETA -->
|
||||
<TextBlock Grid.Column="2"
|
||||
Width="70"
|
||||
Text="{Binding Eta, StringFormat={}{0:hh\\:mm\\:ss}}"
|
||||
@ -34,7 +33,6 @@
|
||||
Margin="12,0"
|
||||
FontSize="12"/>
|
||||
|
||||
<!-- Speed -->
|
||||
<TextBlock Grid.Column="3"
|
||||
Width="70"
|
||||
Text="{Binding Speed, StringFormat={}{0:0.00}}"
|
||||
@ -46,5 +44,20 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</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>
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
|
||||
public interface IJobProcessor
|
||||
{
|
||||
Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly);
|
||||
Task<bool> ProcessJobs(List<SingleTask> tasks, bool singleThreaded);
|
||||
Task<List<SingleTask>> GenerateJobs(SingleJob job, bool estimateOnly, CancellationToken token);
|
||||
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 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);
|
||||
|
||||
@ -18,7 +18,14 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
if (!Directory.Exists(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)
|
||||
{
|
||||
LogError($"{baseName}: Could not read duration.");
|
||||
@ -88,18 +95,18 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
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)
|
||||
{
|
||||
LogInfo("Starting single-threaded splitting...");
|
||||
await RunSingleThreaded(tasks);
|
||||
await RunSingleThreaded(tasks, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogInfo("Starting multi-threaded splitting...");
|
||||
await RunMultiThreaded(tasks);
|
||||
await RunMultiThreaded(tasks, token);
|
||||
}
|
||||
|
||||
LogInfo("Done.");
|
||||
@ -116,7 +123,7 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
// 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);
|
||||
|
||||
@ -135,7 +142,7 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
await sem.WaitAsync();
|
||||
await sem.WaitAsync(token);
|
||||
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
@ -145,9 +152,12 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
{
|
||||
// Acquire a slot ID
|
||||
while (!freeSlots.TryDequeue(out slot))
|
||||
{
|
||||
if ( token.IsCancellationRequested)
|
||||
return;
|
||||
await Task.Yield();
|
||||
|
||||
await ProcessSegment(job, slot + 1);
|
||||
}
|
||||
await ProcessSegment(job, slot + 1, token);
|
||||
|
||||
var processed = Interlocked.Increment(ref processedSegments);
|
||||
var elapsed = sw.Elapsed;
|
||||
@ -174,21 +184,21 @@ public class JobProcessor(ILogger logger) : LoggingBase(logger, 0), IJobProcesso
|
||||
// Single-threaded splitting
|
||||
// -----------------------------
|
||||
|
||||
private async Task RunSingleThreaded(List<SingleTask> jobs)
|
||||
private async Task RunSingleThreaded(List<SingleTask> jobs, CancellationToken token)
|
||||
{
|
||||
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);
|
||||
try
|
||||
{
|
||||
await processor.ProcessSegment(t);
|
||||
await processor.ProcessSegment(t, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ namespace splitter;
|
||||
|
||||
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 outputFile = job.OutputFileName;
|
||||
@ -43,19 +43,19 @@ public class SimpleSplitter(int segmentNo, ILogger logger) : LoggingBase(logger,
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
Arguments = args,
|
||||
FileName = "ffmpeg",
|
||||
Arguments = args,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var proc = Process.Start(psi) ?? throw new Exception("Failed to start ffmpeg.");
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(outputFile);
|
||||
ShowFFMpegProgress(length, proc, name);
|
||||
await ShowFFMpegProgress(length, proc, name, token);
|
||||
|
||||
proc.WaitForExit();
|
||||
await proc.WaitForExitAsync(token);
|
||||
|
||||
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();
|
||||
|
||||
string? line;
|
||||
while ((line = proc.StandardError.ReadLine()) != null)
|
||||
while ((line = await proc.StandardError.ReadLineAsync(token)) != null)
|
||||
{
|
||||
// Look for "time=00:00:03.52"
|
||||
var idx = line.IndexOf("time=", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@ -24,7 +24,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
d.Dispose();
|
||||
}
|
||||
|
||||
public async Task ProcessSegment(SingleTask job)
|
||||
public async Task ProcessSegment(SingleTask job, CancellationToken token)
|
||||
{
|
||||
string inputFile = job.Job.InputFile;
|
||||
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}");
|
||||
|
||||
// 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;
|
||||
|
||||
// 3) Start FFmpeg encode (video from stdin + audio from original)
|
||||
var encode = StartFfmpegEncode(
|
||||
var encode = await StartFfmpegEncode(
|
||||
inputFile,
|
||||
outputFile,
|
||||
start,
|
||||
@ -70,7 +70,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
encHeight,
|
||||
fps,
|
||||
ffmpegPassthroughParameters,
|
||||
job.Job.PlainText);
|
||||
job.Job.PlainText,
|
||||
token);
|
||||
|
||||
using var encodeStdin = encode.StandardInput.BaseStream;
|
||||
|
||||
@ -99,9 +100,11 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
while (frameIndex < totalFrames)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
frameIndex++;
|
||||
|
||||
var read = ReadExact(decodeStdout, inBuffer, 0, inBytes);
|
||||
var read = await ReadExact(decodeStdout, inBuffer, 0, inBytes, token);
|
||||
if (read != inBytes)
|
||||
break;
|
||||
|
||||
@ -164,7 +167,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
// ---------- 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 t = length.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
@ -192,12 +195,12 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
var fileName = Path.GetFileName(inputFile);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string? line;
|
||||
while ((line = p.StandardError.ReadLine()) != null)
|
||||
while ((line = await p.StandardError.ReadLineAsync(token)) != null)
|
||||
if (plainText)
|
||||
LogInfo($"[ffmpeg-decode] {fileName}: {line}");
|
||||
}
|
||||
@ -223,7 +226,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
return rotateStr;
|
||||
}
|
||||
|
||||
private Process StartFfmpegEncode(
|
||||
private async Task<Process> StartFfmpegEncode(
|
||||
string inputFile,
|
||||
string outputFile,
|
||||
double start,
|
||||
@ -232,7 +235,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
int height,
|
||||
double fps,
|
||||
string[] passthrough,
|
||||
bool plainText)
|
||||
bool plainText,
|
||||
CancellationToken token)
|
||||
{
|
||||
var pass = passthrough.Length > 0 ? string.Join(" ", passthrough) : "";
|
||||
var fpsStr = fps.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
@ -266,12 +270,12 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
var fileName = Path.GetFileName(outputFile);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
string? line;
|
||||
while ((line = p.StandardError.ReadLine()) != null)
|
||||
while ((line = await p.StandardError.ReadLineAsync(token)) != null)
|
||||
{
|
||||
if (plainText)
|
||||
LogInfo($"[ffmpeg-encode] {fileName}: {line}");
|
||||
@ -285,12 +289,12 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
|
||||
|
||||
// ---------- 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;
|
||||
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)
|
||||
break;
|
||||
total += read;
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var rotation = await ProbeRotation(inputFile, info.Duration);
|
||||
var rotation = await ProbeRotation(inputFile, info.Duration, token);
|
||||
info = info with { Rotation = rotation };
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static async Task<int> ProbeRotation(string inputFile, double duration)
|
||||
=> await new VideoRotationSampler(null).DetectRotationAsync(inputFile, duration);
|
||||
private static async Task<int> ProbeRotation(string inputFile, double duration, CancellationToken token)
|
||||
=> await new VideoRotationSampler(null).DetectRotationAsync(inputFile, duration, token);
|
||||
|
||||
private static readonly JsonSerializerOptions _ffprobeJsonOptions =
|
||||
new ()
|
||||
@ -39,7 +39,7 @@ public static class ProbeVideo
|
||||
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement
|
||||
};
|
||||
|
||||
private static VideoInfo ProbeSize(string inputFile)
|
||||
private static async Task<VideoInfo> ProbeSize(string inputFile, CancellationToken token)
|
||||
{
|
||||
var args =
|
||||
"-v error " +
|
||||
@ -61,8 +61,8 @@ public static class ProbeVideo
|
||||
using var p = new Process { StartInfo = psi };
|
||||
p.Start();
|
||||
|
||||
var json = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
var json = await p.StandardOutput.ReadToEndAsync(token);
|
||||
await p.WaitForExitAsync(token);
|
||||
|
||||
var result = JsonSerializer.Deserialize<FfprobeResult>(json, _ffprobeJsonOptions);
|
||||
var stream = result?.Streams?.FirstOrDefault();
|
||||
|
||||
@ -6,7 +6,7 @@ public sealed class VideoRotationSampler
|
||||
{
|
||||
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 int RotationDetectorFrameWidth = 320;
|
||||
public static int RotationDetectorFrameHeight = 180;
|
||||
@ -38,7 +38,8 @@ public sealed class VideoRotationSampler
|
||||
|
||||
public async Task<int> DetectRotationAsync(
|
||||
string inputFile,
|
||||
double videoLengthSeconds)
|
||||
double videoLengthSeconds,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (videoLengthSeconds <= 0)
|
||||
return 0;
|
||||
@ -54,7 +55,8 @@ public sealed class VideoRotationSampler
|
||||
t,
|
||||
RotationDetectorSampleLength,
|
||||
RotationDetectorFrameWidth,
|
||||
RotationDetectorFrameHeight);
|
||||
RotationDetectorFrameHeight,
|
||||
token);
|
||||
|
||||
if (frame != null && !frame.Empty())
|
||||
{
|
||||
@ -98,18 +100,21 @@ public sealed class VideoRotationSampler
|
||||
double start,
|
||||
double length,
|
||||
int width,
|
||||
int height)
|
||||
int height,
|
||||
CancellationToken token)
|
||||
{
|
||||
var p = StartFfmpegDecode(inputFile, start, length, rotate: null, plainText: false);
|
||||
|
||||
int needed = _buffer.Length;
|
||||
int read = 0;
|
||||
var needed = _buffer.Length;
|
||||
var read = 0;
|
||||
|
||||
using var stdout = p.StandardOutput.BaseStream;
|
||||
|
||||
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)
|
||||
return null;
|
||||
read += r;
|
||||
|
||||
@ -38,7 +38,7 @@ static partial class Program
|
||||
var allJobs = new List<SingleTask>();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ static partial class Program
|
||||
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 ( cts != null )
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user