Browse Source

use nvenc to detect encoder capability (#2459)

pull/2460/head
Jason Dove 10 months ago committed by GitHub
parent
commit
b820b798cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 1
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  3. 67
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  4. 3
      ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaDevice.cs
  5. 132
      ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaHelper.cs
  6. 43
      ErsatzTV.FFmpeg/Capabilities/Nvidia/NvEncSharpRedirector.cs
  7. 121
      ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs
  8. 1
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  9. 1
      ErsatzTV.FFmpeg/Format/VideoProfile.cs
  10. 5
      ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs
  11. 9
      ErsatzTV/Pages/FFmpegEditor.razor
  12. 3
      ErsatzTV/Services/RunOnce/PlatformSettingsService.cs
  13. 8
      ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs
  14. 17
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

1
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 - 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 - Targeted deep scans will now extract text subtitles for the scanned show
- Fix playlist preview - Fix playlist preview
- Use NvEnc API to detect encoder capability instead of heuristic based on GPU model/architecture
### Changed ### Changed
- Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration - Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration

1
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -1114,6 +1114,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
(VideoFormat.H264, VideoProfile.Main) => VideoProfile.Main, (VideoFormat.H264, VideoProfile.Main) => VideoProfile.Main,
(VideoFormat.H264, VideoProfile.High) => VideoProfile.High, (VideoFormat.H264, VideoProfile.High) => VideoProfile.High,
(VideoFormat.H264, VideoProfile.High10) => VideoProfile.High10, (VideoFormat.H264, VideoProfile.High10) => VideoProfile.High10,
(VideoFormat.H264, VideoProfile.High444p) => VideoProfile.High444p,
_ => Option<string>.None _ => Option<string>.None
}; };

