From aa5ba5a78e1dcf1b5b90af0855dbe6989fc84967 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:15:23 -0500 Subject: [PATCH] fix recent nvidia regression (#2437) * fix recent nvidia regression * update transcoding tests for graphics engine --- .../OutputOption/PixelFormatOutputOption.cs | 15 ++++--- .../Pipeline/NvidiaPipelineBuilder.cs | 30 +++++++++---- .../Core/FFmpeg/TranscodingTests.cs | 44 ++++++++++++++----- 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/ErsatzTV.FFmpeg/OutputOption/PixelFormatOutputOption.cs b/ErsatzTV.FFmpeg/OutputOption/PixelFormatOutputOption.cs index 5e4a18a16..083e46685 100644 --- a/ErsatzTV.FFmpeg/OutputOption/PixelFormatOutputOption.cs +++ b/ErsatzTV.FFmpeg/OutputOption/PixelFormatOutputOption.cs @@ -2,14 +2,17 @@ namespace ErsatzTV.FFmpeg.OutputOption; -public class PixelFormatOutputOption : OutputOption +public class PixelFormatOutputOption( + IPixelFormat pixelFormat, + HardwareAccelerationMode encoderMode = HardwareAccelerationMode.None) + : OutputOption { - private readonly IPixelFormat _pixelFormat; - public PixelFormatOutputOption(IPixelFormat pixelFormat) => _pixelFormat = pixelFormat; - - public override string[] OutputOptions => ["-pix_fmt", _pixelFormat.Name]; + public override string[] OutputOptions => + [ + "-pix_fmt", encoderMode is HardwareAccelerationMode.Nvenc ? pixelFormat.FFmpegName : pixelFormat.Name + ]; public override FrameState NextState(FrameState currentState) => - currentState with { PixelFormat = Some(_pixelFormat) }; + currentState with { PixelFormat = Some(pixelFormat) }; } diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index 381aabc7a..97df70689 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -181,6 +181,19 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder true); currentState = filter.NextState(currentState); videoInputFile.FilterSteps.Add(filter); + + if (desiredState.BitDepth == 8) + { + var filter2 = new ScaleCudaFilter( + currentState with { PixelFormat = new PixelFormatYuv420P() }, + videoStream.FrameSize, + videoStream.FrameSize, + Option.None, + false, + false); + currentState = filter2.NextState(currentState); + videoInputFile.FilterSteps.Add(filter2); + } } } @@ -214,7 +227,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder { Option desiredPixelFormat = Some((IPixelFormat)new PixelFormatYuv420P()); - if (desiredPixelFormat != currentState.PixelFormat) + if (desiredPixelFormat.Map(pf => pf.FFmpegName) != currentState.PixelFormat.Map(pf => pf.FFmpegName)) { if (currentState.FrameDataLocation == FrameDataLocation.Software) { @@ -244,8 +257,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder } // need to upload for any sort of overlay - if (currentState.FrameDataLocation == FrameDataLocation.Software && - currentState.BitDepth == 8 && !context.HasSubtitleText + if (currentState is { FrameDataLocation: FrameDataLocation.Software, BitDepth: 8 } + && !context.HasSubtitleText && (context.HasSubtitleOverlay || context.HasWatermark || context.HasGraphicsEngine)) { var hardwareUpload = new HardwareUploadCudaFilter(currentState); @@ -263,10 +276,9 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder fontsFolder, subtitleOverlayFilterSteps); - // need to use software overlay for watermark with fade points - // because `-loop 1` seems to cause a green line at the bottom of the resulting video with overlay_cuda - if (context.HasWatermark && watermarkInputFile - .Map(wm => wm.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)).IfNone(false)) + // need to use software overlay with 10 bit primary content and graphics engine + if (currentState.FrameDataLocation is FrameDataLocation.Hardware && context.HasGraphicsEngine && + currentState.BitDepth == 10) { var hardwareDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat, None); currentState = hardwareDownload.NextState(currentState); @@ -434,12 +446,12 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder } else { - pipelineSteps.Add(new PixelFormatOutputOption(format)); + pipelineSteps.Add(new PixelFormatOutputOption(format, ffmpegState.EncoderHardwareAccelerationMode)); } } else { - pipelineSteps.Add(new PixelFormatOutputOption(format)); + pipelineSteps.Add(new PixelFormatOutputOption(format, ffmpegState.EncoderHardwareAccelerationMode)); } } diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index a977dafae..810a65ecb 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -113,16 +113,16 @@ public class TranscodingTests [ Watermark.None, Watermark.PermanentOpaqueScaled, - Watermark.PermanentOpaqueActualSize, - Watermark.PermanentTransparentScaled, - Watermark.PermanentTransparentActualSize + // Watermark.PermanentOpaqueActualSize, + // Watermark.PermanentTransparentScaled, + // Watermark.PermanentTransparentActualSize ]; public static Subtitle[] Subtitles = [ Subtitle.None, Subtitle.Picture, - Subtitle.Text + // Subtitle.Text ]; public static Padding[] Paddings = @@ -253,6 +253,13 @@ public class TranscodingTests Arg.Any>()) .Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "song_album_cover_512.png")); + var graphicsElementLoader = Substitute.For(); + graphicsElementLoader.LoadAll( + Arg.Any(), + Arg.Any>(), + Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + var oldService = new FFmpegProcessService( new FakeStreamSelector(), tempFilePool, @@ -272,7 +279,7 @@ public class TranscodingTests LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()), Substitute.For(), - Substitute.For(), + graphicsElementLoader, LoggerFactory.CreateLogger()); var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service); @@ -362,7 +369,7 @@ public class TranscodingTests PlayoutItemResult playoutItemResult = await service.ForPlayoutItem( ExecutableName("ffmpeg"), ExecutableName("ffprobe"), - false, + true, channel, videoVersion, new MediaItemAudioVersion(song, songVersion), @@ -617,7 +624,11 @@ public class TranscodingTests hasSubtitleFilters.ShouldBe(subtitle != Subtitle.None); bool hasWatermarkFilters = filterChain.WatermarkOverlayFilterSteps.Any(s => - s is OverlayWatermarkFilter or OverlayWatermarkCudaFilter or OverlayWatermarkQsvFilter); + s is OverlayWatermarkFilter or OverlayWatermarkCudaFilter + or OverlayWatermarkQsvFilter) || + filterChain.GraphicsEngineOverlayFilterSteps.Any(s => + s is OverlayGraphicsEngineFilter or OverlayGraphicsEngineCudaFilter + or OverlayGraphicsEngineVaapiFilter); hasWatermarkFilters.ShouldBe(watermark != Watermark.None); } @@ -668,7 +679,7 @@ public class TranscodingTests PlayoutItemResult playoutItemResult = await service.ForPlayoutItem( ExecutableName("ffmpeg"), ExecutableName("ffprobe"), - false, + true, channel, v, new MediaItemAudioVersion(null, v), @@ -725,6 +736,7 @@ public class TranscodingTests { ImageSource = ChannelWatermarkImageSource.Custom, Mode = ChannelWatermarkMode.Intermittent, + Image = "ASDF", // TODO: how do we make sure this actually appears FrequencyMinutes = 1, DurationSeconds = 2, @@ -735,6 +747,7 @@ public class TranscodingTests { ImageSource = ChannelWatermarkImageSource.Custom, Mode = ChannelWatermarkMode.Intermittent, + Image = "ASDF", // TODO: how do we make sure this actually appears FrequencyMinutes = 1, DurationSeconds = 2, @@ -745,6 +758,7 @@ public class TranscodingTests { ImageSource = ChannelWatermarkImageSource.Custom, Mode = ChannelWatermarkMode.Permanent, + Image = "ASDF", Opacity = 100, Size = WatermarkSize.Scaled, WidthPercent = 15 @@ -754,6 +768,7 @@ public class TranscodingTests { ImageSource = ChannelWatermarkImageSource.Custom, Mode = ChannelWatermarkMode.Permanent, + Image = "ASDF", Opacity = 100, Size = WatermarkSize.ActualSize }; @@ -762,6 +777,7 @@ public class TranscodingTests { ImageSource = ChannelWatermarkImageSource.Custom, Mode = ChannelWatermarkMode.Permanent, + Image = "ASDF", Opacity = 80, Size = WatermarkSize.Scaled, WidthPercent = 15 @@ -771,6 +787,7 @@ public class TranscodingTests { ImageSource = ChannelWatermarkImageSource.Custom, Mode = ChannelWatermarkMode.Permanent, + Image = "ASDF", Opacity = 80, Size = WatermarkSize.ActualSize }; @@ -930,6 +947,13 @@ public class TranscodingTests Arg.Any>()) .Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "song_album_cover_512.png")); + var graphicsElementLoader = Substitute.For(); + graphicsElementLoader.LoadAll( + Arg.Any(), + Arg.Any>(), + Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + var oldService = new FFmpegProcessService( new FakeStreamSelector(), Substitute.For(), @@ -949,7 +973,7 @@ public class TranscodingTests LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()), Substitute.For(), - Substitute.For(), + graphicsElementLoader, LoggerFactory.CreateLogger()); return service; @@ -1125,5 +1149,5 @@ public class TranscodingTests private static string ExecutableName(string baseName) => OperatingSystem.IsWindows() ? $"{baseName}.exe" - : $"/home/jason/Downloads/ffmpeg/ffmpeg-n7.1.1-56-gc2184b65d2-linux64-gpl-7.1/bin/{baseName}"; + : $"{baseName}"; }