From 09858df65463e9ada8281dd83cc2c4aa3caa17b8 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:11:32 -0600 Subject: [PATCH] fix case when cuda hw decode falls back to sw (#2718) * fix case when cuda hw decode falls back to sw * use a new filter --- CHANGELOG.md | 1 + .../Cuda/CudaSoftwareFallbackUploadFilter.cs | 9 +++++++ .../Filter/Cuda/HardwareUploadCudaFilter.cs | 13 ++++++++++ .../Filter/HardwareUploadCudaFilter.cs | 17 ------------- .../CudaHardwareAccelerationOption.cs | 2 +- .../Pipeline/NvidiaPipelineBuilder.cs | 24 ++++++++++++------- 6 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 ErsatzTV.FFmpeg/Filter/Cuda/CudaSoftwareFallbackUploadFilter.cs create mode 100644 ErsatzTV.FFmpeg/Filter/Cuda/HardwareUploadCudaFilter.cs delete mode 100644 ErsatzTV.FFmpeg/Filter/HardwareUploadCudaFilter.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7b0778a..ad56c11de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Automatically kill playback troubleshooting ffmpeg process if it hasn't completed after two minutes - Fix playback of certain BT.2020 content - Use playlist item count when using a playlist as filler (instead of a fixed count of 1 for each playlist item) +- NVIDIA: fix stream failure with certain content that should decode in hardware but falls back to software ### Changed - No longer round framerate to nearest integer when normalizing framerate diff --git a/ErsatzTV.FFmpeg/Filter/Cuda/CudaSoftwareFallbackUploadFilter.cs b/ErsatzTV.FFmpeg/Filter/Cuda/CudaSoftwareFallbackUploadFilter.cs new file mode 100644 index 000000000..69bf8fea9 --- /dev/null +++ b/ErsatzTV.FFmpeg/Filter/Cuda/CudaSoftwareFallbackUploadFilter.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.FFmpeg.Filter.Cuda; + +public class CudaSoftwareFallbackUploadFilter : BaseFilter +{ + public override string Filter => "hwupload"; + + public override FrameState NextState(FrameState currentState) => + currentState with { FrameDataLocation = FrameDataLocation.Hardware }; +} diff --git a/ErsatzTV.FFmpeg/Filter/Cuda/HardwareUploadCudaFilter.cs b/ErsatzTV.FFmpeg/Filter/Cuda/HardwareUploadCudaFilter.cs new file mode 100644 index 000000000..4921de270 --- /dev/null +++ b/ErsatzTV.FFmpeg/Filter/Cuda/HardwareUploadCudaFilter.cs @@ -0,0 +1,13 @@ +namespace ErsatzTV.FFmpeg.Filter.Cuda; + +public class HardwareUploadCudaFilter(FrameDataLocation frameDataLocation) : BaseFilter +{ + public override string Filter => frameDataLocation switch + { + FrameDataLocation.Hardware => string.Empty, + _ => "hwupload_cuda" + }; + + public override FrameState NextState(FrameState currentState) => + currentState with { FrameDataLocation = FrameDataLocation.Hardware }; +} diff --git a/ErsatzTV.FFmpeg/Filter/HardwareUploadCudaFilter.cs b/ErsatzTV.FFmpeg/Filter/HardwareUploadCudaFilter.cs deleted file mode 100644 index b15a31466..000000000 --- a/ErsatzTV.FFmpeg/Filter/HardwareUploadCudaFilter.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace ErsatzTV.FFmpeg.Filter; - -public class HardwareUploadCudaFilter : BaseFilter -{ - private readonly FrameState _currentState; - - public HardwareUploadCudaFilter(FrameState currentState) => _currentState = currentState; - - public override string Filter => _currentState.FrameDataLocation switch - { - FrameDataLocation.Hardware => string.Empty, - _ => "hwupload_cuda" - }; - - public override FrameState NextState(FrameState currentState) => - currentState with { FrameDataLocation = FrameDataLocation.Hardware }; -} diff --git a/ErsatzTV.FFmpeg/GlobalOption/HardwareAcceleration/CudaHardwareAccelerationOption.cs b/ErsatzTV.FFmpeg/GlobalOption/HardwareAcceleration/CudaHardwareAccelerationOption.cs index a5f12e2f2..ad7832237 100644 --- a/ErsatzTV.FFmpeg/GlobalOption/HardwareAcceleration/CudaHardwareAccelerationOption.cs +++ b/ErsatzTV.FFmpeg/GlobalOption/HardwareAcceleration/CudaHardwareAccelerationOption.cs @@ -11,7 +11,7 @@ public class CudaHardwareAccelerationOption(bool isVulkanHdr) : GlobalOption return ["-init_hw_device", "cuda=nv", "-init_hw_device", "vulkan=vk@nv", "-hwaccel", "vulkan"]; } - return ["-hwaccel", "cuda"]; + return ["-init_hw_device", "cuda", "-hwaccel", "cuda"]; } } } diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index 5aaa1ee83..2aaf2a8de 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -134,6 +134,15 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder foreach (IDecoder decoder in maybeDecoder) { videoInputFile.AddOption(decoder); + + // sometimes cuda fails to decode in hardware and falls back to software + // in that case, we need to upload to get the frame in hardware as expected + // this *should* no-op when frames are already in hardware + if (ffmpegState.DecoderHardwareAccelerationMode is HardwareAccelerationMode.Nvenc) + { + videoInputFile.FilterSteps.Add(new CudaSoftwareFallbackUploadFilter()); + } + return Some(decoder); } @@ -266,7 +275,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder && !context.HasSubtitleText && (context.HasSubtitleOverlay || context.HasWatermark || context.HasGraphicsEngine)) { - var hardwareUpload = new HardwareUploadCudaFilter(currentState); + var hardwareUpload = new HardwareUploadCudaFilter(currentState.FrameDataLocation); currentState = hardwareUpload.NextState(currentState); videoInputFile.FilterSteps.Add(hardwareUpload); } @@ -562,7 +571,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder else { watermark.FilterSteps.Add( - new HardwareUploadCudaFilter(currentState with { FrameDataLocation = FrameDataLocation.Software })); + new HardwareUploadCudaFilter(FrameDataLocation.Software)); var watermarkFilter = new OverlayWatermarkCudaFilter( watermark.DesiredState, @@ -612,7 +621,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder if (context.HasWatermark || context.HasGraphicsEngine) { - var subtitleHardwareUpload = new HardwareUploadCudaFilter(currentState); + var subtitleHardwareUpload = new HardwareUploadCudaFilter(currentState.FrameDataLocation); currentState = subtitleHardwareUpload.NextState(currentState); videoInputFile.FilterSteps.Add(subtitleHardwareUpload); } @@ -626,8 +635,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder { if (_ffmpegCapabilities.HasFilter(FFmpegKnownFilter.ScaleNpp)) { - var subtitleHardwareUpload = new HardwareUploadCudaFilter( - currentState with { FrameDataLocation = FrameDataLocation.Software }); + var subtitleHardwareUpload = new HardwareUploadCudaFilter(FrameDataLocation.Software); subtitle.FilterSteps.Add(subtitleHardwareUpload); // only scale if scaling or padding was used for main video stream @@ -648,8 +656,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder subtitle.FilterSteps.Add(scaleFilter); } - var subtitleHardwareUpload = new HardwareUploadCudaFilter( - currentState with { FrameDataLocation = FrameDataLocation.Software }); + var subtitleHardwareUpload = new HardwareUploadCudaFilter(FrameDataLocation.Software); subtitle.FilterSteps.Add(subtitleHardwareUpload); } @@ -700,8 +707,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder { graphicsEngine.FilterSteps.Add(new PixelFormatFilter(new PixelFormatYuva420P())); - graphicsEngine.FilterSteps.Add( - new HardwareUploadCudaFilter(currentState with { FrameDataLocation = FrameDataLocation.Software })); + graphicsEngine.FilterSteps.Add(new HardwareUploadCudaFilter(FrameDataLocation.Software)); var graphicsEngineFilter = new OverlayGraphicsEngineCudaFilter(); graphicsEngineOverlayFilterSteps.Add(graphicsEngineFilter);