emby-toolbox/EmbyToolbox/Services/FfmpegEncoderDiscoveryService.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

208 lines
7.0 KiB
C#

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<string>? _cachedEncoders;
public HashSet<string> 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<string> 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<string> 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<string> 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<string> 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<string>(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<string> ParseEncoderNames(string text)
{
var result = new HashSet<string>(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<string> 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);
}
}