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 Action? _onPercentageProgress; private Action? _onTimeProgress; private Action? _onOutput; private Action? _onError; private TimeSpan? _totalTimespan; private FFMpegLogLevel? _logLevel; 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; } public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) { cancel = () => CancelEvent?.Invoke(this, timeout); return this; } public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) { token.Register(() => CancelEvent?.Invoke(this, 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, out var 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, out var 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!; _ffMpegArguments.Pre(); using var instance = processArguments.Start(); var cancelled = false; void OnCancelEvent(object sender, int timeout) { cancelled = true; instance.SendInput("q"); if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) { cancellationTokenSource.Cancel(); instance.Kill(); } } 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) { throw new OperationCanceledException("ffmpeg processing was cancelled"); } return processResult; } finally { CancelEvent -= OnCancelEvent; } } 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, out CancellationTokenSource cancellationTokenSource) { 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); cancellationTokenSource = new CancellationTokenSource(); 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); } } }