From dd7f77751c0a9a6060cecf902ad285309d25c1d5 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Tue, 14 Jun 2022 19:44:34 -0500 Subject: [PATCH] detect nvidia capabilities (#853) * fallback to software codecs for old nvidia cards * update dependencies --- CHANGELOG.md | 2 + .../Queries/GetCachedImagePathHandler.cs | 2 +- .../GetConcatProcessByChannelNumberHandler.cs | 2 +- .../FFmpeg/TranscodingTests.cs | 4 + .../FFmpeg/FFmpegLibraryProcessService.cs | 19 ++++- .../FFmpeg/IFFmpegProcessService.cs | 4 +- ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs | 26 ++++++- .../DefaultHardwareCapabilities.cs | 7 ++ .../HardwareCapabilitiesFactory.cs | 76 +++++++++++++++++++ .../Capabilities/IHardwareCapabilities.cs | 7 ++ .../IHardwareCapabilitiesFactory.cs | 8 ++ .../Capabilities/NoHardwareCapabilities.cs | 7 ++ .../NvidiaHardwareCapabilities.cs | 26 +++++++ ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs | 20 +++-- .../Decoder/Cuvid/DecoderH264Cuvid.cs | 11 ++- .../Decoder/Cuvid/DecoderHevcCuvid.cs | 11 ++- .../Decoder/Cuvid/DecoderMpeg2Cuvid.cs | 13 +++- .../Decoder/Cuvid/DecoderMpeg4Cuvid.cs | 11 ++- .../Decoder/Cuvid/DecoderVc1Cuvid.cs | 11 ++- .../Decoder/Cuvid/DecoderVp9Cuvid.cs | 11 ++- ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs | 68 ++++++++++------- ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs | 9 ++- ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj | 1 + ErsatzTV.FFmpeg/FFmpegState.cs | 4 +- ErsatzTV.FFmpeg/Filter/ComplexFilter.cs | 16 ++-- .../Filter/HardwareUploadFilter.cs | 4 +- .../Filter/SubtitleHardwareUploadFilter.cs | 2 +- .../Filter/SubtitlePixelFormatFilter.cs | 2 +- .../Filter/WatermarkHardwareUploadFilter.cs | 2 +- .../Filter/WatermarkPixelFormatFilter.cs | 2 +- ErsatzTV.FFmpeg/PipelineBuilder.cs | 76 +++++++++++++------ .../ErsatzTV.Infrastructure.csproj | 6 +- ErsatzTV/ErsatzTV.csproj | 10 +-- ErsatzTV/Startup.cs | 2 + 34 files changed, 376 insertions(+), 106 deletions(-) create mode 100644 ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs create mode 100644 ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4b04e7c..637ded3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Fixed - Fix content repeating for up to a minute near the top of every hour +- Check whether hardware-accelerated hevc codecs are supported by the NVIDIA card + - Software codecs will be used if they are unsupported by the NVIDIA card ## [0.6.1-beta] - 2022-06-03 ### Fixed diff --git a/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs b/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs index 2f366990e..bad86168f 100644 --- a/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs +++ b/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs @@ -69,7 +69,7 @@ public class string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None); - Command process = _ffmpegProcessService.ResizeImage( + Command process = await _ffmpegProcessService.ResizeImage( ffmpegPath, originalPath, withExtension, diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs index 7399bcff5..7864d5450 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs @@ -30,7 +30,7 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler(ConfigElementKey.FFmpegSaveReports) .Map(result => result.IfNone(false)); - Command process = _ffmpegProcessService.ConcatChannel( + Command process = await _ffmpegProcessService.ConcatChannel( ffmpegPath, saveReports, channel, diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index ea8cec7e6..ae9ad4377 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.State; using FluentAssertions; using Microsoft.Extensions.Caching.Memory; @@ -302,6 +303,9 @@ public class TranscodingTests new FFmpegPlaybackSettingsCalculator(), new FakeStreamSelector(), new Mock().Object, + new HardwareCapabilitiesFactory( + new MemoryCache(new MemoryCacheOptions()), + LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()); var v = new MediaVersion diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 621e5d4bb..57bd3e16e 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.FFmpeg; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.OutputFormat; @@ -16,6 +17,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService { private readonly FFmpegProcessService _ffmpegProcessService; private readonly IFFmpegStreamSelector _ffmpegStreamSelector; + private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory; private readonly ILogger _logger; private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator; private readonly ITempFilePool _tempFilePool; @@ -25,12 +27,14 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService FFmpegPlaybackSettingsCalculator playbackSettingsCalculator, IFFmpegStreamSelector ffmpegStreamSelector, ITempFilePool tempFilePool, + IHardwareCapabilitiesFactory hardwareCapabilitiesFactory, ILogger logger) { _ffmpegProcessService = ffmpegProcessService; _playbackSettingsCalculator = playbackSettingsCalculator; _ffmpegStreamSelector = ffmpegStreamSelector; _tempFilePool = tempFilePool; + _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; _logger = logger; } @@ -214,6 +218,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService var ffmpegState = new FFmpegState( saveReports, hwAccel, + hwAccel, VaapiDriverName(hwAccel, vaapiDriver), VaapiDeviceName(hwAccel, vaapiDevice), playbackSettings.StreamSeek, @@ -231,6 +236,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService _logger.LogDebug("FFmpeg desired state {FrameState}", desiredState); var pipelineBuilder = new PipelineBuilder( + await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel), videoInputFile, audioInputFile, watermarkInputFile, @@ -333,6 +339,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService var ffmpegState = new FFmpegState( false, hwAccel, + hwAccel, VaapiDriverName(hwAccel, vaapiDriver), VaapiDeviceName(hwAccel, vaapiDevice), playbackSettings.StreamSeek, @@ -359,6 +366,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService _logger.LogDebug("FFmpeg desired error state {FrameState}", desiredState); var pipelineBuilder = new PipelineBuilder( + await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hwAccel), videoInputFile, audioInputFile, None, @@ -372,7 +380,12 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, pipeline); } - public Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) + public async Task ConcatChannel( + string ffmpegPath, + bool saveReports, + Channel channel, + string scheme, + string host) { var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height); @@ -381,6 +394,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService resolution); var pipelineBuilder = new PipelineBuilder( + await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None), None, None, None, @@ -399,13 +413,14 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) => _ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host); - public Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height) + public async Task ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height) { var videoInputFile = new VideoInputFile( inputFile, new List { new(0, string.Empty, None, FrameSize.Unknown, None, true) }); var pipelineBuilder = new PipelineBuilder( + await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, HardwareAccelerationMode.None), videoInputFile, None, None, diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs index 9fa2c8dd6..a5674660f 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -46,11 +46,11 @@ public interface IFFmpegProcessService VaapiDriver vaapiDriver, string vaapiDevice); - Command ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); + Task ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); - Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height); + Task ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height); Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile); diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs index a4659cc68..7434ed41f 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Encoder; using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.OutputFormat; @@ -54,6 +55,7 @@ public class PipelineGeneratorTests var ffmpegState = new FFmpegState( false, HardwareAccelerationMode.None, + HardwareAccelerationMode.None, Option.None, Option.None, TimeSpan.FromSeconds(1), @@ -68,7 +70,15 @@ public class PipelineGeneratorTests 0, Option.None); - var builder = new PipelineBuilder(videoInputFile, audioInputFile, None, None, "", "", _logger); + var builder = new PipelineBuilder( + new DefaultHardwareCapabilities(), + videoInputFile, + audioInputFile, + None, + None, + "", + "", + _logger); FFmpegPipeline result = builder.Build(ffmpegState, desiredState); result.PipelineSteps.Should().HaveCountGreaterThan(0); @@ -85,7 +95,7 @@ public class PipelineGeneratorTests var resolution = new FrameSize(1920, 1080); var concatInputFile = new ConcatInputFile("http://localhost:8080/ffmpeg/concat/1", resolution); - var builder = new PipelineBuilder(None, None, None, None, "", "", _logger); + var builder = new PipelineBuilder(new DefaultHardwareCapabilities(), None, None, None, None, "", "", _logger); FFmpegPipeline result = builder.Concat(concatInputFile, FFmpegState.Concat(false, "Some Channel")); result.PipelineSteps.Should().HaveCountGreaterThan(0); @@ -132,6 +142,7 @@ public class PipelineGeneratorTests var ffmpegState = new FFmpegState( false, HardwareAccelerationMode.None, + HardwareAccelerationMode.None, Option.None, Option.None, Option.None, @@ -146,7 +157,15 @@ public class PipelineGeneratorTests 0, Option.None); - var builder = new PipelineBuilder(videoInputFile, audioInputFile, None, None, "", "", _logger); + var builder = new PipelineBuilder( + new DefaultHardwareCapabilities(), + videoInputFile, + audioInputFile, + None, + None, + "", + "", + _logger); FFmpegPipeline result = builder.Build(ffmpegState, desiredState); result.PipelineSteps.Should().HaveCountGreaterThan(0); @@ -172,6 +191,7 @@ public class PipelineGeneratorTests }); var pipelineBuilder = new PipelineBuilder( + new DefaultHardwareCapabilities(), videoInputFile, Option.None, Option.None, diff --git a/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs new file mode 100644 index 000000000..1034ca641 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/DefaultHardwareCapabilities.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.FFmpeg.Capabilities; + +public class DefaultHardwareCapabilities : IHardwareCapabilities +{ + public bool CanDecode(string videoFormat) => true; + public bool CanEncode(string videoFormat) => true; +} diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs new file mode 100644 index 000000000..50734d1d0 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -0,0 +1,76 @@ +using System.Text; +using System.Text.RegularExpressions; +using CliWrap; +using CliWrap.Buffered; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.FFmpeg.Capabilities; + +public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory +{ + private const string CacheKey = "ffmpeg.hardware.nvidia.architecture"; + private readonly ILogger _logger; + + private readonly IMemoryCache _memoryCache; + + public HardwareCapabilitiesFactory(IMemoryCache memoryCache, ILogger logger) + { + _memoryCache = memoryCache; + _logger = logger; + } + + public async Task GetHardwareCapabilities( + string ffmpegPath, + HardwareAccelerationMode hardwareAccelerationMode) => + hardwareAccelerationMode switch + { + HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath), + _ => new DefaultHardwareCapabilities() + }; + + private async Task GetNvidiaCapabilities(string ffmpegPath) + { + if (_memoryCache.TryGetValue(CacheKey, out int cachedArchitecture)) + { + return new NvidiaHardwareCapabilities(cachedArchitecture); + } + + string[] arguments = + { + "-f", "lavfi", + "-i", "nullsrc", + "-c:v", "h264_nvenc", + "-gpu", "list", + "-f", "null", "-" + }; + + BufferedCommandResult result = await Cli.Wrap(ffmpegPath) + .WithArguments(arguments) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(Encoding.UTF8); + + string output = string.IsNullOrWhiteSpace(result.StandardOutput) + ? result.StandardError + : result.StandardOutput; + + Option maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU"))); + foreach (string line in maybeLine) + { + const string PATTERN = @"SM\s+(\d\.\d)"; + Match match = Regex.Match(line, PATTERN); + if (match.Success && int.TryParse(match.Groups[1].Value.Replace(".", string.Empty), out int architecture)) + { + _logger.LogInformation("Detected NVIDIA GPU architecture SM {Architecture}", architecture); + _memoryCache.Set(CacheKey, architecture); + return new NvidiaHardwareCapabilities(architecture); + } + } + + _logger.LogWarning( + "Error detecting NVIDIA GPU capabilities; some hardware accelerated features will be unavailable: {ExitCode}", + result.ExitCode); + + return new NoHardwareCapabilities(); + } +} diff --git a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs new file mode 100644 index 000000000..cf1d9dc2f --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilities.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.FFmpeg.Capabilities; + +public interface IHardwareCapabilities +{ + public bool CanDecode(string videoFormat); + public bool CanEncode(string videoFormat); +} diff --git a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs new file mode 100644 index 000000000..3d3954927 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.FFmpeg.Capabilities; + +public interface IHardwareCapabilitiesFactory +{ + Task GetHardwareCapabilities( + string ffmpegPath, + HardwareAccelerationMode hardwareAccelerationMode); +} diff --git a/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs new file mode 100644 index 000000000..b78383b42 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/NoHardwareCapabilities.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.FFmpeg.Capabilities; + +public class NoHardwareCapabilities : IHardwareCapabilities +{ + public bool CanDecode(string videoFormat) => false; + public bool CanEncode(string videoFormat) => false; +} diff --git a/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs new file mode 100644 index 000000000..c8a8d990a --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/NvidiaHardwareCapabilities.cs @@ -0,0 +1,26 @@ +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Capabilities; + +public class NvidiaHardwareCapabilities : IHardwareCapabilities +{ + private readonly int _architecture; + + public NvidiaHardwareCapabilities(int architecture) => _architecture = architecture; + + public bool CanDecode(string videoFormat) => + videoFormat switch + { + // pascal is required to decode hevc/vp9 + VideoFormat.Hevc or VideoFormat.Vp9 => _architecture >= 60, + _ => true + }; + + public bool CanEncode(string videoFormat) => + videoFormat switch + { + // pascal is required to encode hevc + VideoFormat.Hevc => _architecture >= 60, + _ => true + }; +} diff --git a/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs b/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs index 91b177cd1..db41a4d72 100644 --- a/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs +++ b/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs @@ -1,4 +1,5 @@ -using ErsatzTV.FFmpeg.Decoder.Cuvid; +using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.FFmpeg.Decoder.Cuvid; using ErsatzTV.FFmpeg.Decoder.Qsv; using ErsatzTV.FFmpeg.Format; using Microsoft.Extensions.Logging; @@ -8,16 +9,18 @@ namespace ErsatzTV.FFmpeg.Decoder; public static class AvailableDecoders { public static Option ForVideoFormat( + IHardwareCapabilities hardwareCapabilities, FFmpegState ffmpegState, FrameState currentState, FrameState desiredState, Option watermarkInputFile, Option subtitleInputFile, ILogger logger) => - (ffmpegState.HardwareAccelerationMode, currentState.VideoFormat, + (ffmpegState.DecoderHardwareAccelerationMode, currentState.VideoFormat, currentState.PixelFormat.Match(pf => pf.Name, () => string.Empty)) switch { - (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc, _) => new DecoderHevcCuvid(), + (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc, _) + when hardwareCapabilities.CanDecode(VideoFormat.Hevc) => new DecoderHevcCuvid(ffmpegState), // nvenc doesn't support hardware decoding of 10-bit content (HardwareAccelerationMode.Nvenc, VideoFormat.H264, PixelFormat.YUV420P10LE or PixelFormat.YUV444P10LE) @@ -27,12 +30,15 @@ public static class AvailableDecoders (HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg2Video, _) when desiredState.Deinterlaced => new DecoderMpeg2Video(), - (HardwareAccelerationMode.Nvenc, VideoFormat.H264, _) => new DecoderH264Cuvid(), + (HardwareAccelerationMode.Nvenc, VideoFormat.H264, _) + when hardwareCapabilities.CanDecode(VideoFormat.H264) => new DecoderH264Cuvid(ffmpegState), (HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Cuvid( + ffmpegState, desiredState.Deinterlaced), - (HardwareAccelerationMode.Nvenc, VideoFormat.Vc1, _) => new DecoderVc1Cuvid(), - (HardwareAccelerationMode.Nvenc, VideoFormat.Vp9, _) => new DecoderVp9Cuvid(), - (HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg4, _) => new DecoderMpeg4Cuvid(), + (HardwareAccelerationMode.Nvenc, VideoFormat.Vc1, _) => new DecoderVc1Cuvid(ffmpegState), + (HardwareAccelerationMode.Nvenc, VideoFormat.Vp9, _) + when hardwareCapabilities.CanDecode(VideoFormat.Vp9) => new DecoderVp9Cuvid(ffmpegState), + (HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg4, _) => new DecoderMpeg4Cuvid(ffmpegState), // hevc_qsv decoder sometimes causes green lines with 10-bit content (HardwareAccelerationMode.Qsv, VideoFormat.Hevc, PixelFormat.YUV420P10LE) => new DecoderHevc(), diff --git a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderH264Cuvid.cs b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderH264Cuvid.cs index 355614156..ca99925c1 100644 --- a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderH264Cuvid.cs +++ b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderH264Cuvid.cs @@ -2,16 +2,23 @@ public class DecoderH264Cuvid : DecoderBase { + private readonly FFmpegState _ffmpegState; + + public DecoderH264Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; + public override string Name => "h264_cuvid"; - protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + protected override FrameDataLocation OutputFrameDataLocation => + _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None + ? FrameDataLocation.Software + : FrameDataLocation.Hardware; public override IList InputOptions(InputFile inputFile) { IList result = base.InputOptions(inputFile); result.Add("-hwaccel_output_format"); - result.Add("cuda"); + result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); return result; } diff --git a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderHevcCuvid.cs b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderHevcCuvid.cs index 3b518282a..51ebe1a34 100644 --- a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderHevcCuvid.cs +++ b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderHevcCuvid.cs @@ -2,16 +2,23 @@ public class DecoderHevcCuvid : DecoderBase { + private readonly FFmpegState _ffmpegState; + + public DecoderHevcCuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; + public override string Name => "hevc_cuvid"; - protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + protected override FrameDataLocation OutputFrameDataLocation => + _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None + ? FrameDataLocation.Software + : FrameDataLocation.Hardware; public override IList InputOptions(InputFile inputFile) { IList result = base.InputOptions(inputFile); result.Add("-hwaccel_output_format"); - result.Add("cuda"); + result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); return result; } diff --git a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg2Cuvid.cs b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg2Cuvid.cs index 32d377ce2..186699adc 100644 --- a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg2Cuvid.cs +++ b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg2Cuvid.cs @@ -3,20 +3,27 @@ public class DecoderMpeg2Cuvid : DecoderBase { private readonly bool _contentIsInterlaced; + private readonly FFmpegState _ffmpegState; - public DecoderMpeg2Cuvid(bool contentIsInterlaced) => _contentIsInterlaced = contentIsInterlaced; + public DecoderMpeg2Cuvid(FFmpegState ffmpegState, bool contentIsInterlaced) + { + _ffmpegState = ffmpegState; + _contentIsInterlaced = contentIsInterlaced; + } public override string Name => "mpeg2_cuvid"; protected override FrameDataLocation OutputFrameDataLocation => - _contentIsInterlaced ? FrameDataLocation.Software : FrameDataLocation.Hardware; + _contentIsInterlaced || _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None + ? FrameDataLocation.Software + : FrameDataLocation.Hardware; public override IList InputOptions(InputFile inputFile) { IList result = base.InputOptions(inputFile); result.Add("-hwaccel_output_format"); - result.Add("cuda"); + result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); return result; } diff --git a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg4Cuvid.cs b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg4Cuvid.cs index 9bc1c0a16..1de1d018a 100644 --- a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg4Cuvid.cs +++ b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg4Cuvid.cs @@ -2,16 +2,23 @@ public class DecoderMpeg4Cuvid : DecoderBase { + private readonly FFmpegState _ffmpegState; + + public DecoderMpeg4Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; + public override string Name => "mpeg4_cuvid"; - protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + protected override FrameDataLocation OutputFrameDataLocation => + _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None + ? FrameDataLocation.Software + : FrameDataLocation.Hardware; public override IList InputOptions(InputFile inputFile) { IList result = base.InputOptions(inputFile); result.Add("-hwaccel_output_format"); - result.Add("cuda"); + result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); return result; } diff --git a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVc1Cuvid.cs b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVc1Cuvid.cs index 078ef19b7..c671ef7f9 100644 --- a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVc1Cuvid.cs +++ b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVc1Cuvid.cs @@ -2,16 +2,23 @@ public class DecoderVc1Cuvid : DecoderBase { + private readonly FFmpegState _ffmpegState; + + public DecoderVc1Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; + public override string Name => "vc1_cuvid"; - protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + protected override FrameDataLocation OutputFrameDataLocation => + _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None + ? FrameDataLocation.Software + : FrameDataLocation.Hardware; public override IList InputOptions(InputFile inputFile) { IList result = base.InputOptions(inputFile); result.Add("-hwaccel_output_format"); - result.Add("cuda"); + result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); return result; } diff --git a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVp9Cuvid.cs b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVp9Cuvid.cs index 9adc2995f..da7a4ad1d 100644 --- a/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVp9Cuvid.cs +++ b/ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVp9Cuvid.cs @@ -2,16 +2,23 @@ public class DecoderVp9Cuvid : DecoderBase { + private readonly FFmpegState _ffmpegState; + + public DecoderVp9Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; + public override string Name => "vp9_cuvid"; - protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; + protected override FrameDataLocation OutputFrameDataLocation => + _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None + ? FrameDataLocation.Software + : FrameDataLocation.Hardware; public override IList InputOptions(InputFile inputFile) { IList result = base.InputOptions(inputFile); result.Add("-hwaccel_output_format"); - result.Add("cuda"); + result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); return result; } diff --git a/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs b/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs index 9db8e7d50..a9226266d 100644 --- a/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs +++ b/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs @@ -1,4 +1,5 @@ -using ErsatzTV.FFmpeg.Encoder.Nvenc; +using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.FFmpeg.Encoder.Nvenc; using ErsatzTV.FFmpeg.Encoder.Qsv; using ErsatzTV.FFmpeg.Encoder.Vaapi; using ErsatzTV.FFmpeg.Encoder.VideoToolbox; @@ -11,45 +12,54 @@ namespace ErsatzTV.FFmpeg.Encoder; public static class AvailableEncoders { public static Option ForVideoFormat( + IHardwareCapabilities hardwareCapabilities, FFmpegState ffmpegState, FrameState currentState, FrameState desiredState, Option maybeWatermarkInputFile, Option maybeSubtitleInputFile, ILogger logger) => - (ffmpegState.HardwareAccelerationMode, desiredState.VideoFormat) switch + (ffmpegState.EncoderHardwareAccelerationMode, desiredState.VideoFormat) switch { - (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc( - currentState, - maybeWatermarkInputFile, - maybeSubtitleInputFile), - (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc( - currentState, - maybeWatermarkInputFile, - maybeSubtitleInputFile), + (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(VideoFormat.Hevc) => + new EncoderHevcNvenc( + currentState, + maybeWatermarkInputFile, + maybeSubtitleInputFile), + (HardwareAccelerationMode.Nvenc, VideoFormat.H264) when hardwareCapabilities.CanEncode(VideoFormat.H264) => + new EncoderH264Nvenc( + currentState, + maybeWatermarkInputFile, + maybeSubtitleInputFile), - (HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv( - currentState, - maybeWatermarkInputFile, - maybeSubtitleInputFile), - (HardwareAccelerationMode.Qsv, VideoFormat.H264) => new EncoderH264Qsv( - currentState, - maybeWatermarkInputFile, - maybeSubtitleInputFile), + (HardwareAccelerationMode.Qsv, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(VideoFormat.Hevc) => + new EncoderHevcQsv( + currentState, + maybeWatermarkInputFile, + maybeSubtitleInputFile), + (HardwareAccelerationMode.Qsv, VideoFormat.H264) when hardwareCapabilities.CanEncode(VideoFormat.H264) => + new EncoderH264Qsv( + currentState, + maybeWatermarkInputFile, + maybeSubtitleInputFile), - (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi( - currentState, - maybeWatermarkInputFile, - maybeSubtitleInputFile), - (HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi( - currentState, - maybeWatermarkInputFile, - maybeSubtitleInputFile), + (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) when hardwareCapabilities.CanEncode(VideoFormat.Hevc) => + new EncoderHevcVaapi( + currentState, + maybeWatermarkInputFile, + maybeSubtitleInputFile), + (HardwareAccelerationMode.Vaapi, VideoFormat.H264) when hardwareCapabilities.CanEncode(VideoFormat.H264) => + new EncoderH264Vaapi( + currentState, + maybeWatermarkInputFile, + maybeSubtitleInputFile), - (HardwareAccelerationMode.VideoToolbox, VideoFormat.Hevc) => new EncoderHevcVideoToolbox(), - (HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) => new EncoderH264VideoToolbox(), + (HardwareAccelerationMode.VideoToolbox, VideoFormat.Hevc) when hardwareCapabilities.CanEncode( + VideoFormat.Hevc) => new EncoderHevcVideoToolbox(), + (HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) when hardwareCapabilities.CanEncode( + VideoFormat.H264) => new EncoderH264VideoToolbox(), - (_, VideoFormat.Hevc) => new EncoderLibx265(), + (_, VideoFormat.Hevc) => new EncoderLibx265(currentState), (_, VideoFormat.H264) => new EncoderLibx264(), (_, VideoFormat.Mpeg2Video) => new EncoderMpeg2Video(), diff --git a/ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs b/ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs index a92e4211c..957ab7455 100644 --- a/ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs +++ b/ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs @@ -1,9 +1,16 @@ -using ErsatzTV.FFmpeg.Format; +using ErsatzTV.FFmpeg.Filter; +using ErsatzTV.FFmpeg.Format; namespace ErsatzTV.FFmpeg.Encoder; public class EncoderLibx265 : EncoderBase { + private readonly FrameState _currentState; + + public EncoderLibx265(FrameState currentState) => _currentState = currentState; + + public override string Filter => new HardwareDownloadFilter(_currentState).Filter; + // TODO: is tag:v needed for mpegts? public override IList OutputOptions => new List { "-c:v", Name, "-tag:v", "hvc1", "-x265-params", "log-level=error" }; diff --git a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj index 6dada5313..8bf72ecf5 100644 --- a/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj +++ b/ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj @@ -9,6 +9,7 @@ + diff --git a/ErsatzTV.FFmpeg/FFmpegState.cs b/ErsatzTV.FFmpeg/FFmpegState.cs index ae534c907..9ab27a8f2 100644 --- a/ErsatzTV.FFmpeg/FFmpegState.cs +++ b/ErsatzTV.FFmpeg/FFmpegState.cs @@ -4,7 +4,8 @@ namespace ErsatzTV.FFmpeg; public record FFmpegState( bool SaveReport, - HardwareAccelerationMode HardwareAccelerationMode, + HardwareAccelerationMode DecoderHardwareAccelerationMode, + HardwareAccelerationMode EncoderHardwareAccelerationMode, Option VaapiDriver, Option VaapiDevice, Option Start, @@ -23,6 +24,7 @@ public record FFmpegState( new( saveReport, HardwareAccelerationMode.None, + HardwareAccelerationMode.None, Option.None, Option.None, Option.None, diff --git a/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs b/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs index 0d2564b6d..ce25fc514 100644 --- a/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs @@ -148,7 +148,7 @@ public class ComplexFilter : IPipelineStep } IPipelineFilterStep overlayFilter = AvailableWatermarkOverlayFilters.ForAcceleration( - _ffmpegState.HardwareAccelerationMode, + _ffmpegState.EncoderHardwareAccelerationMode, _currentState, watermarkInputFile.DesiredState, _resolution); @@ -164,16 +164,16 @@ public class ComplexFilter : IPipelineStep // also wait to upload if a subtitle overlay is coming string uploadDownloadFilter = string.Empty; if (_maybeSubtitleInputFile.IsNone && - (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi || - _ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && + (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi || + _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && _currentState.VideoFormat == VideoFormat.Hevc)) { uploadDownloadFilter = new HardwareUploadFilter(_ffmpegState).Filter; } if (_maybeSubtitleInputFile.Map(s => !s.IsImageBased).IfNone(false) && - _ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Vaapi && - _ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox) + _ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Vaapi && + _ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox) { uploadDownloadFilter = new HardwareDownloadFilter(_currentState).Filter; } @@ -217,7 +217,7 @@ public class ComplexFilter : IPipelineStep { IPipelineFilterStep overlayFilter = AvailableSubtitleOverlayFilters.ForAcceleration( - _ffmpegState.HardwareAccelerationMode, + _ffmpegState.EncoderHardwareAccelerationMode, _currentState); filter = overlayFilter.Filter; } @@ -238,8 +238,8 @@ public class ComplexFilter : IPipelineStep // vaapi uses software overlay and needs to upload // videotoolbox seems to require a hwupload for hevc string uploadFilter = string.Empty; - if (_ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi - || _ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && + if (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi + || _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && _currentState.VideoFormat == VideoFormat.Hevc) { uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter; diff --git a/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs b/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs index 41b4ac0de..70bd9e4aa 100644 --- a/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs @@ -6,7 +6,7 @@ public class HardwareUploadFilter : BaseFilter public HardwareUploadFilter(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; - public override string Filter => _ffmpegState.HardwareAccelerationMode switch + public override string Filter => _ffmpegState.EncoderHardwareAccelerationMode switch { HardwareAccelerationMode.None => string.Empty, HardwareAccelerationMode.Nvenc => "hwupload_cuda", @@ -15,7 +15,7 @@ public class HardwareUploadFilter : BaseFilter _ => "hwupload" }; - public override FrameState NextState(FrameState currentState) => _ffmpegState.HardwareAccelerationMode switch + public override FrameState NextState(FrameState currentState) => _ffmpegState.EncoderHardwareAccelerationMode switch { HardwareAccelerationMode.None => currentState, _ => currentState with { FrameDataLocation = FrameDataLocation.Hardware } diff --git a/ErsatzTV.FFmpeg/Filter/SubtitleHardwareUploadFilter.cs b/ErsatzTV.FFmpeg/Filter/SubtitleHardwareUploadFilter.cs index 30ac95f76..154af760e 100644 --- a/ErsatzTV.FFmpeg/Filter/SubtitleHardwareUploadFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/SubtitleHardwareUploadFilter.cs @@ -12,7 +12,7 @@ public class SubtitleHardwareUploadFilter : BaseFilter } public override string Filter => - _ffmpegState.HardwareAccelerationMode switch + _ffmpegState.EncoderHardwareAccelerationMode switch { HardwareAccelerationMode.None => string.Empty, HardwareAccelerationMode.Nvenc => "hwupload_cuda", diff --git a/ErsatzTV.FFmpeg/Filter/SubtitlePixelFormatFilter.cs b/ErsatzTV.FFmpeg/Filter/SubtitlePixelFormatFilter.cs index 40faaeae6..458378a5e 100644 --- a/ErsatzTV.FFmpeg/Filter/SubtitlePixelFormatFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/SubtitlePixelFormatFilter.cs @@ -10,7 +10,7 @@ public class SubtitlePixelFormatFilter : BaseFilter { get { - Option maybeFormat = _ffmpegState.HardwareAccelerationMode switch + Option maybeFormat = _ffmpegState.EncoderHardwareAccelerationMode switch { HardwareAccelerationMode.Nvenc => "yuva420p", HardwareAccelerationMode.Qsv => "yuva420p", diff --git a/ErsatzTV.FFmpeg/Filter/WatermarkHardwareUploadFilter.cs b/ErsatzTV.FFmpeg/Filter/WatermarkHardwareUploadFilter.cs index 01c75bc0e..51ce458cb 100644 --- a/ErsatzTV.FFmpeg/Filter/WatermarkHardwareUploadFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/WatermarkHardwareUploadFilter.cs @@ -11,7 +11,7 @@ public class WatermarkHardwareUploadFilter : BaseFilter _ffmpegState = ffmpegState; } - public override string Filter => _ffmpegState.HardwareAccelerationMode switch + public override string Filter => _ffmpegState.EncoderHardwareAccelerationMode switch { HardwareAccelerationMode.None => string.Empty, HardwareAccelerationMode.Nvenc => "hwupload_cuda", diff --git a/ErsatzTV.FFmpeg/Filter/WatermarkPixelFormatFilter.cs b/ErsatzTV.FFmpeg/Filter/WatermarkPixelFormatFilter.cs index 261b433fa..93cd2de28 100644 --- a/ErsatzTV.FFmpeg/Filter/WatermarkPixelFormatFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/WatermarkPixelFormatFilter.cs @@ -19,7 +19,7 @@ public class WatermarkPixelFormatFilter : BaseFilter { bool hasFadePoints = _watermarkState.MaybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0; - Option maybeFormat = _ffmpegState.HardwareAccelerationMode switch + Option maybeFormat = _ffmpegState.EncoderHardwareAccelerationMode switch { HardwareAccelerationMode.Nvenc => "yuva420p", HardwareAccelerationMode.Qsv => "yuva420p", diff --git a/ErsatzTV.FFmpeg/PipelineBuilder.cs b/ErsatzTV.FFmpeg/PipelineBuilder.cs index 2452a51e6..8045f2703 100644 --- a/ErsatzTV.FFmpeg/PipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/PipelineBuilder.cs @@ -1,4 +1,5 @@ -using ErsatzTV.FFmpeg.Decoder; +using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.FFmpeg.Decoder; using ErsatzTV.FFmpeg.Encoder; using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Filter; @@ -18,6 +19,7 @@ public class PipelineBuilder { private readonly Option _audioInputFile; private readonly string _fontsFolder; + private readonly IHardwareCapabilities _hardwareCapabilities; private readonly ILogger _logger; private readonly List _pipelineSteps; private readonly string _reportsFolder; @@ -26,6 +28,7 @@ public class PipelineBuilder private readonly Option _watermarkInputFile; public PipelineBuilder( + IHardwareCapabilities hardwareCapabilities, Option videoInputFile, Option audioInputFile, Option watermarkInputFile, @@ -46,6 +49,7 @@ public class PipelineBuilder new ClosedGopOutputOption() }; + _hardwareCapabilities = hardwareCapabilities; _videoInputFile = videoInputFile; _audioInputFile = audioInputFile; _watermarkInputFile = watermarkInputFile; @@ -200,7 +204,7 @@ public class PipelineBuilder else { Option maybeAccel = AvailableHardwareAccelerationOptions.ForMode( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, ffmpegState.VaapiDevice, _logger); @@ -209,24 +213,45 @@ public class PipelineBuilder ffmpegState = ffmpegState with { // disable hw accel if we don't match anything - HardwareAccelerationMode = HardwareAccelerationMode.None + DecoderHardwareAccelerationMode = HardwareAccelerationMode.None, + EncoderHardwareAccelerationMode = HardwareAccelerationMode.None }; } foreach (IPipelineStep accel in maybeAccel) { - currentState = accel.NextState(currentState); - _pipelineSteps.Add(accel); + bool canDecode = _hardwareCapabilities.CanDecode(currentState.VideoFormat); + bool canEncode = _hardwareCapabilities.CanEncode(desiredState.VideoFormat); + + // disable hw accel if decoder/encoder isn't supported + if (!canDecode || !canEncode) + { + ffmpegState = ffmpegState with + { + DecoderHardwareAccelerationMode = canDecode + ? ffmpegState.DecoderHardwareAccelerationMode + : HardwareAccelerationMode.None, + EncoderHardwareAccelerationMode = canEncode + ? ffmpegState.EncoderHardwareAccelerationMode + : HardwareAccelerationMode.None + }; + } + + if (canDecode || canEncode) + { + currentState = accel.NextState(currentState); + _pipelineSteps.Add(accel); + } } // nvenc requires yuv420p background with yuva420p overlay - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc && hasOverlay) + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Nvenc && hasOverlay) { desiredState = desiredState with { PixelFormat = new PixelFormatYuv420P() }; } // qsv should stay nv12 - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Qsv && hasOverlay) + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv && hasOverlay) { IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone(new PixelFormatYuv420P()); desiredState = desiredState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; @@ -240,6 +265,7 @@ public class PipelineBuilder } foreach (IDecoder decoder in AvailableDecoders.ForVideoFormat( + _hardwareCapabilities, ffmpegState, currentState, desiredState, @@ -262,7 +288,7 @@ public class PipelineBuilder if (videoStream.StillImage) { - var option = new InfiniteLoopInputOption(ffmpegState.HardwareAccelerationMode); + var option = new InfiniteLoopInputOption(ffmpegState.EncoderHardwareAccelerationMode); _videoInputFile.Iter(f => f.AddOption(option)); } @@ -277,7 +303,7 @@ public class PipelineBuilder if (desiredState.InfiniteLoop) { - var option = new InfiniteLoopInputOption(ffmpegState.HardwareAccelerationMode); + var option = new InfiniteLoopInputOption(ffmpegState.EncoderHardwareAccelerationMode); _audioInputFile.Iter(f => f.AddOption(option)); _videoInputFile.Iter(f => f.AddOption(option)); } @@ -325,7 +351,7 @@ public class PipelineBuilder if (desiredState.Deinterlaced && !currentState.Deinterlaced) { IPipelineFilterStep step = AvailableDeinterlaceFilters.ForAcceleration( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, currentState, desiredState, _watermarkInputFile, @@ -335,7 +361,7 @@ public class PipelineBuilder } // TODO: this is a software-only flow, will need to be different for hardware accel - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.None) + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) { if (currentState.ScaledSize != desiredState.ScaledSize || currentState.PaddedSize != desiredState.PaddedSize) @@ -360,7 +386,7 @@ public class PipelineBuilder else if (currentState.ScaledSize != desiredState.ScaledSize) { IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); @@ -382,7 +408,7 @@ public class PipelineBuilder else if (currentState.PaddedSize != desiredState.PaddedSize) { IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); @@ -415,7 +441,7 @@ public class PipelineBuilder currentState = formatFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(formatFilter)); - switch (ffmpegState.HardwareAccelerationMode) + switch (ffmpegState.EncoderHardwareAccelerationMode) { case HardwareAccelerationMode.Nvenc: var uploadFilter = new HardwareUploadFilter(ffmpegState); @@ -426,13 +452,13 @@ public class PipelineBuilder } else { - if (ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Qsv) + if (ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Qsv) { // the filter re-applies the current pixel format, so we have to set it first currentState = currentState with { PixelFormat = desiredState.PixelFormat }; IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); @@ -444,7 +470,7 @@ public class PipelineBuilder } // nvenc custom logic - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc) + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Nvenc) { foreach (VideoInputFile videoInputFile in _videoInputFile) { @@ -461,7 +487,7 @@ public class PipelineBuilder currentState = currentState with { PixelFormat = desiredState.PixelFormat }; IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); @@ -488,7 +514,7 @@ public class PipelineBuilder if (currentState.PixelFormat.Map(pf => pf.FFmpegName) != desiredPixelFormat.FFmpegName) { // qsv doesn't seem to like this - if (ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Qsv) + if (ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Qsv) { IPipelineStep step = new PixelFormatOutputOption(desiredPixelFormat); currentState = step.NextState(currentState); @@ -547,7 +573,7 @@ public class PipelineBuilder { // vaapi and videotoolbox use a software overlay, so we need to ensure the background is already in software // though videotoolbox uses software decoders, so no need to download for that - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi) + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi) { var downloadFilter = new HardwareDownloadFilter(currentState); currentState = downloadFilter.NextState(currentState); @@ -565,12 +591,12 @@ public class PipelineBuilder // text-based subtitles are always added in software, so always try to download the background // nvidia needs some extra format help if the only filter will be the download filter - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc && + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Nvenc && currentState.FrameDataLocation == FrameDataLocation.Hardware && _videoInputFile.Map(f => f.FilterSteps.Count).IfNone(1) == 0) { IPipelineFilterStep scaleFilter = AvailableScaleFilters.ForAcceleration( - ffmpegState.HardwareAccelerationMode, + ffmpegState.EncoderHardwareAccelerationMode, currentState, desiredState.ScaledSize, desiredState.PaddedSize); @@ -588,7 +614,7 @@ public class PipelineBuilder { // vaapi and videotoolbox use a software overlay, so we need to ensure the background is already in software // though videotoolbox uses software decoders, so no need to download for that - if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Vaapi) + if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi) { var downloadFilter = new HardwareDownloadFilter(currentState); currentState = downloadFilter.NextState(currentState); @@ -607,7 +633,8 @@ public class PipelineBuilder else if (watermarkInputFile.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)) { // looping is required to fade a static image in and out - watermarkInputFile.AddOption(new InfiniteLoopInputOption(ffmpegState.HardwareAccelerationMode)); + watermarkInputFile.AddOption( + new InfiniteLoopInputOption(ffmpegState.EncoderHardwareAccelerationMode)); } } @@ -634,6 +661,7 @@ public class PipelineBuilder if (_pipelineSteps.OfType().All(e => e.Kind != StreamKind.Video)) { foreach (IEncoder e in AvailableEncoders.ForVideoFormat( + _hardwareCapabilities, ffmpegState, currentState, desiredState, diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index b8a0fe78b..ea1bd4a7e 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -14,12 +14,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index bc10889d4..35d04420d 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -55,16 +55,16 @@ - - + + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 90589b483..b4e1cb30a 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -32,6 +32,7 @@ using ErsatzTV.Core.Metadata.Nfo; using ErsatzTV.Core.Plex; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Trakt; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.Formatters; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; @@ -396,6 +397,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped();