From 19a7f90d526cad4d4f6fbc23369f2b708470c89b Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Tue, 1 Mar 2022 14:03:36 -0600 Subject: [PATCH] ffmpeg lib hardware accel fixes (#663) * more scaling fixes * qsv fixes * cleanup * nvidia fixes * vaapi fixes * test nvidia by default --- CHANGELOG.md | 1 + .../FFmpeg/TranscodingTests.cs | 34 ++++++-- ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs | 13 ++- ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs | 5 +- ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs | 16 +++- .../Encoder/Nvenc/EncoderH264Nvenc.cs | 25 ++++++ .../Encoder/Nvenc/EncoderHevcNvenc.cs | 25 ++++++ .../Encoder/Vaapi/EncoderH264Vaapi.cs | 23 ++++-- .../Encoder/Vaapi/EncoderHevcVaapi.cs | 25 ++++-- .../Filter/AvailableDeinterlaceFilters.cs | 17 +++- .../Filter/AvailableScaleFilters.cs | 4 +- .../Filter/HardwareUploadFilter.cs | 6 +- ErsatzTV.FFmpeg/Filter/ScaleFilter.cs | 8 +- .../Filter/Vaapi/ScaleVaapiFilter.cs | 8 +- .../VaapiHardwareAccelerationOption.cs | 2 +- ErsatzTV.FFmpeg/PipelineBuilder.cs | 82 ++++++++++++++++--- 16 files changed, 247 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0510fed1..f9ba6a37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix song sorting with `Chronological` and `Shuffle In Order` playback orders - Fix watermark on scaled and/or padded video with NVIDIA acceleration - Fix playback of interlaced mpeg2video content with NVIDIA acceleration +- Fix playback of all interlaced content with QSV acceleration ### Changed - Framerate normalization will never normalize framerate below 24fps diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index 3edbff92f..d50095749 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -19,6 +19,7 @@ using LanguageExt; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; +using Serilog; using static LanguageExt.Prelude; namespace ErsatzTV.Core.Tests.FFmpeg @@ -27,6 +28,18 @@ namespace ErsatzTV.Core.Tests.FFmpeg [Explicit] public class TranscodingTests { + private static readonly ILoggerFactory LoggerFactory; + + static TranscodingTests() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + } + [Test] [Explicit] public void DeleteTestVideos() @@ -216,7 +229,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg { StartInfo = new ProcessStartInfo { - FileName = "ffmpeg", + FileName = ExecutableName("ffmpeg"), Arguments = args } }; @@ -237,19 +250,19 @@ namespace ErsatzTV.Core.Tests.FFmpeg It.Is(x => x == ArtworkKind.Watermark), It.IsAny>())) .Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png")); - + var oldService = new FFmpegProcessService( new FFmpegPlaybackSettingsCalculator(), new FakeStreamSelector(), imageCache.Object, new Mock().Object, - new Mock>().Object); + LoggerFactory.CreateLogger()); var service = new FFmpegLibraryProcessService( oldService, new FFmpegPlaybackSettingsCalculator(), new FakeStreamSelector(), - new Mock>().Object); + LoggerFactory.CreateLogger()); var v = new MediaVersion { @@ -270,11 +283,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg var localStatisticsProvider = new LocalStatisticsProvider( metadataRepository.Object, - new LocalFileSystem(new Mock>().Object), - new Mock>().Object); + new LocalFileSystem(LoggerFactory.CreateLogger()), + LoggerFactory.CreateLogger()); await localStatisticsProvider.RefreshStatistics( - "ffprobe", + ExecutableName("ffprobe"), new Movie { MediaVersions = new List @@ -337,7 +350,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg } Process process = await service.ForPlayoutItem( - "ffmpeg", + ExecutableName("ffmpeg"), false, new Channel(Guid.NewGuid()) { @@ -442,7 +455,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg } using var sha = SHA256.Create(); - byte[] textData = System.Text.Encoding.UTF8.GetBytes(text); + byte[] textData = Encoding.UTF8.GetBytes(text); byte[] hash = sha.ComputeHash(textData); return BitConverter.ToString(hash).Replace("-", string.Empty); } @@ -455,5 +468,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg public Task> SelectAudioStream(Channel channel, MediaVersion version) => Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); } + + private static string ExecutableName(string baseName) => + OperatingSystem.IsWindows() ? $"{baseName}.exe" : baseName; } } diff --git a/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs b/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs index 6fae32562..d6aa397bf 100644 --- a/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs +++ b/ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs @@ -12,6 +12,7 @@ public static class AvailableDecoders FFmpegState ffmpegState, FrameState currentState, FrameState desiredState, + Option watermarkInputFile, ILogger logger) { return (ffmpegState.HardwareAccelerationMode, currentState.VideoFormat, @@ -41,14 +42,22 @@ public static class AvailableDecoders (HardwareAccelerationMode.Qsv, VideoFormat.H264, PixelFormat.YUV420P10LE or PixelFormat.YUV444P10LE) => new DecoderH264(), + // qsv uses software deinterlace filter, so decode in software + (HardwareAccelerationMode.Qsv, VideoFormat.H264, _) when desiredState.Deinterlaced => new DecoderH264(), + (HardwareAccelerationMode.Qsv, VideoFormat.Mpeg2Video, _) when desiredState.Deinterlaced => + new DecoderMpeg2Video(), + (HardwareAccelerationMode.Qsv, VideoFormat.Hevc, _) => new DecoderHevcQsv(), (HardwareAccelerationMode.Qsv, VideoFormat.H264, _) => new DecoderH264Qsv(), (HardwareAccelerationMode.Qsv, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Qsv(), (HardwareAccelerationMode.Qsv, VideoFormat.Vc1, _) => new DecoderVc1Qsv(), (HardwareAccelerationMode.Qsv, VideoFormat.Vp9, _) => new DecoderVp9Qsv(), - // vaapi should use implicit decoders - (HardwareAccelerationMode.Vaapi, _, _) => new DecoderVaapi(), + // vaapi should use implicit decoders when scaling or no watermark + // otherwise, fall back to software decoders + (HardwareAccelerationMode.Vaapi, _, _) when watermarkInputFile.IsNone || + (currentState.ScaledSize != desiredState.ScaledSize) => + new DecoderVaapi(), // videotoolbox should use implicit decoders (HardwareAccelerationMode.VideoToolbox, _, _) => new DecoderVideoToolbox(), diff --git a/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs b/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs index b59f31046..2d22fcd65 100644 --- a/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs +++ b/ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs @@ -5,8 +5,11 @@ namespace ErsatzTV.FFmpeg.Decoder; public class DecoderVaapi : DecoderBase { protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; + public override string Name => "implicit_vaapi"; - public override IList InputOptions(InputFile inputFile) => Array.Empty(); + + public override IList InputOptions(InputFile inputFile) => + new List { "-hwaccel_output_format", "vaapi" }; public override FrameState NextState(FrameState currentState) { diff --git a/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs b/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs index 2b0f5893b..65e857994 100644 --- a/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs +++ b/ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs @@ -19,16 +19,24 @@ public static class AvailableEncoders ILogger logger) => (ffmpegState.HardwareAccelerationMode, desiredState.VideoFormat) switch { - (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc(), - (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc(), + (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc( + currentState, + maybeWatermarkInputFile), + (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc( + currentState, + maybeWatermarkInputFile), (HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv( currentState, maybeWatermarkInputFile), (HardwareAccelerationMode.Qsv, VideoFormat.H264) => new EncoderH264Qsv(currentState), - (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(currentState), - (HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(currentState), + (HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi( + currentState, + maybeWatermarkInputFile), + (HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi( + currentState, + maybeWatermarkInputFile), (HardwareAccelerationMode.VideoToolbox, VideoFormat.Hevc) => new EncoderHevcVideoToolbox(), (HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) => new EncoderH264VideoToolbox(), diff --git a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs index 5d5550e2b..be9d06dcd 100644 --- a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs +++ b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs @@ -1,9 +1,19 @@ using ErsatzTV.FFmpeg.Format; +using LanguageExt; namespace ErsatzTV.FFmpeg.Encoder.Nvenc; public class EncoderH264Nvenc : EncoderBase { + private readonly FrameState _currentState; + private readonly Option _maybeWatermarkInputFile; + + public EncoderH264Nvenc(FrameState currentState, Option maybeWatermarkInputFile) + { + _currentState = currentState; + _maybeWatermarkInputFile = maybeWatermarkInputFile; + } + public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.H264, @@ -12,4 +22,19 @@ public class EncoderH264Nvenc : EncoderBase public override string Name => "h264_nvenc"; public override StreamKind Kind => StreamKind.Video; + + // need to upload if we're still in software and a watermark is used + public override string Filter + { + get + { + // only upload to hw if we need to overlay a watermark + if (_maybeWatermarkInputFile.IsSome && _currentState.FrameDataLocation == FrameDataLocation.Software) + { + return "hwupload_cuda"; + } + + return string.Empty; + } + } } diff --git a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs index a116c87bd..cfc736c66 100644 --- a/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs +++ b/ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs @@ -1,9 +1,19 @@ using ErsatzTV.FFmpeg.Format; +using LanguageExt; namespace ErsatzTV.FFmpeg.Encoder.Nvenc; public class EncoderHevcNvenc : EncoderBase { + private readonly FrameState _currentState; + private readonly Option _maybeWatermarkInputFile; + + public EncoderHevcNvenc(FrameState currentState, Option maybeWatermarkInputFile) + { + _currentState = currentState; + _maybeWatermarkInputFile = maybeWatermarkInputFile; + } + public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.Hevc, @@ -12,4 +22,19 @@ public class EncoderHevcNvenc : EncoderBase public override string Name => "hevc_nvenc"; public override StreamKind Kind => StreamKind.Video; + + // need to upload if we're still in software and a watermark is used + public override string Filter + { + get + { + // only upload to hw if we need to overlay a watermark + if (_maybeWatermarkInputFile.IsSome && _currentState.FrameDataLocation == FrameDataLocation.Software) + { + return "hwupload_cuda"; + } + + return string.Empty; + } + } } diff --git a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs index 1d9707ebf..1b5859c8d 100644 --- a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs +++ b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs @@ -1,26 +1,39 @@ using ErsatzTV.FFmpeg.Format; +using LanguageExt; namespace ErsatzTV.FFmpeg.Encoder.Vaapi; public class EncoderH264Vaapi : EncoderBase { private readonly FrameState _currentState; + private readonly Option _maybeWatermarkInputFile; - public EncoderH264Vaapi(FrameState currentState) + public EncoderH264Vaapi(FrameState currentState, Option maybeWatermarkInputFile) { _currentState = currentState; + _maybeWatermarkInputFile = maybeWatermarkInputFile; } public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.H264, - FrameDataLocation = FrameDataLocation.Hardware + // don't change the frame data location }; public override string Name => "h264_vaapi"; public override StreamKind Kind => StreamKind.Video; - public override string Filter => _currentState.FrameDataLocation == FrameDataLocation.Software - ? "format=nv12|vaapi,hwupload" - : string.Empty; + // need to upload if we're still in software unless a watermark is used + public override string Filter + { + get + { + if (_maybeWatermarkInputFile.IsNone && _currentState.FrameDataLocation == FrameDataLocation.Software) + { + return "format=nv12|vaapi,hwupload"; + } + + return string.Empty; + } + } } diff --git a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs index 2a82ac37a..c980acdf2 100644 --- a/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs +++ b/ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs @@ -1,26 +1,39 @@ using ErsatzTV.FFmpeg.Format; namespace ErsatzTV.FFmpeg.Encoder.Vaapi; +using LanguageExt; public class EncoderHevcVaapi : EncoderBase { private readonly FrameState _currentState; + private readonly Option _maybeWatermarkInputFile; - public EncoderHevcVaapi(FrameState currentState) + public EncoderHevcVaapi(FrameState currentState, Option maybeWatermarkInputFile) { _currentState = currentState; + _maybeWatermarkInputFile = maybeWatermarkInputFile; } public override FrameState NextState(FrameState currentState) => currentState with { - VideoFormat = VideoFormat.Hevc, - FrameDataLocation = FrameDataLocation.Hardware + VideoFormat = VideoFormat.Hevc + // don't change the frame data location }; public override string Name => "hevc_vaapi"; public override StreamKind Kind => StreamKind.Video; - public override string Filter => _currentState.FrameDataLocation == FrameDataLocation.Software - ? "format=nv12|vaapi,hwupload" - : string.Empty; + // need to upload if we're still in software unless a watermark is used + public override string Filter + { + get + { + if (_maybeWatermarkInputFile.IsNone && _currentState.FrameDataLocation == FrameDataLocation.Software) + { + return "format=nv12|vaapi,hwupload"; + } + + return string.Empty; + } + } } diff --git a/ErsatzTV.FFmpeg/Filter/AvailableDeinterlaceFilters.cs b/ErsatzTV.FFmpeg/Filter/AvailableDeinterlaceFilters.cs index 8613ad6d8..f5a07571e 100644 --- a/ErsatzTV.FFmpeg/Filter/AvailableDeinterlaceFilters.cs +++ b/ErsatzTV.FFmpeg/Filter/AvailableDeinterlaceFilters.cs @@ -1,6 +1,6 @@ using ErsatzTV.FFmpeg.Filter.Cuda; -using ErsatzTV.FFmpeg.Filter.Qsv; using ErsatzTV.FFmpeg.Filter.Vaapi; +using LanguageExt; namespace ErsatzTV.FFmpeg.Filter; @@ -8,12 +8,21 @@ public static class AvailableDeinterlaceFilters { public static IPipelineFilterStep ForAcceleration( HardwareAccelerationMode accelMode, - FrameState currentState) => + FrameState currentState, + FrameState desiredState, + Option watermarkInputFile) => accelMode switch { HardwareAccelerationMode.Nvenc => new YadifCudaFilter(currentState), - HardwareAccelerationMode.Qsv => new DeinterlaceQsvFilter(currentState), - HardwareAccelerationMode.Vaapi => new DeinterlaceVaapiFilter(currentState), + + // deinterlace_qsv seems to create timestamp issues + // HardwareAccelerationMode.Qsv => new DeinterlaceQsvFilter(currentState), + + // fall back to software deinterlace with watermark and no scaling + HardwareAccelerationMode.Vaapi when watermarkInputFile.IsNone || + (currentState.ScaledSize != desiredState.ScaledSize) => + new DeinterlaceVaapiFilter(currentState), + _ => new YadifFilter(currentState) }; } diff --git a/ErsatzTV.FFmpeg/Filter/AvailableScaleFilters.cs b/ErsatzTV.FFmpeg/Filter/AvailableScaleFilters.cs index 06e7486ba..fdfc1255a 100644 --- a/ErsatzTV.FFmpeg/Filter/AvailableScaleFilters.cs +++ b/ErsatzTV.FFmpeg/Filter/AvailableScaleFilters.cs @@ -14,7 +14,9 @@ public static class AvailableScaleFilters accelMode switch { HardwareAccelerationMode.Nvenc => new ScaleCudaFilter(currentState, scaledSize, paddedSize), - HardwareAccelerationMode.Qsv => new ScaleQsvFilter(currentState, scaledSize), + HardwareAccelerationMode.Qsv when currentState.FrameDataLocation == FrameDataLocation.Hardware || + scaledSize == paddedSize => + new ScaleQsvFilter(currentState, scaledSize), HardwareAccelerationMode.Vaapi => new ScaleVaapiFilter(currentState, scaledSize, paddedSize), _ => new ScaleFilter(currentState, scaledSize, paddedSize) }; diff --git a/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs b/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs index 9338a6ceb..01ed144ae 100644 --- a/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/HardwareUploadFilter.cs @@ -9,7 +9,11 @@ public class HardwareUploadFilter : BaseFilter _ffmpegState = ffmpegState; } - public override FrameState NextState(FrameState currentState) => currentState; + public override FrameState NextState(FrameState currentState) => _ffmpegState.HardwareAccelerationMode switch + { + HardwareAccelerationMode.None => currentState, + _ => currentState with { FrameDataLocation = FrameDataLocation.Hardware } + }; public override string Filter => _ffmpegState.HardwareAccelerationMode switch { diff --git a/ErsatzTV.FFmpeg/Filter/ScaleFilter.cs b/ErsatzTV.FFmpeg/Filter/ScaleFilter.cs index 63d51dc66..2d507dcf7 100644 --- a/ErsatzTV.FFmpeg/Filter/ScaleFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/ScaleFilter.cs @@ -19,8 +19,14 @@ public class ScaleFilter : BaseFilter { get { + string aspectRatio = string.Empty; + if (_scaledSize != _paddedSize) + { + aspectRatio = ":force_original_aspect_ratio=decrease"; + } + string scale = - $"scale={_paddedSize.Width}:{_paddedSize.Height}:flags=fast_bilinear:force_original_aspect_ratio=decrease"; + $"scale={_paddedSize.Width}:{_paddedSize.Height}:flags=fast_bilinear{aspectRatio}"; string hwdownload = string.Empty; if (_currentState.FrameDataLocation == FrameDataLocation.Hardware) diff --git a/ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs b/ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs index 3487fd371..12529e5d4 100644 --- a/ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs @@ -37,8 +37,14 @@ public class ScaleVaapiFilter : BaseFilter format = $":format={pixelFormat.FFmpegName}"; } + string aspectRatio = string.Empty; + if (_scaledSize != _paddedSize) + { + aspectRatio = ":force_original_aspect_ratio=1"; + } + string targetSize = $"{_paddedSize.Width}:{_paddedSize.Height}"; - scale = $"scale_vaapi={targetSize}:force_original_aspect_ratio=1:force_divisible_by=2{format}"; + scale = $"scale_vaapi={targetSize}{aspectRatio}:force_divisible_by=2{format}"; } if (_currentState.FrameDataLocation == FrameDataLocation.Hardware) diff --git a/ErsatzTV.FFmpeg/Option/HardwareAcceleration/VaapiHardwareAccelerationOption.cs b/ErsatzTV.FFmpeg/Option/HardwareAcceleration/VaapiHardwareAccelerationOption.cs index cd4eec585..3bcb2a5cf 100644 --- a/ErsatzTV.FFmpeg/Option/HardwareAcceleration/VaapiHardwareAccelerationOption.cs +++ b/ErsatzTV.FFmpeg/Option/HardwareAcceleration/VaapiHardwareAccelerationOption.cs @@ -10,7 +10,7 @@ public class VaapiHardwareAccelerationOption : GlobalOption } public override IList GlobalOptions => new List - { "-hwaccel", "vaapi", "-vaapi_device", _vaapiDevice, "-hwaccel_output_format", "vaapi" }; + { "-hwaccel", "vaapi", "-vaapi_device", _vaapiDevice }; public override FrameState NextState(FrameState currentState) => currentState with { diff --git a/ErsatzTV.FFmpeg/PipelineBuilder.cs b/ErsatzTV.FFmpeg/PipelineBuilder.cs index a86acf19a..0909ead35 100644 --- a/ErsatzTV.FFmpeg/PipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/PipelineBuilder.cs @@ -2,6 +2,7 @@ using ErsatzTV.FFmpeg.Encoder; using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Filter; +using ErsatzTV.FFmpeg.Filter.Cuda; using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.Option; using ErsatzTV.FFmpeg.Option.HardwareAcceleration; @@ -173,6 +174,14 @@ public class PipelineBuilder desiredState = desiredState with { PixelFormat = new PixelFormatYuv420P() }; } + // qsv should stay nv12 + if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Qsv && + _watermarkInputFile.IsSome) + { + IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone(new PixelFormatYuv420P()); + desiredState = desiredState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; + } + foreach (string desiredVaapiDriver in ffmpegState.VaapiDriver) { IPipelineStep step = new LibvaDriverNameVariable(desiredVaapiDriver); @@ -180,7 +189,12 @@ public class PipelineBuilder _pipelineSteps.Add(step); } - foreach (IDecoder decoder in AvailableDecoders.ForVideoFormat(ffmpegState, currentState, desiredState, _logger)) + foreach (IDecoder decoder in AvailableDecoders.ForVideoFormat( + ffmpegState, + currentState, + desiredState, + _watermarkInputFile, + _logger)) { foreach (VideoInputFile videoInputFile in _videoInputFile) { @@ -256,7 +270,9 @@ public class PipelineBuilder { IPipelineFilterStep step = AvailableDeinterlaceFilters.ForAcceleration( ffmpegState.HardwareAccelerationMode, - currentState); + currentState, + desiredState, + _watermarkInputFile); currentState = step.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(step)); } @@ -327,8 +343,9 @@ public class PipelineBuilder currentState = sarStep.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(sarStep)); } - - if (_watermarkInputFile.IsSome && currentState.PixelFormat != desiredState.PixelFormat) + + if (_watermarkInputFile.IsSome && currentState.PixelFormat.Map(pf => pf.FFmpegName) != + desiredState.PixelFormat.Map(pf => pf.FFmpegName)) { // this should only happen with nvenc? // use scale filter to fix pixel format @@ -341,20 +358,59 @@ public class PipelineBuilder currentState = formatFilter.NextState(currentState); _videoInputFile.Iter(f => f.FilterSteps.Add(formatFilter)); - _videoInputFile.Iter(f => f.FilterSteps.Add(new HardwareUploadFilter(ffmpegState))); + switch (ffmpegState.HardwareAccelerationMode) + { + case HardwareAccelerationMode.Nvenc: + var uploadFilter = new HardwareUploadFilter(ffmpegState); + currentState = uploadFilter.NextState(currentState); + _videoInputFile.Iter(f => f.FilterSteps.Add(uploadFilter)); + break; + } } else + { + if (ffmpegState.HardwareAccelerationMode != 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, + currentState, + desiredState.ScaledSize, + desiredState.PaddedSize); + currentState = scaleFilter.NextState(currentState); + _videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter)); + } + } + } + } + + // nvenc custom logic + if (ffmpegState.HardwareAccelerationMode == HardwareAccelerationMode.Nvenc) + { + foreach (VideoInputFile videoInputFile in _videoInputFile) + { + // if we only deinterlace, we need to set pixel format again (using scale_cuda) + bool onlyYadif = videoInputFile.FilterSteps.Count == 1 && + videoInputFile.FilterSteps.Any(fs => fs is YadifCudaFilter); + + // if we have no filters and a watermark, we need to set pixel format + bool unfilteredWithWatermark = videoInputFile.FilterSteps.Count == 0 + && _watermarkInputFile.IsSome; + + if (onlyYadif || unfilteredWithWatermark) { // 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, currentState, desiredState.ScaledSize, desiredState.PaddedSize); currentState = scaleFilter.NextState(currentState); - _videoInputFile.Iter(f => f.FilterSteps.Add(scaleFilter)); + videoInputFile.FilterSteps.Add(scaleFilter); } } } @@ -370,14 +426,18 @@ public class PipelineBuilder _pipelineSteps.Add(step); } } - + foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) { if (currentState.PixelFormat.Map(pf => pf.FFmpegName) != desiredPixelFormat.FFmpegName) { - IPipelineStep step = new PixelFormatOutputOption(desiredPixelFormat); - currentState = step.NextState(currentState); - _pipelineSteps.Add(step); + // qsv doesn't seem to like this + if (ffmpegState.HardwareAccelerationMode != HardwareAccelerationMode.Qsv) + { + IPipelineStep step = new PixelFormatOutputOption(desiredPixelFormat); + currentState = step.NextState(currentState); + _pipelineSteps.Add(step); + } } }