From 87deaa6f3a54e656713b632e93ced44ff197e594 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 10 Feb 2022 15:39:45 -0600 Subject: [PATCH] nvidia improvements (#628) --- CHANGELOG.md | 3 + .../FFmpeg/FFmpegComplexFilterBuilderTests.cs | 207 +++++++++++++++++ .../FFmpeg/TranscodingTests.cs | 49 ++-- .../FFmpeg/FFmpegComplexFilterBuilder.cs | 211 +++++++++++++----- ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs | 18 +- ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs | 3 +- 6 files changed, 415 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51405be71..fc1de1ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix ui crash adding empty path to local library - Properly flag items as `File Not Found` when local library path (folder) is missing from disk - Fix playback bug with unknown pixel format +- Fix playback of interlaced mpeg2video on NVIDIA ### Added - Include `Series` category tag for all episodes in XMLTV @@ -20,6 +21,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Intermittent watermarks will now fade in and out - Show collection name in some playout build error messages +- Use hardware-accelerated filter for watermarks on NVIDIA +- Use hardware-accelerated deinterlace for some content on NVIDIA ## [0.4.0-alpha] - 2022-01-29 ### Fixed diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs index 2260182c9..04320e6ab 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs @@ -304,6 +304,213 @@ namespace ErsatzTV.Core.Tests.FFmpeg filter.VideoLabel.Should().Be(expectedVideoLabel); }); } + + [Test] + [TestCase( + false, + false, + false, + ChannelWatermarkLocation.BottomLeft, + false, + 100, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + false, + ChannelWatermarkLocation.BottomLeft, + false, + 100, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]", + "0:1", + "[v]", + true)] + [TestCase( + false, + false, + true, + ChannelWatermarkLocation.TopLeft, + false, + 100, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + true, + ChannelWatermarkLocation.TopLeft, + false, + 100, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "0:1", + "[v]", + true)] + [TestCase( + false, + false, + false, + ChannelWatermarkLocation.TopLeft, + true, + 100, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + false, + ChannelWatermarkLocation.TopLeft, + true, + 100, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "0:1", + "[v]", + true)] + [TestCase( + false, + false, + false, + ChannelWatermarkLocation.TopLeft, + false, + 90, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + false, + ChannelWatermarkLocation.TopLeft, + false, + 90, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "0:1", + "[v]", + true)] + // TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid? + // [TestCase( + // false, + // true, + // false, + // ChannelWatermarkLocation.TopLeft, + // false, + // 100, + // "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", + // "0:1", + // "[v]")] + // [TestCase( + // false, + // true, + // false, + // ChannelWatermarkLocation.TopLeft, + // true, + // 100, + // "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", + // "0:1", + // "[v]")] + // [TestCase( + // true, + // true, + // false, + // ChannelWatermarkLocation.TopLeft, + // false, + // 100, + // "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", + // "[a]", + // "[v]")] + [TestCase( + true, + false, + false, + ChannelWatermarkLocation.TopLeft, + false, + 100, + "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "[a]", + "[v]", + false)] + [TestCase( + true, + false, + false, + ChannelWatermarkLocation.TopLeft, + false, + 100, + "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "[a]", + "[v]", + true)] + public void Should_Return_NVENC_Watermark( + bool alignAudio, + bool deinterlace, + bool intermittent, + ChannelWatermarkLocation location, + bool scaled, + int opacity, + string expectedVideoFilter, + string expectedAudioLabel, + string expectedVideoLabel, + bool scaledSource) + { + var watermark = new ChannelWatermark + { + Mode = intermittent + ? ChannelWatermarkMode.Intermittent + : ChannelWatermarkMode.Permanent, + DurationSeconds = intermittent ? 15 : 0, + FrequencyMinutes = intermittent ? 10 : 0, + Location = location, + Size = scaled ? ChannelWatermarkSize.Scaled : ChannelWatermarkSize.ActualSize, + WidthPercent = scaled ? 20 : 0, + Opacity = opacity, + HorizontalMarginPercent = 7, + VerticalMarginPercent = 5 + }; + + Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent + ? Some( + WatermarkCalculator.CalculateFadePoints( + new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), + TimeSpan.Zero, + TimeSpan.FromMinutes(55), + TimeSpan.Zero, + watermark.FrequencyMinutes, + watermark.DurationSeconds)) + : None; + + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) + .WithWatermark( + Some(watermark), + maybeFadePoints, + new Resolution { Width = 1920, Height = 1080 }, + None) + .WithDeinterlace(deinterlace) + .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); + + if (scaledSource) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be(expectedAudioLabel); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } [Test] [TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")] diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index fef11ae4f..b8bfc173d 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -37,8 +37,26 @@ namespace ErsatzTV.Core.Tests.FFmpeg Assert.Pass(); } + public enum Padding + { + NoPadding, + WithPadding + } + private class TestData { + public static Padding[] Paddings = + { + Padding.NoPadding, + Padding.WithPadding + }; + + public static VideoScanKind[] VideoScanKinds = + { + VideoScanKind.Progressive, + VideoScanKind.Interlaced + }; + public static string[] InputCodecs = { "h264", @@ -51,9 +69,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg { "yuv420p", "yuv420p10le", - "yuvj420p", - "yuv444p", - "yuv444p10le" + // "yuvj420p", + // "yuv444p", + // "yuv444p10le" }; public static Resolution[] Resolutions = @@ -115,27 +133,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg string inputPixelFormat, [ValueSource(typeof(TestData), nameof(TestData.Resolutions))] Resolution profileResolution, - [Values(true, false)] - bool pad, + [ValueSource(typeof(TestData), nameof(TestData.Paddings))] + Padding padding, + [ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] + VideoScanKind videoScanKind, // [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) - // [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec, - // [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) + [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec, + [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) - [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec, - [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) + // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec, + // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) { string name = GetStringSha256Hash( - $"{inputCodec}_{inputPixelFormat}_{pad}_{profileResolution}_{profileCodec}_{profileAcceleration}"); + $"{inputCodec}_{inputPixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}"); string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); if (!File.Exists(file)) { - string resolution = pad ? "1920x1060" : "1920x1080"; + string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080"; + + string videoFilter = videoScanKind == VideoScanKind.Interlaced ? "-vf tinterlace=interleave_top,fieldorder=tff" : string.Empty; + string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty; - var args = - $"-y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {file}"; + string args = + $"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {flags} {file}"; var p1 = new Process { StartInfo = new ProcessStartInfo diff --git a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs index db51cf51f..1de58258e 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs @@ -25,6 +25,7 @@ namespace ErsatzTV.Core.FFmpeg private Option> _maybeFadePoints = None; private Option _watermarkIndex; private string _pixelFormat; + private string _videoDecoder; private string _videoEncoder; private Option _subtitle; private bool _boxBlur; @@ -75,6 +76,12 @@ namespace ErsatzTV.Core.FFmpeg return this; } + public FFmpegComplexFilterBuilder WithDecoder(string decoder) + { + _videoDecoder = decoder; + return this; + } + public FFmpegComplexFilterBuilder WithInputPixelFormat(Option maybePixelFormat) { foreach (string pixelFormat in maybePixelFormat) @@ -145,20 +152,32 @@ namespace ErsatzTV.Core.FFmpeg string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a"); HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None); + bool isHardwareDecode = acceleration switch { HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4", - + // we need an initial hwupload_cuda when only padding with these pixel formats HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome => !isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"), - HardwareAccelerationKind.Nvenc => !isSong, + HardwareAccelerationKind.Nvenc => !isSong && + (string.IsNullOrWhiteSpace(_videoDecoder) || + _videoDecoder.Contains("cuvid")), HardwareAccelerationKind.Qsv => !isSong, HardwareAccelerationKind.VideoToolbox => false, _ => false }; + bool nvencDeinterlace = acceleration == HardwareAccelerationKind.Nvenc && _videoDecoder == "mpeg2_cuvid" && + _deinterlace; + // mpeg2_cuvid will handle deinterlace and is "not" a hardware decode + if (nvencDeinterlace) + { + _deinterlace = false; + isHardwareDecode = false; + } + var audioFilterQueue = new List(); var videoFilterQueue = new List(); var watermarkPreprocess = new List(); @@ -215,15 +234,17 @@ namespace ErsatzTV.Core.FFmpeg if (_deinterlace) { - string filter = acceleration switch + Option maybeFilter = acceleration switch { HardwareAccelerationKind.Qsv => "deinterlace_qsv", + HardwareAccelerationKind.Nvenc when !usesHardwareFilters && _pixelFormat.Contains("p10le") => + "hwupload_cuda,yadif_cuda", HardwareAccelerationKind.Nvenc => "yadif_cuda", HardwareAccelerationKind.Vaapi => "deinterlace_vaapi", _ => "yadif=1" }; - if (!string.IsNullOrWhiteSpace(filter)) + foreach (string filter in maybeFilter) { videoFilterQueue.Add(filter); } @@ -243,67 +264,64 @@ namespace ErsatzTV.Core.FFmpeg videoFilterQueue.Add("format=nv12|vaapi,hwupload"); } - _scaleToSize.IfSome( - size => - { - string filter = acceleration switch - { - HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", - HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" => - $"hwupload_cuda,scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}", - HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}", - _ when videoOnly => $"scale={size.Width}:{size.Height}:force_original_aspect_ratio=increase,crop={size.Width}:{size.Height}", - _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" - }; - - if (!string.IsNullOrWhiteSpace(filter)) - { - videoFilterQueue.Add(filter); - } - }); - bool scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome; bool usesSoftwareFilters = _padToSize.IsSome || _watermark.IsSome; + bool hasFadePoints = _maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0; - if (scaleOrPad && _boxBlur == false) - { - videoFilterQueue.Add("setsar=1"); - } - + var softwareFilterQueue = new List(); if (usesSoftwareFilters) { if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters)) { - videoFilterQueue.Add("hwdownload"); - string format = acceleration switch + Option maybeFormat = acceleration switch { HardwareAccelerationKind.Vaapi => "format=nv12|vaapi", + HardwareAccelerationKind.Nvenc when _padToSize.IsNone || nvencDeinterlace => None, HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" => "format=p010le,format=nv12", HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p", _ when isSong => "format=yuv420p", _ => "format=nv12" }; - videoFilterQueue.Add(format); + + foreach (string format in maybeFormat) + { + softwareFilterQueue.Add("hwdownload"); + softwareFilterQueue.Add(format); + } + + if (nvencDeinterlace) + { + softwareFilterQueue.Add("hwdownload"); + } } if (_boxBlur) { - videoFilterQueue.Add("boxblur=40"); + softwareFilterQueue.Add("boxblur=40"); } if (videoOnly) { - videoFilterQueue.Add("deband"); + softwareFilterQueue.Add("deband"); } foreach (ChannelWatermark watermark in _watermark) { - if (watermark.Opacity != 100 || _maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0) + Option maybeFormats = acceleration switch { - const string FORMATS = "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8"; - watermarkPreprocess.Add($"format={FORMATS}"); + // overlay_cuda only supports alpha with yuva420p + HardwareAccelerationKind.Nvenc => "yuva420p", + + _ when watermark.Opacity != 100 || hasFadePoints => + "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8", + + _ => None + }; + + foreach (string formats in maybeFormats) + { + watermarkPreprocess.Add($"format={formats}"); } double horizontalMargin = Math.Round(watermark.HorizontalMarginPercent / 100.0 * _resolution.Width); @@ -338,9 +356,18 @@ namespace ErsatzTV.Core.FFmpeg watermarkPreprocess.AddRange(fadePoints.Map(fp => fp.ToFilter())); } - watermarkOverlay = $"overlay={position}"; + if (acceleration == HardwareAccelerationKind.Nvenc) + { + watermarkPreprocess.Add("hwupload_cuda"); + } - if (_maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0) + watermarkOverlay = acceleration switch + { + HardwareAccelerationKind.Nvenc => $"overlay_cuda={position}", + _ => $"overlay={position}" + }; + + if (hasFadePoints && acceleration != HardwareAccelerationKind.Nvenc) { watermarkOverlay += "," + acceleration switch { @@ -351,27 +378,8 @@ namespace ErsatzTV.Core.FFmpeg } } } - - _padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2")); - foreach (string subtitle in _subtitle) - { - videoFilterQueue.Add(subtitle); - } - string outputPixelFormat = null; - - if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None && - string.IsNullOrWhiteSpace(watermarkOverlay)) - { - string upload = acceleration switch - { - HardwareAccelerationKind.Qsv => "hwupload=extra_hw_frames=64", - _ => "hwupload" - }; - videoFilterQueue.Add(upload); - } - if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay)) { switch (acceleration, _videoEncoder, _pixelFormat) @@ -385,6 +393,90 @@ namespace ErsatzTV.Core.FFmpeg } } + string outputFormat = (_videoEncoder, _pixelFormat) switch + { + ("hevc_nvenc", "yuv420p10le") => "p010le", + ("h264_nvenc", "yuv420p10le") => "p010le", + _ => null + }; + + _scaleToSize.IfSome( + size => + { + string filter = acceleration switch + { + HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", + HardwareAccelerationKind.Nvenc when _watermark.IsSome && _scaleToSize.IsNone => + $"format=yuv420p,hwupload_cuda,scale_cuda={size.Width}:{size.Height}", + HardwareAccelerationKind.Nvenc when _watermark.IsSome && _padToSize.IsNone => + $"scale_cuda={size.Width}:{size.Height}", + HardwareAccelerationKind.Nvenc when _watermark.IsNone && !string.IsNullOrEmpty(outputFormat) => + $"scale_cuda={size.Width}:{size.Height}:format={outputFormat}", + HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" && usesHardwareFilters == false => + $"hwupload_cuda,scale_cuda={size.Width}:{size.Height}", + HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}", + HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}", + _ when videoOnly => $"scale={size.Width}:{size.Height}:force_original_aspect_ratio=increase,crop={size.Width}:{size.Height}", + _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" + }; + + if (!string.IsNullOrWhiteSpace(filter)) + { + videoFilterQueue.Add(filter); + } + }); + + if (scaleOrPad && _boxBlur == false) + { + if (acceleration == HardwareAccelerationKind.Nvenc) + { + if (!isHardwareDecode && !string.IsNullOrWhiteSpace(outputPixelFormat)) + { + videoFilterQueue.Add($"hwdownload,format={outputPixelFormat}"); + } + } + + videoFilterQueue.Add("setsar=1"); + } + + videoFilterQueue.AddRange(softwareFilterQueue); + + _padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2")); + + if (acceleration == HardwareAccelerationKind.Nvenc && _watermark.IsSome) + { + if (_scaleToSize.IsSome) + { + videoFilterQueue.Add("hwdownload,format=nv12,format=yuv420p"); + videoFilterQueue.Add("hwupload_cuda"); + } + else if (_padToSize.IsNone) + { + videoFilterQueue.Add("scale_cuda=format=yuv420p"); + } + else + { + videoFilterQueue.Add("format=yuv420p"); + videoFilterQueue.Add("hwupload_cuda"); + } + } + + foreach (string subtitle in _subtitle) + { + videoFilterQueue.Add(subtitle); + } + + if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None && + string.IsNullOrWhiteSpace(watermarkOverlay)) + { + string upload = acceleration switch + { + HardwareAccelerationKind.Qsv => "hwupload=extra_hw_frames=64", + _ => "hwupload" + }; + videoFilterQueue.Add(upload); + } + bool hasAudioFilters = audioFilterQueue.Any(); if (hasAudioFilters) { @@ -447,6 +539,9 @@ namespace ErsatzTV.Core.FFmpeg case (true, HardwareAccelerationKind.Nvenc): complexFilter.Append(",hwupload_cuda"); break; + // no need to upload since we're already in the GPU with overlay_cuda + case (_, HardwareAccelerationKind.Nvenc) when scaleOrPad == false && _watermark.IsSome: + break; case (_, HardwareAccelerationKind.Qsv): complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64"); break; diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 6f7bedb76..4b1b436e2 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -286,7 +286,8 @@ namespace ErsatzTV.Core.FFmpeg string audioPath, string decoder, Option codec, - Option pixelFormat) + Option pixelFormat, + bool deinterlace) { if (audioPath == videoPath) { @@ -306,6 +307,15 @@ namespace ErsatzTV.Core.FFmpeg { _arguments.Add("-c:v"); _arguments.Add(decoder); + + if (decoder == "mpeg2_cuvid" && deinterlace) + { + _arguments.Add("-deint"); + _arguments.Add("2"); + } + + _complexFilterBuilder = _complexFilterBuilder + .WithDecoder(decoder); } _complexFilterBuilder = _complexFilterBuilder @@ -633,14 +643,14 @@ namespace ErsatzTV.Core.FFmpeg } }); - _arguments.Add("-map"); - _arguments.Add(videoLabel); - foreach (string _ in audioPath) { _arguments.Add("-map"); _arguments.Add(audioLabel); } + + _arguments.Add("-map"); + _arguments.Add(videoLabel); return this; } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index 7afa0200f..3cb374d70 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -114,7 +114,8 @@ namespace ErsatzTV.Core.FFmpeg audioPath, playbackSettings.VideoDecoder, videoStream.Codec, - videoStream.PixelFormat) + videoStream.PixelFormat, + playbackSettings.Deinterlace) .WithWatermark(watermarkOptions, maybeFadePoints, channel.FFmpegProfile.Resolution) .WithFrameRate(playbackSettings.FrameRate) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)