using System.Diagnostics; using System.Text; using System.IO; namespace AmiReel.Services; public sealed class ProcessRunner { public async Task RunAsync( string executable, IEnumerable arguments, Action? log, Action? position, CancellationToken token, bool allowFailure = false, Action? outputHandler = null) { ProcessStartInfo start = new() { FileName = executable, UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, StandardErrorEncoding = Encoding.UTF8, StandardOutputEncoding = Encoding.UTF8 }; foreach (string argument in arguments) start.ArgumentList.Add(argument); using Process process = new() { StartInfo = start, EnableRaisingEvents = true }; StringBuilder output = new(); process.Start(); Task stdout = PumpAsync(process.StandardOutput, output, log, position, token, outputHandler); Task stderr = PumpAsync(process.StandardError, output, log, position, token, outputHandler); using CancellationTokenRegistration registration = token.Register(() => { try { if (!process.HasExited) process.Kill(true); } catch { } }); await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(token)); if (process.ExitCode != 0 && !allowFailure) throw new InvalidOperationException($"Process exited with code {process.ExitCode}.\n{output}"); return output.ToString(); } private static async Task PumpAsync( StreamReader reader, StringBuilder output, Action? log, Action? position, CancellationToken token, Action? outputHandler) { while (await reader.ReadLineAsync(token) is { } line) { output.AppendLine(line); FfmpegOutputLine parsed = FfmpegProgressParser.Parse(line); if (!IsNoise(parsed)) log?.Invoke(line); if (parsed.Time is { } time) position?.Invoke(time); outputHandler?.Invoke(parsed); } } internal static bool IsNoise(FfmpegOutputLine output) { string line = output.Line.TrimStart(); if (string.IsNullOrWhiteSpace(line)) return true; if (line.StartsWith("frame=", StringComparison.OrdinalIgnoreCase)) return true; if (output.Frame is not null || output.Time is not null) return true; if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase) || line.Contains("skipped", StringComparison.OrdinalIgnoreCase) || line.Contains("could not", StringComparison.OrdinalIgnoreCase) || line.Contains("failed", StringComparison.OrdinalIgnoreCase) || line.Contains("not found", StringComparison.OrdinalIgnoreCase) || line.Contains("Output file is empty", StringComparison.OrdinalIgnoreCase)) return false; return FfmpegOutputFilter.IsBoilerplateLine(line) || line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("encoder :", StringComparison.OrdinalIgnoreCase) || line.StartsWith("title :", StringComparison.OrdinalIgnoreCase) || line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("[", StringComparison.OrdinalIgnoreCase); } }