using System.Diagnostics; using System.Text.RegularExpressions; using FFMpegCore.Enums; using FFMpegCore.Exceptions; using FFMpegCore.Helpers; using Instances; namespace FFMpegCore; public class FFMpegArgumentProcessor { private static readonly Regex ProgressRegex = new(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); private readonly List> _configurations; private readonly FFMpegArguments _ffMpegArguments; private CancellationTokenRegistration? _cancellationTokenRegistration; private bool _cancelled; private FFMpegLogLevel? _logLevel; private Action? _onError; private Action? _onOutput; private Action? _onPercentageProgress; private Action? _onTimeProgress; private TimeSpan? _totalTimespan; internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) { _configurations = new List>(); _ffMpegArguments = ffMpegArguments; } public string Arguments => _ffMpegArguments.Text; private event EventHandler CancelEvent = null!; /// /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is /// calculated. /// Total time is needed to calculate the percentage that has been processed of the full file. /// /// Action to invoke when progress percentage is updated /// The total timespan of the mediafile being processed public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) { _totalTimespan = totalTimeSpan; _onPercentageProgress = onPercentageProgress; return this; } /// /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed /// /// Action that will be invoked with the parsed timestamp as argument public FFMpegArgumentProcessor NotifyOnProgress(Action onTimeProgress) { _onTimeProgress = onTimeProgress; return this; } /// /// Register action that will be invoked during the ffmpeg processing, when a line is output /// /// public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) { _onOutput = onOutput; return this; } public FFMpegArgumentProcessor NotifyOnError(Action onError) { _onError = onError; return this; } private void Cancel(int timeout) { _cancelled = true; CancelEvent?.Invoke(this, timeout); } public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) { cancel = () => Cancel(timeout); return this; } public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) { token.ThrowIfCancellationRequested(); _cancellationTokenRegistration?.Dispose(); _cancellationTokenRegistration = token.Register(() => Cancel(timeout)); return this; } public FFMpegArgumentProcessor Configure(Action configureOptions) { _configurations.Add(configureOptions); return this; } /// /// Sets the log level of this process. Overides the /// that is set in the for this specific process. /// /// The log level of the ffmpeg execution. public FFMpegArgumentProcessor WithLogLevel(FFMpegLogLevel logLevel) { _logLevel = logLevel; return this; } public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options); using var cancellationTokenSource = new CancellationTokenSource(); IProcessResult? processResult = null; try { processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); } catch (OperationCanceledException) { if (throwOnError) { throw; } } return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options); using var cancellationTokenSource = new CancellationTokenSource(); IProcessResult? processResult = null; try { processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false); } catch (OperationCanceledException) { if (throwOnError) { throw; } } return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } private async Task Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) { IProcessResult processResult = null!; if (_cancelled) { _cancellationTokenRegistration?.Dispose(); throw new OperationCanceledException("cancelled before starting processing"); } _ffMpegArguments.Pre(); using var instance = processArguments.Start(); void OnCancelEvent(object sender, int timeout) { ExecuteIgnoringFinishedProcessExceptions(() => instance.SendInput("q")); if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) { cancellationTokenSource.Cancel(); ExecuteIgnoringFinishedProcessExceptions(() => instance.Kill()); } static void ExecuteIgnoringFinishedProcessExceptions(Action action) { try { action(); } catch (Instances.Exceptions.InstanceProcessAlreadyExitedException) { //ignore } catch (ObjectDisposedException) { //ignore } } } CancelEvent += OnCancelEvent; try { await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t => { processResult = t.Result; cancellationTokenSource.Cancel(); _ffMpegArguments.Post(); }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); if (_cancelled) { _cancellationTokenRegistration?.Dispose(); throw new OperationCanceledException("ffmpeg processing was cancelled"); } return processResult; } finally { CancelEvent -= OnCancelEvent; _cancellationTokenRegistration?.Dispose(); } } private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) { if (throwOnError && exitCode != 0) { throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, string.Join("\n", errorData)); } _onPercentageProgress?.Invoke(100.0); if (_totalTimespan.HasValue) { _onTimeProgress?.Invoke(_totalTimespan.Value); } return exitCode == 0; } internal FFOptions GetConfiguredOptions(FFOptions? ffOptions) { var options = ffOptions ?? GlobalFFOptions.Current.Clone(); foreach (var configureOptions in _configurations) { configureOptions(options); } return options; } private ProcessArguments PrepareProcessArguments(FFOptions ffOptions) { FFMpegHelper.RootExceptionCheck(); FFMpegHelper.VerifyFFMpegExists(ffOptions); var arguments = _ffMpegArguments.Text; //If local loglevel is null, set the global. if (_logLevel == null) { _logLevel = ffOptions.LogLevel; } //If neither local nor global loglevel is null, set the argument. if (_logLevel != null) { var normalizedLogLevel = _logLevel.ToString() .ToLower(); arguments += $" -v {normalizedLogLevel}"; } var startInfo = new ProcessStartInfo { FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffOptions), Arguments = arguments, StandardOutputEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory }; var processArguments = new ProcessArguments(startInfo); if (_onOutput != null) { processArguments.OutputDataReceived += OutputData; } if (_onError != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) { processArguments.ErrorDataReceived += ErrorData; } return processArguments; } private void ErrorData(object sender, string msg) { _onError?.Invoke(msg); var match = ProgressRegex.Match(msg); if (!match.Success) { return; } var processed = MediaAnalysisUtils.ParseDuration(match.Groups[1].Value); _onTimeProgress?.Invoke(processed); if (_onPercentageProgress == null || _totalTimespan == null) { return; } var percentage = Math.Round(processed.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100, 2); _onPercentageProgress(percentage); } private void OutputData(object sender, string msg) { Debug.WriteLine(msg); _onOutput?.Invoke(msg); } }