From b820b798cb1758deafa10dca5ba73adcc17aba98 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 27 Sep 2025 11:30:14 -0500 Subject: [PATCH] use nvenc to detect encoder capability (#2459) --- CHANGELOG.md | 1 + .../FFmpeg/FFmpegLibraryProcessService.cs | 1 + .../HardwareCapabilitiesFactory.cs | 67 ++++----- .../Capabilities/Nvidia/CudaDevice.cs | 3 + .../Capabilities/Nvidia/CudaHelper.cs | 132 ++++++++++++++++++ .../Nvidia/NvEncSharpRedirector.cs | 43 ++++++ .../NvidiaHardwareCapabilities.cs | 121 ++++++++++++---- ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj | 1 + ErsatzTV.FFmpeg/Format/VideoProfile.cs | 1 + .../Pipeline/NvidiaPipelineBuilder.cs | 5 +- ErsatzTV/Pages/FFmpegEditor.razor | 9 +- .../RunOnce/PlatformSettingsService.cs | 3 + .../FFmpegProfileEditViewModelValidator.cs | 8 +- .../ViewModels/FFmpegProfileEditViewModel.cs | 17 ++- 14 files changed, 350 insertions(+), 62 deletions(-) create mode 100644 ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaDevice.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/Nvidia/NvEncSharpRedirector.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index afd01505e..f92d89dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix extracted text subtitles getting into invalid state after media server deep scans - Targeted deep scans will now extract text subtitles for the scanned show - Fix playlist preview +- Use NvEnc API to detect encoder capability instead of heuristic based on GPU model/architecture ### Changed - Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index b8f20d916..6db3c45d2 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -1114,6 +1114,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService (VideoFormat.H264, VideoProfile.Main) => VideoProfile.Main, (VideoFormat.H264, VideoProfile.High) => VideoProfile.High, (VideoFormat.H264, VideoProfile.High10) => VideoProfile.High10, + (VideoFormat.H264, VideoProfile.High444p) => VideoProfile.High444p, _ => Option.None }; diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index 3bf501cf3..3fc1cec7c 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.RegularExpressions; using CliWrap; using CliWrap.Buffered; +using ErsatzTV.FFmpeg.Capabilities.Nvidia; using ErsatzTV.FFmpeg.Capabilities.Qsv; using ErsatzTV.FFmpeg.Capabilities.Vaapi; using ErsatzTV.FFmpeg.Capabilities.VideoToolbox; @@ -18,8 +19,7 @@ namespace ErsatzTV.FFmpeg.Capabilities; public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory { - private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture"; - private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model"; + private const string CudaDeviceKey = "ffmpeg.hardware.cuda.device"; private static readonly CompositeFormat VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}"); @@ -104,7 +104,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return hardwareAccelerationMode switch { - HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities), + HardwareAccelerationMode.Nvenc => GetNvidiaCapabilities(ffmpegCapabilities), HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice), HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDisplay, vaapiDriver, vaapiDevice), HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareCapabilities(ffmpegCapabilities, _logger), @@ -122,6 +122,24 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return string.Empty; } + Option> maybeDevices = CudaHelper.GetDevices(); + foreach (List devices in maybeDevices.Where(list => list.Count > 0)) + { + var sb = new StringBuilder(); + foreach (CudaDevice device in devices) + { + sb.AppendLine( + CultureInfo.InvariantCulture, + $"GPU #{device.Handle} < {device.Model} > has Compute SM {device.Version.Major}.{device.Version.Minor}"); + + sb.AppendLine(CudaHelper.GetDeviceDetails(device)); + } + + return sb.ToString(); + } + + // if we don't have a list of cuda devices, fall back to ffmpeg check + string[] arguments = { "-f", "lavfi", @@ -497,41 +515,24 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory } } - private async Task GetNvidiaCapabilities( - string ffmpegPath, - IFFmpegCapabilities ffmpegCapabilities) + private IHardwareCapabilities GetNvidiaCapabilities(IFFmpegCapabilities ffmpegCapabilities) { - if (_memoryCache.TryGetValue(ArchitectureCacheKey, out int cachedArchitecture) - && _memoryCache.TryGetValue(ModelCacheKey, out string? cachedModel) - && cachedModel is not null) + if (_memoryCache.TryGetValue(CudaDeviceKey, out CudaDevice? cudaDevice) && cudaDevice is not null) { - return new NvidiaHardwareCapabilities( - cachedArchitecture, - cachedModel, - ffmpegCapabilities, - _logger); + return new NvidiaHardwareCapabilities(cudaDevice, ffmpegCapabilities, _logger); } - string output = await GetNvidiaOutput(ffmpegPath); - - Option maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU"))); - foreach (string line in maybeLine) + Option> maybeDevices = CudaHelper.GetDevices(); + foreach (CudaDevice firstDevice in maybeDevices.Map(list => list.HeadOrNone())) { - const string ARCHITECTURE_PATTERN = @"SM\s+(\d+\.\d+)"; - Match match = Regex.Match(line, ARCHITECTURE_PATTERN); - if (match.Success && int.TryParse(match.Groups[1].Value.Replace(".", string.Empty), out int architecture)) - { - const string MODEL_PATTERN = @"(GTX\s+[0-9a-zA-Z]+[\sTtIi]+)"; - Match modelMatch = Regex.Match(line, MODEL_PATTERN); - string model = modelMatch.Success ? modelMatch.Groups[1].Value.Trim() : "unknown"; - _logger.LogDebug( - "Detected NVIDIA GPU model {Model} architecture SM {Architecture}", - model, - architecture); - _memoryCache.Set(ArchitectureCacheKey, architecture); - _memoryCache.Set(ModelCacheKey, model); - return new NvidiaHardwareCapabilities(architecture, model, ffmpegCapabilities, _logger); - } + _logger.LogDebug( + "Detected NVIDIA GPU model {Model} architecture SM {Major}.{Minor}", + firstDevice.Model, + firstDevice.Version.Major, + firstDevice.Version.Minor); + + _memoryCache.Set(CudaDeviceKey, firstDevice); + return new NvidiaHardwareCapabilities(firstDevice, ffmpegCapabilities, _logger); } _logger.LogWarning( diff --git a/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaDevice.cs b/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaDevice.cs new file mode 100644 index 000000000..a0d2eeda9 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaDevice.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.FFmpeg.Capabilities.Nvidia; + +public record CudaDevice(int Handle, string Model, Version Version); diff --git a/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs b/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs new file mode 100644 index 000000000..93767830f --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs @@ -0,0 +1,132 @@ +using System.Globalization; +using System.Text; +using ErsatzTV.FFmpeg.Format; +using Lennox.NvEncSharp; + +namespace ErsatzTV.FFmpeg.Capabilities.Nvidia; + +internal static class CudaHelper +{ + private static bool _success; + private static bool _initialized; + private static readonly Lock Lock = new(); + + private static readonly Dictionary AllCodecs = new() + { + [VideoFormat.H264] = NvEncCodecGuids.H264, + [VideoFormat.Hevc] = NvEncCodecGuids.Hevc + }; + + private static bool EnsureInit() + { + if (_initialized) + { + return _success; + } + + lock (Lock) + { + if (_initialized) + { + return _success; + } + + try + { + LibNvEnc.TryInitialize(out string? error); + if (string.IsNullOrEmpty(error)) + { + LibCuda.Initialize(); + _success = true; + } + } + catch (LibNvEncException) + { + _success = false; + } + + _initialized = true; + } + + return _success; + } + + internal static Option> GetDevices() + { + var result = new List(); + + if (!EnsureInit()) + { + return Option>.None; + } + + foreach (var description in CuDevice.GetDescriptions()) + { + var device = description.Device; + + string name = device.GetName(); + int nullIndex = name.IndexOf('\0'); + if (nullIndex > 0) + { + name = name[..nullIndex]; + } + + int major = device.GetAttribute(CuDeviceAttribute.ComputeCapabilityMajor); + int minor = device.GetAttribute(CuDeviceAttribute.ComputeCapabilityMinor); + + result.Add(new CudaDevice(device.Handle, name, new Version(major, minor))); + } + + return result; + } + + internal static string GetDeviceDetails(CudaDevice device) + { + var sb = new StringBuilder(); + + try + { + var dev = CuDevice.GetDevice(device.Handle); + using var context = dev.CreateContext(); + var sessionParams = new NvEncOpenEncodeSessionExParams + { + Version = LibNvEnc.NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER, + ApiVersion = LibNvEnc.NVENCAPI_VERSION, + Device = context.Handle, + DeviceType = NvEncDeviceType.Cuda + }; + + var encoder = LibNvEnc.OpenEncoder(ref sessionParams); + try + { + sb.AppendLine(" Encoding:"); + IReadOnlyList codecGuids = encoder.GetEncodeGuids(); + foreach ((string codecName, Guid codecGuid) in AllCodecs) + { + if (codecGuids.Contains(codecGuid)) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" - Supports {codecName} 8-bit"); + + var cap = new NvEncCapsParam { CapsToQuery = NvEncCaps.Support10bitEncode }; + var capsVal = 0; + encoder.GetEncodeCaps(codecGuid, ref cap, ref capsVal); + if (capsVal > 0) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" - Supports {codecName} 10-bit"); + } + } + } + } + finally + { + encoder.DestroyEncoder(); + } + } + catch (Exception) + { + // do nothing + } + + return sb.ToString(); + } +} diff --git a/ErsatzTV.FFmpeg/Capabilities/Nvidia/NvEncSharpRedirector.cs b/ErsatzTV.FFmpeg/Capabilities/Nvidia/NvEncSharpRedirector.cs new file mode 100644 index 000000000..ff141ce64 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/Nvidia/NvEncSharpRedirector.cs @@ -0,0 +1,43 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace ErsatzTV.FFmpeg.Capabilities.Nvidia; + +public static class NvEncSharpRedirector +{ + static NvEncSharpRedirector() + { + NativeLibrary.SetDllImportResolver(typeof(Lennox.NvEncSharp.LibCuda).Assembly, Resolver); + } + + private static IntPtr Resolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName.Equals("nvEncodeAPI64.dll", StringComparison.OrdinalIgnoreCase)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return NativeLibrary.Load("libnvidia-encode.so", assembly, searchPath); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return NativeLibrary.Load("nvEncodeAPI64.dll", assembly, searchPath); + } + + if (libraryName.Equals("nvEncodeAPI.dll", StringComparison.OrdinalIgnoreCase)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return NativeLibrary.Load("libnvidia-encode.so", assembly, searchPath); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return NativeLibrary.Load("nvEncodeAPI.dll", assembly, searchPath); + } + + if (libraryName.Equals("nvcuda.dll", StringComparison.OrdinalIgnoreCase)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return NativeLibrary.Load("libcuda.so", assembly, searchPath); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return NativeLibrary.Load("nvcuda.dll", assembly, searchPath); + } + + return IntPtr.Zero; + } + + public static void Init() { } +} diff --git a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs index fc5706e05..57ade22e3 100644 --- a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs @@ -1,30 +1,32 @@ +using ErsatzTV.FFmpeg.Capabilities.Nvidia; using ErsatzTV.FFmpeg.Format; +using Lennox.NvEncSharp; using Microsoft.Extensions.Logging; namespace ErsatzTV.FFmpeg.Capabilities; public class NvidiaHardwareCapabilities : IHardwareCapabilities { - private readonly int _architecture; + private readonly CudaDevice _cudaDevice; private readonly IFFmpegCapabilities _ffmpegCapabilities; private readonly ILogger _logger; - private readonly List _maxwellGm206 = new() { "GTX 750", "GTX 950", "GTX 960", "GTX 965M" }; - private readonly string _model; + private readonly List _maxwellGm206 = ["GTX 750", "GTX 950", "GTX 960", "GTX 965M"]; + private readonly Version _maxwell = new(5, 2); + private readonly Version _pascal = new(6, 0); + private readonly Version _ampere = new(8, 6); public NvidiaHardwareCapabilities( - int architecture, - string model, + CudaDevice cudaDevice, IFFmpegCapabilities ffmpegCapabilities, ILogger logger) { - _architecture = architecture; - _model = model; + _cudaDevice = cudaDevice; _ffmpegCapabilities = ffmpegCapabilities; _logger = logger; } // this fails with some 1650 cards, so let's try greater than 75 - public bool HevcBFrames => _architecture > 75; + public bool HevcBFrames => _cudaDevice.Version >= new Version(7, 5); public FFmpegCapability CanDecode( string videoFormat, @@ -39,13 +41,13 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities bool isHardware = videoFormat switch { // some second gen maxwell can decode hevc, otherwise pascal is required - VideoFormat.Hevc => _architecture == 52 && _maxwellGm206.Contains(_model) || _architecture >= 60, + VideoFormat.Hevc => _cudaDevice.Version == _maxwell && _maxwellGm206.Contains(_cudaDevice.Model) || _cudaDevice.Version >= _pascal, // pascal is required to decode vp9 10-bit - VideoFormat.Vp9 when bitDepth == 10 => !isHdr && _architecture >= 60, + VideoFormat.Vp9 when bitDepth == 10 => !isHdr && _cudaDevice.Version >= _pascal, // some second gen maxwell can decode vp9, otherwise pascal is required - VideoFormat.Vp9 => !isHdr && _architecture == 52 && _maxwellGm206.Contains(_model) || _architecture >= 60, + VideoFormat.Vp9 => !isHdr && _cudaDevice.Version == _maxwell && _maxwellGm206.Contains(_cudaDevice.Model) || _cudaDevice.Version >= _pascal, // no hardware decoding of 10-bit h264 VideoFormat.H264 => bitDepth < 10, @@ -58,7 +60,7 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities VideoFormat.Mpeg4 => false, // ampere is required for av1 decoding - VideoFormat.Av1 => _architecture >= 86, + VideoFormat.Av1 => _cudaDevice.Version >= _ampere, // generated images are decoded into software VideoFormat.GeneratedImage => false, @@ -93,21 +95,92 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities { int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); - bool isHardware = videoFormat switch + try { - // pascal is required to encode 10-bit hevc - VideoFormat.Hevc when bitDepth == 10 => _architecture >= 60, - - // second gen maxwell is required to encode hevc - VideoFormat.Hevc => _architecture >= 52, - - // nvidia cannot encode 10-bit h264 - VideoFormat.H264 when bitDepth == 10 => false, + var dev = CuDevice.GetDevice(0); + using var context = dev.CreateContext(); + var sessionParams = new NvEncOpenEncodeSessionExParams + { + Version = LibNvEnc.NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER, + ApiVersion = LibNvEnc.NVENCAPI_VERSION, + Device = context.Handle, + DeviceType = NvEncDeviceType.Cuda + }; - _ => true - }; + var encoder = LibNvEnc.OpenEncoder(ref sessionParams); + try + { + _logger.LogDebug( + "Checking NvEnc {Format} / {Profile} / {BitDepth}-bit", + videoFormat, + videoProfile, + bitDepth); + + var codecGuid = videoFormat switch + { + VideoFormat.Hevc => NvEncCodecGuids.Hevc, + _ => NvEncCodecGuids.H264 + }; + + IReadOnlyList codecGuids = encoder.GetEncodeGuids(); + if (!codecGuids.Contains(codecGuid)) + { + _logger.LogWarning("NvEnc {Format} is not supported; will use software encode", videoFormat); + return FFmpegCapability.Software; + } + + var profileGuid = (videoFormat, videoProfile.IfNone(string.Empty), bitDepth) switch + { + (VideoFormat.Hevc, _, 8) => NvEncProfileGuids.HevcMain, + (VideoFormat.Hevc, _, 10) => NvEncProfileGuids.HevcMain10, + + (VideoFormat.H264, _, 10) => NvEncProfileGuids.H264High444, + (VideoFormat.H264, VideoProfile.High, _) => NvEncProfileGuids.H264High, + // high10 is for libx264, nvenc needs high444 + (VideoFormat.H264, VideoProfile.High10, _) => NvEncProfileGuids.H264High444, + + _ => NvEncProfileGuids.H264Main + }; + + IReadOnlyList profileGuids = encoder.GetEncodeProfileGuids(codecGuid); + if (!profileGuids.Contains(profileGuid)) + { + _logger.LogWarning( + "NvEnc {Format} / {Profile} is not supported; will use software encode", + videoFormat, + videoProfile); + return FFmpegCapability.Software; + } + + if (bitDepth == 10) + { + var cap = new NvEncCapsParam { CapsToQuery = NvEncCaps.Support10bitEncode }; + var capsVal = 0; + encoder.GetEncodeCaps(codecGuid, ref cap, ref capsVal); + if (capsVal == 0) + { + _logger.LogWarning( + "NvEnc {Format} / {Profile} / {BitDepth}-bit is not supported; will use software encode", + videoFormat, + videoProfile, + bitDepth); + return FFmpegCapability.Software; + } + } + + return FFmpegCapability.Hardware; + } + finally + { + encoder.DestroyEncoder(); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unexpected error checking NvEnc capabilities; falling back to software"); + } - return isHardware ? FFmpegCapability.Hardware : FFmpegCapability.Software; + return FFmpegCapability.Software; } public Option GetRateControlMode(string videoFormat, Option maybePixelFormat) => diff --git a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj index 20c8e2837..6db981a40 100644 --- a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj +++ b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj @@ -13,6 +13,7 @@ + diff --git a/ErsatzTV.FFmpeg/Format/VideoProfile.cs b/ErsatzTV.FFmpeg/Format/VideoProfile.cs index 49f26ac38..ff432105e 100644 --- a/ErsatzTV.FFmpeg/Format/VideoProfile.cs +++ b/ErsatzTV.FFmpeg/Format/VideoProfile.cs @@ -5,4 +5,5 @@ public static class VideoProfile public const string Main = "main"; public const string High = "high"; public const string High10 = "high10"; + public const string High444p = "high444p"; } diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index fedc5fe99..6f2271dba 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -311,7 +311,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc(desiredState.VideoProfile, desiredState.VideoPreset), - (_, _) => GetSoftwareEncoder(ffmpegState, currentState, desiredState) + // don't pass NVENC profile down to libx264 + (_, _) => GetSoftwareEncoder(ffmpegState, currentState, desiredState with { VideoProfile = Option.None }) }; foreach (IEncoder encoder in maybeEncoder) @@ -391,7 +392,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) { - _logger.LogDebug("Using software encoder"); + //_logger.LogDebug("Using software encoder"); if ((context.HasSubtitleOverlay || context.HasWatermark || context.HasGraphicsEngine) && currentState.FrameDataLocation == FrameDataLocation.Hardware) diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 06221a09d..11f40a812 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -79,7 +79,14 @@ Clearable="true"> main high - high10 + @if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc) + { + high444p + } + else + { + high10 + } diff --git a/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs b/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs index 6e00ab6fc..73992954f 100644 --- a/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs +++ b/ErsatzTV/Services/RunOnce/PlatformSettingsService.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.FFmpeg.Capabilities.Nvidia; using ErsatzTV.FFmpeg.Runtime; using Microsoft.Extensions.Caching.Memory; @@ -16,6 +17,8 @@ public class PlatformSettingsService(IServiceScopeFactory serviceScopeFactory) : IRuntimeInfo runtimeInfo = scope.ServiceProvider.GetRequiredService(); if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux)) { + NvEncSharpRedirector.Init(); + if (Directory.Exists("/dev/dri")) { ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService(); diff --git a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs index 43b7169ea..98a23456f 100644 --- a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs +++ b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs @@ -127,11 +127,17 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.TenBit, + x => x.HardwareAcceleration != HardwareAccelerationKind.Nvenc && x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.TenBit, () => RuleFor(x => x.VideoProfile) .Must(vp => vp == VideoProfile.High10) .WithMessage("VideoProfile must be high10 with 10-bit h264")); + When( + x => x.HardwareAcceleration == HardwareAccelerationKind.Nvenc && x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.TenBit, + () => RuleFor(x => x.VideoProfile) + .Must(vp => vp == VideoProfile.High444p) + .WithMessage("VideoProfile must be high444p with NVIDIA 10-bit h264")); + When( x => x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.EightBit, () => RuleFor(x => x.VideoProfile) diff --git a/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs b/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs index 66b276ab1..d4b8d9f68 100644 --- a/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs +++ b/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs @@ -7,6 +7,8 @@ namespace ErsatzTV.ViewModels; public class FFmpegProfileEditViewModel { + private string _videoProfile; + public FFmpegProfileEditViewModel() { } @@ -62,7 +64,20 @@ public class FFmpegProfileEditViewModel public int VideoBitrate { get; set; } public int VideoBufferSize { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; } - public string VideoProfile { get; set; } + + public string VideoProfile + { + get => + (HardwareAcceleration, VideoFormat, BitDepth) switch + { + (HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, FFmpegProfileBitDepth.TenBit) => FFmpeg + .Format.VideoProfile.High444p, + (_, FFmpegProfileVideoFormat.H264, _) => _videoProfile, + _ => string.Empty + }; + set => _videoProfile = value; + } + public string VideoPreset { get; set; } public bool AllowBFrames { get; set; } public FFmpegProfileBitDepth BitDepth { get; set; }