67
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -5,6 +5,7 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using ErsatzTV.FFmpeg.Capabilities.Nvidia;
using ErsatzTV.FFmpeg.Capabilities.Qsv; using ErsatzTV.FFmpeg.Capabilities.Qsv;
using ErsatzTV.FFmpeg.Capabilities.Vaapi; using ErsatzTV.FFmpeg.Capabilities.Vaapi;
using ErsatzTV.FFmpeg.Capabilities.VideoToolbox; using ErsatzTV.FFmpeg.Capabilities.VideoToolbox;
@ -18,8 +19,7 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{ {
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture"; private const string CudaDeviceKey = "ffmpeg.hardware.cuda.device";
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model";
private static readonly CompositeFormat private static readonly CompositeFormat
VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}"); VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}");
@ -104,7 +104,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return hardwareAccelerationMode switch return hardwareAccelerationMode switch
{ {
HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities), HardwareAccelerationMode.Nvenc => GetNvidiaCapabilities(ffmpegCapabilities),
HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice), HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice),
HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDisplay, vaapiDriver, vaapiDevice), HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDisplay, vaapiDriver, vaapiDevice),
HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareCapabilities(ffmpegCapabilities, _logger), HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareCapabilities(ffmpegCapabilities, _logger),
@ -122,6 +122,24 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return string.Empty; return string.Empty;
} }
Option<List<CudaDevice>> maybeDevices = CudaHelper.GetDevices();
foreach (List<CudaDevice> 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 = string[] arguments =
{ {
"-f", "lavfi", "-f", "lavfi",
@ -497,41 +515,24 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
} }
} }
private async Task<IHardwareCapabilities> GetNvidiaCapabilities( private IHardwareCapabilities GetNvidiaCapabilities(IFFmpegCapabilities ffmpegCapabilities)
string ffmpegPath,
IFFmpegCapabilities ffmpegCapabilities)
{ {
if (_memoryCache.TryGetValue(ArchitectureCacheKey, out int cachedArchitecture) if (_memoryCache.TryGetValue(CudaDeviceKey, out CudaDevice? cudaDevice) && cudaDevice is not null)
&& _memoryCache.TryGetValue(ModelCacheKey, out string? cachedModel)
&& cachedModel is not null)
{ {
return new NvidiaHardwareCapabilities( return new NvidiaHardwareCapabilities(cudaDevice, ffmpegCapabilities, _logger);
cachedArchitecture,
cachedModel,
ffmpegCapabilities,
_logger);
} }
string output = await GetNvidiaOutput(ffmpegPath); Option<List<CudaDevice>> maybeDevices = CudaHelper.GetDevices();
foreach (CudaDevice firstDevice in maybeDevices.Map(list => list.HeadOrNone()))
Option<string> maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU")));
foreach (string line in maybeLine)
{ {
const string ARCHITECTURE_PATTERN = @"SM\s+(\d+\.\d+)"; _logger.LogDebug(
Match match = Regex.Match(line, ARCHITECTURE_PATTERN); "Detected NVIDIA GPU model {Model} architecture SM {Major}.{Minor}",
if (match.Success && int.TryParse(match.Groups[1].Value.Replace(".", string.Empty), out int architecture)) firstDevice.Model,
{ firstDevice.Version.Major,
const string MODEL_PATTERN = @"(GTX\s+[0-9a-zA-Z]+[\sTtIi]+)"; firstDevice.Version.Minor);
Match modelMatch = Regex.Match(line, MODEL_PATTERN);
string model = modelMatch.Success ? modelMatch.Groups[1].Value.Trim() : "unknown"; _memoryCache.Set(CudaDeviceKey, firstDevice);
_logger.LogDebug( return new NvidiaHardwareCapabilities(firstDevice, ffmpegCapabilities, _logger);
"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.LogWarning( _logger.LogWarning(

3
ErsatzTV.FFmpeg/Capabilities/Nvidia/CudaDevice.cs

@ -0,0 +1,3 @@
namespace ErsatzTV.FFmpeg.Capabilities.Nvidia;
public record CudaDevice(int Handle, string Model, Version Version);

132
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<string, Guid> 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<List<CudaDevice>> GetDevices()
{
var result = new List<CudaDevice>();
if (!EnsureInit())
{
return Option<List<CudaDevice>>.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<Guid> 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();
}
}

43
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() { }
}

121
ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs

@ -1,30 +1,32 @@
using ErsatzTV.FFmpeg.Capabilities.Nvidia;
using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.Format;
using Lennox.NvEncSharp;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ErsatzTV.FFmpeg.Capabilities; namespace ErsatzTV.FFmpeg.Capabilities;
public class NvidiaHardwareCapabilities : IHardwareCapabilities public class NvidiaHardwareCapabilities : IHardwareCapabilities
{ {
private readonly int _architecture; private readonly CudaDevice _cudaDevice;
private readonly IFFmpegCapabilities _ffmpegCapabilities; private readonly IFFmpegCapabilities _ffmpegCapabilities;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly List<string> _maxwellGm206 = new() { "GTX 750", "GTX 950", "GTX 960", "GTX 965M" }; private readonly List<string> _maxwellGm206 = ["GTX 750", "GTX 950", "GTX 960", "GTX 965M"];
private readonly string _model; private readonly Version _maxwell = new(5, 2);
private readonly Version _pascal = new(6, 0);
private readonly Version _ampere = new(8, 6);
public NvidiaHardwareCapabilities( public NvidiaHardwareCapabilities(
int architecture, CudaDevice cudaDevice,
string model,
IFFmpegCapabilities ffmpegCapabilities, IFFmpegCapabilities ffmpegCapabilities,
ILogger logger) ILogger logger)
{ {
_architecture = architecture; _cudaDevice = cudaDevice;
_model = model;
_ffmpegCapabilities = ffmpegCapabilities; _ffmpegCapabilities = ffmpegCapabilities;
_logger = logger; _logger = logger;
} }
// this fails with some 1650 cards, so let's try greater than 75 // 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( public FFmpegCapability CanDecode(
string videoFormat, string videoFormat,
@ -39,13 +41,13 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
bool isHardware = videoFormat switch bool isHardware = videoFormat switch
{ {
// some second gen maxwell can decode hevc, otherwise pascal is required // 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 // 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 // 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 // no hardware decoding of 10-bit h264
VideoFormat.H264 => bitDepth < 10, VideoFormat.H264 => bitDepth < 10,
@ -58,7 +60,7 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
VideoFormat.Mpeg4 => false, VideoFormat.Mpeg4 => false,
// ampere is required for av1 decoding // ampere is required for av1 decoding
VideoFormat.Av1 => _architecture >= 86, VideoFormat.Av1 => _cudaDevice.Version >= _ampere,
// generated images are decoded into software // generated images are decoded into software
VideoFormat.GeneratedImage => false, VideoFormat.GeneratedImage => false,
@ -93,21 +95,92 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
{ {
int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8);
bool isHardware = videoFormat switch try
{ {
// pascal is required to encode 10-bit hevc var dev = CuDevice.GetDevice(0);
VideoFormat.Hevc when bitDepth == 10 => _architecture >= 60, using var context = dev.CreateContext();
var sessionParams = new NvEncOpenEncodeSessionExParams
// second gen maxwell is required to encode hevc {
VideoFormat.Hevc => _architecture >= 52, Version = LibNvEnc.NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
ApiVersion = LibNvEnc.NVENCAPI_VERSION,
// nvidia cannot encode 10-bit h264 Device = context.Handle,
VideoFormat.H264 when bitDepth == 10 => false, 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<Guid> 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<Guid> 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<RateControlMode> GetRateControlMode(string videoFormat, Option<IPixelFormat> maybePixelFormat) => public Option<RateControlMode> GetRateControlMode(string videoFormat, Option<IPixelFormat> maybePixelFormat) =>

1
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -13,6 +13,7 @@
<PackageReference Include="CliWrap" Version="3.9.0" /> <PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Hardware.Info" Version="101.1.0" /> <PackageReference Include="Hardware.Info" Version="101.1.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Lennox.NvEncSharp" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
</ItemGroup> </ItemGroup>

1
ErsatzTV.FFmpeg/Format/VideoProfile.cs

@ -5,4 +5,5 @@ public static class VideoProfile
public const string Main = "main"; public const string Main = "main";
public const string High = "high"; public const string High = "high";
public const string High10 = "high10"; public const string High10 = "high10";
public const string High444p = "high444p";
} }

5
ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs

@ -311,7 +311,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
(HardwareAccelerationMode.Nvenc, VideoFormat.H264) => (HardwareAccelerationMode.Nvenc, VideoFormat.H264) =>
new EncoderH264Nvenc(desiredState.VideoProfile, desiredState.VideoPreset), 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<string>.None })
}; };
foreach (IEncoder encoder in maybeEncoder) foreach (IEncoder encoder in maybeEncoder)
@ -391,7 +392,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None)
{ {
_logger.LogDebug("Using software encoder"); //_logger.LogDebug("Using software encoder");
if ((context.HasSubtitleOverlay || context.HasWatermark || context.HasGraphicsEngine) && if ((context.HasSubtitleOverlay || context.HasWatermark || context.HasGraphicsEngine) &&
currentState.FrameDataLocation == FrameDataLocation.Hardware) currentState.FrameDataLocation == FrameDataLocation.Hardware)

9
ErsatzTV/Pages/FFmpegEditor.razor

@ -79,7 +79,14 @@
Clearable="true"> Clearable="true">
<MudSelectItem Value="@VideoProfile.Main">main</MudSelectItem> <MudSelectItem Value="@VideoProfile.Main">main</MudSelectItem>
<MudSelectItem Value="@VideoProfile.High">high</MudSelectItem> <MudSelectItem Value="@VideoProfile.High">high</MudSelectItem>
<MudSelectItem Value="@VideoProfile.High10">high10</MudSelectItem> @if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc)
{
<MudSelectItem Value="@VideoProfile.High444p">high444p</MudSelectItem>
}
else
{
<MudSelectItem Value="@VideoProfile.High10">high10</MudSelectItem>
}
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">

3
ErsatzTV/Services/RunOnce/PlatformSettingsService.cs

@ -1,6 +1,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Capabilities.Nvidia;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -16,6 +17,8 @@ public class PlatformSettingsService(IServiceScopeFactory serviceScopeFactory) :
IRuntimeInfo runtimeInfo = scope.ServiceProvider.GetRequiredService<IRuntimeInfo>(); IRuntimeInfo runtimeInfo = scope.ServiceProvider.GetRequiredService<IRuntimeInfo>();
if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux)) if (runtimeInfo != null && runtimeInfo.IsOSPlatform(OSPlatform.Linux))
{ {
NvEncSharpRedirector.Init();
if (Directory.Exists("/dev/dri")) if (Directory.Exists("/dev/dri"))
{ {
ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>(); ILocalFileSystem localFileSystem = scope.ServiceProvider.GetRequiredService<ILocalFileSystem>();

8
ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs

@ -127,11 +127,17 @@ public class FFmpegProfileEditViewModelValidator : AbstractValidator<FFmpegProfi
.WithMessage("Mpeg2Video does not support 10-bit content")); .WithMessage("Mpeg2Video does not support 10-bit content"));
When( When(
x => 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) () => RuleFor(x => x.VideoProfile)
.Must(vp => vp == VideoProfile.High10) .Must(vp => vp == VideoProfile.High10)
.WithMessage("VideoProfile must be high10 with 10-bit h264")); .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( When(
x => x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.EightBit, x => x.VideoFormat == FFmpegProfileVideoFormat.H264 && x.BitDepth == FFmpegProfileBitDepth.EightBit,
() => RuleFor(x => x.VideoProfile) () => RuleFor(x => x.VideoProfile)

17
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -7,6 +7,8 @@ namespace ErsatzTV.ViewModels;
public class FFmpegProfileEditViewModel public class FFmpegProfileEditViewModel
{ {
private string _videoProfile;
public FFmpegProfileEditViewModel() public FFmpegProfileEditViewModel()
{ {
} }
@ -62,7 +64,20 @@ public class FFmpegProfileEditViewModel
public int VideoBitrate { get; set; } public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; } public int VideoBufferSize { get; set; }
public FFmpegProfileVideoFormat VideoFormat { 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 string VideoPreset { get; set; }
public bool AllowBFrames { get; set; } public bool AllowBFrames { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; } public FFmpegProfileBitDepth BitDepth { get; set; }

Loading…
Cancel
Save