208 lines
7.0 KiB
C#
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);
|
|
}
|
|
}
|