using System; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; namespace axXez.TS3.Audio { public static class FFmpeg { public const int SampleRate = 48000; public const int Channels = 2; /// Returns true if ffmpeg is installed and accessible on PATH. public static bool IsAvailable() { try { using var p = Process.Start(new ProcessStartInfo("ffmpeg") { ArgumentList = { "-version" }, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }); p?.WaitForExit(); return p?.ExitCode == 0; } catch { return false; } } /// Converts any audio file to raw PCM (48kHz stereo s16le). public static async Task ToPcmAsync(string filePath, CancellationToken ct = default) { using var p = Start("-i", filePath); using var ms = new MemoryStream(); _ = p.StandardError.BaseStream.CopyToAsync(Stream.Null, ct); // drain to prevent deadlock await p.StandardOutput.BaseStream.CopyToAsync(ms, ct); await p.WaitForExitAsync(ct); if (p.ExitCode != 0) throw new Exception($"ffmpeg exited with code {p.ExitCode}."); return ms.ToArray(); } /// /// Starts ffmpeg streaming PCM from a URL with reconnect support. /// The caller must dispose the returned when done. /// public static (Process process, Stream stdout) StartPcmStream(string url) { var p = Start("-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5", "-i", url); return (p, p.StandardOutput.BaseStream); } private static Process Start(params string[] inputArgs) { var psi = new ProcessStartInfo("ffmpeg") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; foreach (var a in inputArgs) psi.ArgumentList.Add(a); foreach (var a in new[] { "-vn", "-f", "s16le", "-acodec", "pcm_s16le", "-ar", SampleRate.ToString(), "-ac", Channels.ToString(), "-loglevel", "error", "pipe:1" }) psi.ArgumentList.Add(a); return Process.Start(psi) ?? throw new InvalidOperationException("ffmpeg not found."); } } }