using System.Diagnostics; using System.IO; using System.Text; namespace EmbyToolbox.Services; public sealed record VideoEncoderSettings(string Codec, string ExtraArguments); public sealed class FfmpegEncoderDiscoveryService { private readonly object _gate = new(); private HashSet? _cachedEncoders; public HashSet GetAvailableEncoders(LoggingService logging) { lock (_gate) { if (_cachedEncoders is not null) { return _cachedEncoders; } _cachedEncoders = DiscoverEncoders(logging); return _cachedEncoders; } } public VideoEncoderSettings ResolveVideoEncoder( string selectedMode, string targetVideo, bool autoFallbackToCpu, LoggingService logging) { var encoders = GetAvailableEncoders(logging); var family = ResolveTargetFamily(targetVideo); if (string.Equals(selectedMode, HardwareAccelerationMode.Auto, StringComparison.OrdinalIgnoreCase)) { var auto = TryResolveByPriority(encoders, family); if (auto is not null) { logging.Info($"выбран энкодер (Auto): {auto.Codec}", "conversion.hw"); return auto; } var cpu = BuildCpuSettings(family); logging.Warning("аппаратные энкодеры не найдены, fallback на CPU", "conversion.hw"); return cpu; } var preferred = TryResolveSpecificMode(selectedMode, encoders, family); if (preferred is not null) { logging.Info($"выбран энкодер ({selectedMode}): {preferred.Codec}", "conversion.hw"); return preferred; } logging.Error($"выбранный энкодер недоступен: режим={selectedMode}, target={targetVideo}", "conversion.hw"); if (!autoFallbackToCpu) { throw new InvalidOperationException($"Недоступен выбранный энкодер: {selectedMode}"); } var fallback = BuildCpuSettings(family); logging.Warning($"fallback на CPU: {fallback.Codec}", "conversion.hw"); return fallback; } private static VideoEncoderSettings? TryResolveByPriority(HashSet encoders, string family) { return TryResolveModeInternal(HardwareAccelerationMode.Nvenc, encoders, family) ?? TryResolveModeInternal(HardwareAccelerationMode.Qsv, encoders, family) ?? TryResolveModeInternal(HardwareAccelerationMode.Amf, encoders, family); } private static VideoEncoderSettings? TryResolveSpecificMode(string selectedMode, HashSet encoders, string family) { if (string.Equals(selectedMode, HardwareAccelerationMode.Cpu, StringComparison.OrdinalIgnoreCase)) { return BuildCpuSettings(family); } return TryResolveModeInternal(selectedMode, encoders, family); } private static VideoEncoderSettings? TryResolveModeInternal(string mode, HashSet encoders, string family) { var codec = family == "h265" ? mode.ToUpperInvariant() switch { "NVENC" => "hevc_nvenc", "QSV" => "hevc_qsv", "AMF" => "hevc_amf", _ => string.Empty } : mode.ToUpperInvariant() switch { "NVENC" => "h264_nvenc", "QSV" => "h264_qsv", "AMF" => "h264_amf", _ => string.Empty }; if (string.IsNullOrWhiteSpace(codec) || !encoders.Contains(codec)) { return null; } var extra = mode.ToUpperInvariant() switch { "NVENC" => "-preset p5 -cq 23 -b:v 0", "QSV" => "-global_quality 23", "AMF" => "-quality quality -rc cqp -qp_i 23 -qp_p 23", _ => string.Empty }; return new VideoEncoderSettings(codec, extra); } private static VideoEncoderSettings BuildCpuSettings(string family) { var codec = family == "h265" ? "libx265" : "libx264"; return new VideoEncoderSettings(codec, "-preset medium -crf 23"); } private static string ResolveTargetFamily(string targetVideo) { if (targetVideo.Contains("265", StringComparison.OrdinalIgnoreCase) || targetVideo.Contains("hevc", StringComparison.OrdinalIgnoreCase)) { return "h265"; } return "h264"; } private static HashSet DiscoverEncoders(LoggingService logging) { var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "Tools", "ffmpeg.exe"); if (!File.Exists(ffmpegPath)) { logging.Error($"не найден ffmpeg: {ffmpegPath}", "conversion.hw"); return new HashSet(StringComparer.OrdinalIgnoreCase); } var start = new ProcessStartInfo { FileName = ffmpegPath, Arguments = "-hide_banner -encoders", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = new Process { StartInfo = start }; process.Start(); var stdout = process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); var text = string.Concat(stdout, Environment.NewLine, stderr); var found = ParseEncoderNames(text); logging.Info( $"обнаружено энкодеров ffmpeg: {found.Count}. hw: {FormatHwSummary(found)}", "conversion.hw", command: $"{ffmpegPath} -hide_banner -encoders", stdout: text); return found; } private static HashSet ParseEncoderNames(string text) { var result = new HashSet(StringComparer.OrdinalIgnoreCase); var lines = text.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var t = line.TrimStart(); if (!t.StartsWith('V') && !t.StartsWith("V.", StringComparison.Ordinal)) { continue; } var parts = t.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) { result.Add(parts[1].Trim()); } } return result; } private static string FormatHwSummary(HashSet encoders) { var known = new[] { "h264_nvenc", "hevc_nvenc", "h264_qsv", "hevc_qsv", "h264_amf", "hevc_amf", "libx264", "libx265" }; var present = known.Where(encoders.Contains).ToArray(); return present.Length == 0 ? "none" : string.Join(", ", present); } }