From e8bc051f73913be677410c73eec1c01c65976f8d Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 29 Sep 2023 06:29:59 -0500 Subject: [PATCH] transcoding improvements (#1452) * use noautoscale with vaapi encoder * only use one input file for vaapi with radeonsi driver * fix vaapi 8-bit to 10-bit * fix nvidia subtitle scaling * optimize nvidia subtitle scaling * fix test pgs subtitle --- CHANGELOG.md | 4 +- .../FFmpeg/FFmpegLibraryProcessService.cs | 3 +- .../PipelineBuilderBaseTests.cs | 3 +- ErsatzTV.FFmpeg/CommandGenerator.cs | 9 ++--- ErsatzTV.FFmpeg/FFmpegPipeline.cs | 2 +- ErsatzTV.FFmpeg/Filter/ComplexFilter.cs | 5 +-- .../Filter/Cuda/ScaleCudaFilter.cs | 2 + .../OutputOption/NoAutoScaleOutputOption.cs | 6 +++ .../Pipeline/NvidiaPipelineBuilder.cs | 38 +++++++++++++----- .../Pipeline/PipelineBuilderBase.cs | 15 ++++--- ErsatzTV.FFmpeg/Pipeline/PipelineContext.cs | 3 +- .../Pipeline/QsvPipelineBuilder.cs | 4 ++ .../Pipeline/SoftwarePipelineBuilder.cs | 2 + .../Pipeline/VaapiPipelineBuilder.cs | 21 +++++++++- ErsatzTV.Scanner.Tests/Resources/test.sup | Bin 1789 -> 1789 bytes 15 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 ErsatzTV.FFmpeg/OutputOption/NoAutoScaleOutputOption.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bdf0452e..0fe887d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Include `inputstream.ffmpegdirect` properties in channels.m3u when requested by Kodi ### Fixed -- Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day +- Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day +- Fix VAAPI transcoding 8-bit source content to 10-bit +- Fix NVIDIA subtitle scaling when `scale_npp` filter is unavailable ### Changed - Upgrade ffmpeg to 6.1, which is now *required* for all installs diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index f973b50c8..29895b6c8 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -762,7 +762,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService audioInputFile, watermarkInputFile, concatInputFile, - pipeline.PipelineSteps); + pipeline.PipelineSteps, + pipeline.IsIntelVaapiOrQsv); if (environmentVariables.Any()) { diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 2097612f1..62de86f76 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs @@ -480,7 +480,8 @@ public class PipelineBuilderBaseTests audioInputFile, watermarkInputFile, concatInputFile, - pipeline.PipelineSteps); + pipeline.PipelineSteps, + pipeline.IsIntelVaapiOrQsv); var command = string.Join(" ", arguments); diff --git a/ErsatzTV.FFmpeg/CommandGenerator.cs b/ErsatzTV.FFmpeg/CommandGenerator.cs index f878e6382..cd50d71c5 100644 --- a/ErsatzTV.FFmpeg/CommandGenerator.cs +++ b/ErsatzTV.FFmpeg/CommandGenerator.cs @@ -2,7 +2,6 @@ using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Filter; using ErsatzTV.FFmpeg.GlobalOption; -using ErsatzTV.FFmpeg.GlobalOption.HardwareAcceleration; using ErsatzTV.FFmpeg.InputOption; namespace ErsatzTV.FFmpeg; @@ -17,7 +16,8 @@ public static class CommandGenerator Option maybeAudioInputFile, Option maybeWatermarkInputFile, Option maybeConcatInputFile, - IList pipelineSteps) + IList pipelineSteps, + bool isIntelVaapiOrQsv) { var arguments = new List(); @@ -41,10 +41,7 @@ public static class CommandGenerator foreach (AudioInputFile audioInputFile in maybeAudioInputFile) { - bool isVaapiOrQsv = - pipelineSteps.Any(s => s is VaapiHardwareAccelerationOption or QsvHardwareAccelerationOption); - - if (!includedPaths.Contains(audioInputFile.Path) || isVaapiOrQsv) + if (!includedPaths.Contains(audioInputFile.Path) || isIntelVaapiOrQsv) { includedPaths.Add(audioInputFile.Path); diff --git a/ErsatzTV.FFmpeg/FFmpegPipeline.cs b/ErsatzTV.FFmpeg/FFmpegPipeline.cs index 91cd7f702..33d5c573f 100644 --- a/ErsatzTV.FFmpeg/FFmpegPipeline.cs +++ b/ErsatzTV.FFmpeg/FFmpegPipeline.cs @@ -1,3 +1,3 @@ namespace ErsatzTV.FFmpeg; -public record FFmpegPipeline(IList PipelineSteps); +public record FFmpegPipeline(IList PipelineSteps, bool IsIntelVaapiOrQsv); diff --git a/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs b/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs index 5d4132aaf..b8f485663 100644 --- a/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/ComplexFilter.cs @@ -73,9 +73,8 @@ public class ComplexFilter : IPipelineStep foreach ((string path, _) in _maybeAudioInputFile) { if (!distinctPaths.Contains(path) || - // use audio as a separate input with vaapi/qsv - _pipelineContext.HardwareAccelerationMode is HardwareAccelerationMode.Vaapi - or HardwareAccelerationMode.Qsv) + // use audio as a separate input with intel vaapi/qsv + _pipelineContext.IsIntelVaapiOrQsv) { distinctPaths.Add(path); } diff --git a/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs b/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs index 35b50b498..6f9fb3801 100644 --- a/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs @@ -24,6 +24,8 @@ public class ScaleCudaFilter : BaseFilter _isAnamorphicEdgeCase = isAnamorphicEdgeCase; } + public bool IsFormatOnly => _currentState.ScaledSize == _scaledSize; + public override string Filter { get diff --git a/ErsatzTV.FFmpeg/OutputOption/NoAutoScaleOutputOption.cs b/ErsatzTV.FFmpeg/OutputOption/NoAutoScaleOutputOption.cs new file mode 100644 index 000000000..677744b00 --- /dev/null +++ b/ErsatzTV.FFmpeg/OutputOption/NoAutoScaleOutputOption.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.FFmpeg.OutputOption; + +public class NoAutoScaleOutputOption : OutputOption +{ + public override IList OutputOptions => new List { "-noautoscale" }; +} diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index 6d2855e28..60bfe1298 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -16,6 +16,7 @@ namespace ErsatzTV.FFmpeg.Pipeline; public class NvidiaPipelineBuilder : SoftwarePipelineBuilder { + private readonly IFFmpegCapabilities _ffmpegCapabilities; private readonly IHardwareCapabilities _hardwareCapabilities; private readonly ILogger _logger; @@ -40,6 +41,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder fontsFolder, logger) { + _ffmpegCapabilities = ffmpegCapabilities; _hardwareCapabilities = hardwareCapabilities; _logger = logger; } @@ -417,7 +419,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder return currentState; } - private static FrameState SetSubtitle( + private FrameState SetSubtitle( VideoInputFile videoInputFile, Option subtitleInputFile, PipelineContext context, @@ -466,17 +468,33 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder if (currentState.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 8) { - var subtitleHardwareUpload = new HardwareUploadCudaFilter( - currentState with { FrameDataLocation = FrameDataLocation.Software }); - subtitle.FilterSteps.Add(subtitleHardwareUpload); - - // only scale if scaling or padding was used for main video stream - if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter or PadFilter)) + if (_ffmpegCapabilities.HasFilter("scale_npp")) { - var scaleFilter = new SubtitleScaleNppFilter(desiredState.PaddedSize); - subtitle.FilterSteps.Add(scaleFilter); - } + var subtitleHardwareUpload = new HardwareUploadCudaFilter( + currentState with { FrameDataLocation = FrameDataLocation.Software }); + subtitle.FilterSteps.Add(subtitleHardwareUpload); + // only scale if scaling or padding was used for main video stream + if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter { IsFormatOnly: false } or PadFilter)) + { + var scaleFilter = new SubtitleScaleNppFilter(desiredState.PaddedSize); + subtitle.FilterSteps.Add(scaleFilter); + } + } + else + { + // only scale if scaling or padding was used for main video stream + if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter { IsFormatOnly: false } or PadFilter)) + { + var scaleFilter = new ScaleImageFilter(desiredState.PaddedSize); + subtitle.FilterSteps.Add(scaleFilter); + } + + var subtitleHardwareUpload = new HardwareUploadCudaFilter( + currentState with { FrameDataLocation = FrameDataLocation.Software }); + subtitle.FilterSteps.Add(subtitleHardwareUpload); + } + var subtitlesFilter = new OverlaySubtitleCudaFilter(); subtitleOverlayFilterSteps.Add(subtitlesFilter); } diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index a3ce6a112..5becbdf87 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -66,7 +66,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder pipelineSteps.Add(scaleStep); pipelineSteps.Add(new FileNameOutputOption(outputFile)); - return new FFmpegPipeline(pipelineSteps); + return new FFmpegPipeline(pipelineSteps, false); } public FFmpegPipeline Concat(ConcatInputFile concatInputFile, FFmpegState ffmpegState) @@ -113,7 +113,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder pipelineSteps.Add(new FFReportVariable(_reportsFolder, concatInputFile)); } - return new FFmpegPipeline(pipelineSteps); + return new FFmpegPipeline(pipelineSteps, false); } public FFmpegPipeline WrapSegmenter(ConcatInputFile concatInputFile, FFmpegState ffmpegState) @@ -138,7 +138,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder pipelineSteps.Add(new OutputFormatMpegTs(false)); pipelineSteps.Add(new PipeProtocol()); - return new FFmpegPipeline(pipelineSteps); + return new FFmpegPipeline(pipelineSteps, false); } public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) @@ -174,7 +174,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder _subtitleInputFile.Map(s => s is { IsImageBased: true, Method: SubtitleMethod.Burn }).IfNone(false), _subtitleInputFile.Map(s => s is { IsImageBased: false, Method: SubtitleMethod.Burn }).IfNone(false), desiredState.Deinterlaced, - desiredState.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10); + desiredState.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10, + false); SetThreadCount(ffmpegState, desiredState, pipelineSteps); SetSceneDetect(videoStream, ffmpegState, desiredState, pipelineSteps); @@ -190,6 +191,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder context, pipelineSteps); + context = context with { IsIntelVaapiOrQsv = IsIntelVaapiOrQsv(ffmpegState) }; + if (_audioInputFile.IsNone) { pipelineSteps.Add(new EncoderCopyAudio()); @@ -219,7 +222,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder pipelineSteps.Add(complexFilter); - return new FFmpegPipeline(pipelineSteps); + return new FFmpegPipeline(pipelineSteps, context.IsIntelVaapiOrQsv); } private void LogUnknownDecoder( @@ -397,6 +400,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder } } + protected abstract bool IsIntelVaapiOrQsv(FFmpegState ffmpegState); + protected abstract FFmpegState SetAccelState( VideoStream videoStream, FFmpegState ffmpegState, diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineContext.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineContext.cs index 9a34940b9..7827b8a99 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineContext.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineContext.cs @@ -6,4 +6,5 @@ public record PipelineContext( bool HasSubtitleOverlay, bool HasSubtitleText, bool ShouldDeinterlace, - bool Is10BitOutput); + bool Is10BitOutput, + bool IsIntelVaapiOrQsv); diff --git a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs index d4a5507eb..0e9a5b6cd 100644 --- a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs @@ -44,6 +44,10 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder _logger = logger; } + protected override bool IsIntelVaapiOrQsv(FFmpegState ffmpegState) => + ffmpegState.DecoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv || + ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv; + protected override FFmpegState SetAccelState( VideoStream videoStream, FFmpegState ffmpegState, diff --git a/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs index 034239db4..4afd28558 100644 --- a/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs @@ -35,6 +35,8 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase logger) => _logger = logger; + protected override bool IsIntelVaapiOrQsv(FFmpegState ffmpegState) => false; + protected override FFmpegState SetAccelState( VideoStream videoStream, FFmpegState ffmpegState, diff --git a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs index 2726beb70..930c898b8 100644 --- a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs @@ -8,6 +8,7 @@ using ErsatzTV.FFmpeg.Filter.Vaapi; using ErsatzTV.FFmpeg.Format; using ErsatzTV.FFmpeg.GlobalOption.HardwareAcceleration; using ErsatzTV.FFmpeg.InputOption; +using ErsatzTV.FFmpeg.OutputOption; using ErsatzTV.FFmpeg.State; using Microsoft.Extensions.Logging; @@ -43,6 +44,12 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder _logger = logger; } + // check for intel vaapi (NOT radeon) + protected override bool IsIntelVaapiOrQsv(FFmpegState ffmpegState) => + (ffmpegState.DecoderHardwareAccelerationMode is HardwareAccelerationMode.Vaapi || + ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Vaapi) && + !ffmpegState.VaapiDriver.IfNone(string.Empty).StartsWith("radeon", StringComparison.OrdinalIgnoreCase); + protected override FFmpegState SetAccelState( VideoStream videoStream, FFmpegState ffmpegState, @@ -75,6 +82,12 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder decodeCapability = FFmpegCapability.Software; } + // disable auto scaling when using hw encoding + if (encodeCapability is FFmpegCapability.Hardware) + { + pipelineSteps.Add(new NoAutoScaleOutputOption()); + } + // disable hw accel if decoder/encoder isn't supported return ffmpegState with { @@ -274,8 +287,12 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder { _logger.LogDebug("FrameDataLocation == FrameDataLocation.Hardware"); - var hardwareDownload = - new HardwareDownloadFilter(currentState with { PixelFormat = Some(format) }); + // don't try to download from 8-bit to 10-bit + HardwareDownloadFilter hardwareDownload = currentState.BitDepth == 8 && + desiredPixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10 + ? new HardwareDownloadFilter(currentState) + : new HardwareDownloadFilter(currentState with { PixelFormat = Some(format) }); + currentState = hardwareDownload.NextState(currentState); result.Add(hardwareDownload); } diff --git a/ErsatzTV.Scanner.Tests/Resources/test.sup b/ErsatzTV.Scanner.Tests/Resources/test.sup index fd43361ea55d09e1bd88e2f41fbca12ff57bd46a..e99cdda17eaca460343d159130bb90857dcccc06 100644 GIT binary patch delta 139 zcmey%`)57GuSYwfYf+w*=Wnb zBrDL+P@ity(C{Mz$a2X95)43D5CE#!oW!Kg#F#d@i{+(4x}u-}yK0&$uTroSipXQ6F7#1};Vh=10uj3^oiZAT>U^8*Mq5 zWUU(-exwUDG}LDR371SD!2py60icS_NlfZYjOmlRSY9fmDGCa(tEQ{+Dg{fiNO_v+ gTc#VPSqp0D>qt2WvW2E^p3G{^$O5u(@+CHH0Dx&9SpWb4