Browse Source

feat: add QSV hardware padding via vpp_qsv pad options with capability gate

The QSV pipeline previously always padded in software, forcing a
hwdownload/pad/hwupload round-trip. ffmpeg's vpp_qsv filter is gaining
pad_w/pad_h/pad_x/pad_y/pad_color options, so when the ffmpeg binary
exposes them we can pad on the GPU instead, mirroring the existing
pad_vaapi hardware path.

A new capability probe runs `ffmpeg -h filter=vpp_qsv` and parses the
available AVOption names, surfaced as IFFmpegCapabilities.HasFilterOption.
QsvPipelineBuilder.SetPad now emits PadQsvFilter (vpp_qsv=pad_w=..:pad_h=..
:pad_x=-1:pad_y=-1:pad_color=black) when vpp_qsv advertises pad_w, and
keeps the software PadFilter fallback for HDR tonemap, forced software
pad mode, and stock ffmpeg builds without the pad options. The gate is
what makes this safe to upstream, since stock ffmpeg has no vpp_qsv pad
options yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/2965/head
McKenzie Pepper 2 days ago
parent
commit
30280aef0b
  1. 99
      ErsatzTV.FFmpeg.Tests/Capabilities/FFmpegFilterOptionParserTests.cs
  2. 72
      ErsatzTV.FFmpeg.Tests/Filter/Qsv/PadQsvFilterTests.cs
  3. 174
      ErsatzTV.FFmpeg.Tests/Pipeline/QsvPipelineBuilderTests.cs
  4. 3
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  5. 6
      ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs
  6. 21
      ErsatzTV.FFmpeg/Capabilities/FFmpegFilterOptionParser.cs
  7. 1
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs
  8. 45
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  9. 1
      ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs
  10. 40
      ErsatzTV.FFmpeg/Filter/Qsv/PadQsvFilter.cs
  11. 33
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

99
ErsatzTV.FFmpeg.Tests/Capabilities/FFmpegFilterOptionParserTests.cs

@ -0,0 +1,99 @@
using System.Collections.Generic;
using ErsatzTV.FFmpeg.Capabilities;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.FFmpeg.Tests.Capabilities;
[TestFixture]
public class FFmpegFilterOptionParserTests
{
// stock ffmpeg vpp_qsv, without the pad_w/pad_h/pad_x/pad_y/pad_color options
private const string WithoutPadOptions = @"Filter vpp_qsv
Quick Sync Video VPP.
Inputs:
#0: default (video)
Outputs:
#0: default (video)
vpp_qsv AVOptions:
deinterlace <int> ..FV....... deinterlace mode (from 0 to 2) (default 0)
bob 1 ..FV....... Get one frame for each field
advanced 2 ..FV....... Get one frame for each frame
denoise <int> ..FV....... denoise level [0, 100] (from 0 to 100) (default 0)
framerate <rational> ..FV....... output framerate (from 0 to DBL_MAX) (default 0/1)
w <string> ..FV....... Output video width
h <string> ..FV....... Output video height
format <string> ..FV....... Output pixel format
async_depth <int> ..FV....... Internal parallelization depth (from 0 to INT_MAX) (default 4)
scale_mode <int> ..FV....... scaling mode (from 0 to 2) (default 0)
";
// patched ffmpeg vpp_qsv, carrying the pad_w/pad_h/pad_x/pad_y/pad_color options
private const string WithPadOptions = @"Filter vpp_qsv
Quick Sync Video VPP.
Inputs:
#0: default (video)
Outputs:
#0: default (video)
vpp_qsv AVOptions:
deinterlace <int> ..FV....... deinterlace mode (from 0 to 2) (default 0)
bob 1 ..FV....... Get one frame for each field
advanced 2 ..FV....... Get one frame for each frame
denoise <int> ..FV....... denoise level [0, 100] (from 0 to 100) (default 0)
framerate <rational> ..FV....... output framerate (from 0 to DBL_MAX) (default 0/1)
w <string> ..FV....... Output video width
h <string> ..FV....... Output video height
format <string> ..FV....... Output pixel format
async_depth <int> ..FV....... Internal parallelization depth (from 0 to INT_MAX) (default 4)
scale_mode <int> ..FV....... scaling mode (from 0 to 2) (default 0)
pad_w <int> ..FV....... Padded width (from 0 to 8192) (default 0)
pad_h <int> ..FV....... Padded height (from 0 to 8192) (default 0)
pad_x <int> ..FV....... Horizontal position (from -1 to 8192) (default -1)
pad_y <int> ..FV....... Vertical position (from -1 to 8192) (default -1)
pad_color <color> ..FV....... Padding color (default ""black"")
";
[Test]
public void Should_Parse_Option_Names()
{
IReadOnlySet<string> options = FFmpegFilterOptionParser.Parse(WithoutPadOptions);
options.ShouldContain("deinterlace");
options.ShouldContain("denoise");
options.ShouldContain("framerate");
options.ShouldContain("w");
options.ShouldContain("h");
options.ShouldContain("format");
}
[Test]
public void Should_Not_Parse_Enum_Constants()
{
IReadOnlySet<string> options = FFmpegFilterOptionParser.Parse(WithoutPadOptions);
options.ShouldNotContain("bob");
options.ShouldNotContain("advanced");
}
[Test]
public void Should_Detect_Pad_Options_When_Present()
{
IReadOnlySet<string> options = FFmpegFilterOptionParser.Parse(WithPadOptions);
options.ShouldContain("pad_w");
options.ShouldContain("pad_h");
options.ShouldContain("pad_x");
options.ShouldContain("pad_y");
options.ShouldContain("pad_color");
}
[Test]
public void Should_Not_Detect_Pad_Options_When_Absent()
{
IReadOnlySet<string> options = FFmpegFilterOptionParser.Parse(WithoutPadOptions);
options.ShouldNotContain("pad_w");
options.ShouldNotContain("pad_h");
options.ShouldNotContain("pad_color");
}
}

72
ErsatzTV.FFmpeg.Tests/Filter/Qsv/PadQsvFilterTests.cs

@ -0,0 +1,72 @@
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Filter.Qsv;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.State;
using LanguageExt;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.FFmpeg.Tests.Filter.Qsv;
[TestFixture]
public class PadQsvFilterTests
{
private static FrameState FrameStateAt(FrameDataLocation location, Option<IPixelFormat> pixelFormat) => new(
false,
false,
string.Empty,
Option<string>.None,
Option<string>.None,
true,
pixelFormat,
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
Option<int>.None,
Option<int>.None,
Option<int>.None,
false,
false,
location);
[Test]
public void Should_Emit_Hardware_Pad_When_Frame_In_Hardware()
{
var filter = new PadQsvFilter(
FrameStateAt(FrameDataLocation.Hardware, Option<IPixelFormat>.None),
new FrameSize(1920, 1080),
64);
filter.Filter.ShouldBe("vpp_qsv=pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black");
}
[Test]
public void Should_Upload_Before_Pad_When_Frame_In_Software()
{
var filter = new PadQsvFilter(
FrameStateAt(FrameDataLocation.Software, Option<IPixelFormat>.Some(new PixelFormatNv12("yuv420p"))),
new FrameSize(1920, 1080),
64);
filter.Filter.ShouldBe(
"format=nv12,hwupload=extra_hw_frames=64,vpp_qsv=pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black");
}
[Test]
public void Should_Report_Padded_Size_And_Hardware_Location()
{
var filter = new PadQsvFilter(
FrameStateAt(FrameDataLocation.Software, Option<IPixelFormat>.None),
new FrameSize(1920, 1080),
64);
FrameState nextState = filter.NextState(
FrameStateAt(FrameDataLocation.Software, Option<IPixelFormat>.None));
nextState.PaddedSize.ShouldBe(new FrameSize(1920, 1080));
nextState.FrameDataLocation.ShouldBe(FrameDataLocation.Hardware);
}
}

174
ErsatzTV.FFmpeg.Tests/Pipeline/QsvPipelineBuilderTests.cs

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.InputOption;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.Preset;
using ErsatzTV.FFmpeg.State;
using LanguageExt;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using static LanguageExt.Prelude;
namespace ErsatzTV.FFmpeg.Tests.Pipeline;
[TestFixture]
public class QsvPipelineBuilderTests
{
private readonly ILogger _logger = Substitute.For<ILogger>();
[Test]
public void Pad_Uses_Hardware_Vpp_Qsv_When_Pad_Option_Available()
{
string command = BuildPadCommand(vppQsvSupportsPad: true);
// hardware pad: vpp_qsv does the padding, with no software pad and no hwdownload round-trip around it
command.ShouldContain("vpp_qsv=pad_w=1920:pad_h=1080:pad_x=-1:pad_y=-1:pad_color=black");
command.ShouldNotContain("pad=1920:1080");
command.ShouldNotContain("hwdownload,format=nv12,pad");
}
[Test]
public void Pad_Falls_Back_To_Software_When_Pad_Option_Unavailable()
{
string command = BuildPadCommand(vppQsvSupportsPad: false);
// software fallback: download to system memory, pad, then continue
command.ShouldNotContain("vpp_qsv=pad_w");
command.ShouldContain("hwdownload,format=nv12,pad=1920:1080:-1:-1:color=black");
}
private string BuildPadCommand(bool vppQsvSupportsPad)
{
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{
new(
0,
VideoFormat.H264,
VideoProfile.Main,
new PixelFormatYuv420P(),
ColorParams.Default,
new FrameSize(1440, 1080),
"1:1",
"4:3",
FrameRate.DefaultFrameRate,
false,
ScanKind.Progressive)
});
var desiredState = new FrameState(
true,
false,
VideoFormat.Hevc,
VideoProfile.Main,
VideoPreset.Unset,
false,
new PixelFormatNv12("yuv420p"),
new FrameSize(1440, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
2000,
4000,
90_000,
false,
false);
var ffmpegState = new FFmpegState(
false,
HardwareAccelerationMode.Qsv,
HardwareAccelerationMode.Qsv,
Option<string>.None,
Option<string>.None,
TimeSpan.FromSeconds(1),
Option<TimeSpan>.None,
false,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
OutputFormatKind.MpegTs,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,
false,
"clip",
false);
var hardwareCapabilities = Substitute.For<IHardwareCapabilities>();
hardwareCapabilities.CanDecode(
Arg.Any<string>(),
Arg.Any<Option<string>>(),
Arg.Any<Option<IPixelFormat>>(),
Arg.Any<ColorParams>())
.Returns(FFmpegCapability.Hardware);
hardwareCapabilities.CanEncode(
Arg.Any<string>(),
Arg.Any<Option<string>>(),
Arg.Any<Option<IPixelFormat>>())
.Returns(FFmpegCapability.Hardware);
hardwareCapabilities.GetRateControlMode(Arg.Any<string>(), Arg.Any<Option<IPixelFormat>>())
.Returns(Option<RateControlMode>.None);
var vppQsvOptions = new System.Collections.Generic.HashSet<string> { "w", "h", "format", "deinterlace" };
if (vppQsvSupportsPad)
{
vppQsvOptions.UnionWith(["pad_w", "pad_h", "pad_x", "pad_y", "pad_color"]);
}
var ffmpegCapabilities = new FFmpegCapabilities(
string.Empty,
new System.Collections.Generic.HashSet<string> { FFmpegKnownHardwareAcceleration.Qsv.Name },
new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.Dictionary<string, IReadOnlySet<string>>
{
[FFmpegKnownFilter.VppQsv.Name] = vppQsvOptions
});
var builder = new QsvPipelineBuilder(
ffmpegCapabilities,
hardwareCapabilities,
HardwareAccelerationMode.Qsv,
videoInputFile,
Option<AudioInputFile>.None,
Option<WatermarkInputFile>.None,
Option<SubtitleInputFile>.None,
None,
Option<GraphicsEngineInput>.None,
"",
"",
_logger);
FFmpegPipeline result = builder.Build(ffmpegState, desiredState);
IList<string> arguments = CommandGenerator.GenerateArguments(
videoInputFile,
Option<AudioInputFile>.None,
Option<WatermarkInputFile>.None,
Option<ConcatInputFile>.None,
Option<GraphicsEngineInput>.None,
result.PipelineSteps,
result.IsIntelVaapiOrQsv);
return string.Join(" ", arguments);
}
}

3
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -562,5 +562,6 @@ public class PipelineBuilderBaseTests
new System.Collections.Generic.HashSet<string>(), new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>(), new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>(), new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.HashSet<string>()); new System.Collections.Generic.HashSet<string>(),
new System.Collections.Generic.Dictionary<string, IReadOnlySet<string>>());
} }

6
ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs

@ -10,7 +10,8 @@ public class FFmpegCapabilities(
IReadOnlySet<string> ffmpegFilters, IReadOnlySet<string> ffmpegFilters,
IReadOnlySet<string> ffmpegEncoders, IReadOnlySet<string> ffmpegEncoders,
IReadOnlySet<string> ffmpegOptions, IReadOnlySet<string> ffmpegOptions,
IReadOnlySet<string> ffmpegDemuxFormats) IReadOnlySet<string> ffmpegDemuxFormats,
IReadOnlyDictionary<string, IReadOnlySet<string>> ffmpegFilterOptions)
: IFFmpegCapabilities : IFFmpegCapabilities
{ {
public string Version => ffmpegVersion; public string Version => ffmpegVersion;
@ -59,6 +60,9 @@ public class FFmpegCapabilities(
public bool HasFilter(FFmpegKnownFilter filter) => ffmpegFilters.Contains(filter.Name); public bool HasFilter(FFmpegKnownFilter filter) => ffmpegFilters.Contains(filter.Name);
public bool HasFilterOption(FFmpegKnownFilter filter, string optionName) =>
ffmpegFilterOptions.TryGetValue(filter.Name, out IReadOnlySet<string>? options) && options.Contains(optionName);
public bool HasOption(FFmpegKnownOption ffmpegOption) => ffmpegOptions.Contains(ffmpegOption.Name); public bool HasOption(FFmpegKnownOption ffmpegOption) => ffmpegOptions.Contains(ffmpegOption.Name);
public bool HasDemuxFormat(FFmpegKnownFormat format) => ffmpegDemuxFormats.Contains(format.Name); public bool HasDemuxFormat(FFmpegKnownFormat format) => ffmpegDemuxFormats.Contains(format.Name);

21
ErsatzTV.FFmpeg/Capabilities/FFmpegFilterOptionParser.cs

@ -0,0 +1,21 @@
using System.Collections.Immutable;
using System.Text.RegularExpressions;
namespace ErsatzTV.FFmpeg.Capabilities;
public static partial class FFmpegFilterOptionParser
{
public static IReadOnlySet<string> Parse(string filterHelpOutput) =>
filterHelpOutput.Split("\n").Map(s => s.Trim())
.Bind(l => ParseLine(l))
.ToImmutableHashSet();
private static Option<string> ParseLine(string input)
{
Match match = FilterOptionRegex().Match(input);
return match.Success ? match.Groups[1].Value : Option<string>.None;
}
[GeneratedRegex(@"^([\w-]+)\s+<")]
private static partial Regex FilterOptionRegex();
}

1
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs

@ -24,6 +24,7 @@ public record FFmpegKnownFilter
public static readonly FFmpegKnownFilter Scale = new("scale"); public static readonly FFmpegKnownFilter Scale = new("scale");
public static readonly FFmpegKnownFilter Subtitles = new("subtitles"); public static readonly FFmpegKnownFilter Subtitles = new("subtitles");
public static readonly FFmpegKnownFilter Tonemap = new("tonemap"); public static readonly FFmpegKnownFilter Tonemap = new("tonemap");
public static readonly FFmpegKnownFilter VppQsv = new("vpp_qsv");
public static readonly FFmpegKnownFilter Yadif = new("yadif"); public static readonly FFmpegKnownFilter Yadif = new("yadif");
public static readonly FFmpegKnownFilter ZScale = new("zscale"); public static readonly FFmpegKnownFilter ZScale = new("zscale");

45
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -36,6 +36,9 @@ public partial class HardwareCapabilitiesFactory(
private static readonly CompositeFormat QsvCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.qsv.{0}"); private static readonly CompositeFormat QsvCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.qsv.{0}");
private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}"); private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}");
private static readonly CompositeFormat FFmpegFilterOptionsCacheKeyFormat =
CompositeFormat.Parse("ffmpeg.filter.{0}");
private static readonly string[] QsvArguments = private static readonly string[] QsvArguments =
{ {
"-f", "lavfi", "-f", "lavfi",
@ -54,6 +57,8 @@ public partial class HardwareCapabilitiesFactory(
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "encoders")); memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "encoders"));
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options")); memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options"));
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats")); memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats"));
memoryCache.Remove(
string.Format(CultureInfo.InvariantCulture, FFmpegFilterOptionsCacheKeyFormat, FFmpegKnownFilter.VppQsv.Name));
} }
public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath, CancellationToken cancellationToken) public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath, CancellationToken cancellationToken)
@ -87,6 +92,13 @@ public partial class HardwareCapabilitiesFactory(
IReadOnlySet<string> ffmpegDemuxFormats = await GetFFmpegFormats(ffmpegPath, "D", cancellationToken) IReadOnlySet<string> ffmpegDemuxFormats = await GetFFmpegFormats(ffmpegPath, "D", cancellationToken)
.Map(set => set.Intersect(FFmpegKnownFormat.AllFormats).ToImmutableHashSet()); .Map(set => set.Intersect(FFmpegKnownFormat.AllFormats).ToImmutableHashSet());
IReadOnlySet<string> vppQsvOptions =
await GetFFmpegFilterOptions(ffmpegPath, FFmpegKnownFilter.VppQsv.Name, cancellationToken);
var ffmpegFilterOptions = new Dictionary<string, IReadOnlySet<string>>
{
[FFmpegKnownFilter.VppQsv.Name] = vppQsvOptions
};
return new FFmpegCapabilities( return new FFmpegCapabilities(
ffmpegVersion, ffmpegVersion,
ffmpegHardwareAccelerations, ffmpegHardwareAccelerations,
@ -94,7 +106,8 @@ public partial class HardwareCapabilitiesFactory(
ffmpegFilters, ffmpegFilters,
ffmpegEncoders, ffmpegEncoders,
ffmpegOptions, ffmpegOptions,
ffmpegDemuxFormats); ffmpegDemuxFormats,
ffmpegFilterOptions);
} }
public async Task<IHardwareCapabilities> GetHardwareCapabilities( public async Task<IHardwareCapabilities> GetHardwareCapabilities(
@ -438,6 +451,36 @@ public partial class HardwareCapabilitiesFactory(
return capabilitiesResult; return capabilitiesResult;
} }
private async Task<IReadOnlySet<string>> GetFFmpegFilterOptions(
string ffmpegPath,
string filterName,
CancellationToken cancellationToken)
{
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegFilterOptionsCacheKeyFormat, filterName);
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
cachedCapabilities is not null)
{
return cachedCapabilities;
}
string[] arguments = ["-hide_banner", "-h", $"filter={filterName}"];
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
? result.StandardError
: result.StandardOutput;
IReadOnlySet<string> capabilitiesResult = FFmpegFilterOptionParser.Parse(output);
memoryCache.Set(cacheKey, capabilitiesResult);
return capabilitiesResult;
}
private async Task<IReadOnlySet<string>> GetFFmpegFormats( private async Task<IReadOnlySet<string>> GetFFmpegFormats(
string ffmpegPath, string ffmpegPath,
string muxDemux, string muxDemux,

1
ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs

@ -9,6 +9,7 @@ public interface IFFmpegCapabilities
bool HasDecoder(FFmpegKnownDecoder decoder); bool HasDecoder(FFmpegKnownDecoder decoder);
bool HasEncoder(FFmpegKnownEncoder encoder); bool HasEncoder(FFmpegKnownEncoder encoder);
bool HasFilter(FFmpegKnownFilter filter); bool HasFilter(FFmpegKnownFilter filter);
bool HasFilterOption(FFmpegKnownFilter filter, string optionName);
bool HasOption(FFmpegKnownOption ffmpegOption); bool HasOption(FFmpegKnownOption ffmpegOption);
bool HasDemuxFormat(FFmpegKnownFormat format); bool HasDemuxFormat(FFmpegKnownFormat format);
Option<IDecoder> SoftwareDecoderForVideoFormat(string videoFormat); Option<IDecoder> SoftwareDecoderForVideoFormat(string videoFormat);

40
ErsatzTV.FFmpeg/Filter/Qsv/PadQsvFilter.cs

@ -0,0 +1,40 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter.Qsv;
public class PadQsvFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly FrameSize _paddedSize;
private readonly int _extraHardwareFrames;
public PadQsvFilter(FrameState currentState, FrameSize paddedSize, int extraHardwareFrames)
{
_currentState = currentState;
_paddedSize = paddedSize;
_extraHardwareFrames = extraHardwareFrames;
}
public override string Filter
{
get
{
var pad =
$"vpp_qsv=pad_w={_paddedSize.Width}:pad_h={_paddedSize.Height}:pad_x=-1:pad_y=-1:pad_color=black";
if (_currentState.FrameDataLocation == FrameDataLocation.Hardware)
{
return pad;
}
string initialPixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, FFmpegFormat.NV12);
return $"format={initialPixelFormat},hwupload=extra_hw_frames={_extraHardwareFrames},{pad}";
}
}
public override FrameState NextState(FrameState currentState) => currentState with
{
PaddedSize = _paddedSize,
FrameDataLocation = FrameDataLocation.Hardware
};
}

33
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -18,6 +18,7 @@ namespace ErsatzTV.FFmpeg.Pipeline;
public class QsvPipelineBuilder : SoftwarePipelineBuilder public class QsvPipelineBuilder : SoftwarePipelineBuilder
{ {
private readonly IFFmpegCapabilities _ffmpegCapabilities;
private readonly IHardwareCapabilities _hardwareCapabilities; private readonly IHardwareCapabilities _hardwareCapabilities;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -46,6 +47,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
fontsFolder, fontsFolder,
logger) logger)
{ {
_ffmpegCapabilities = ffmpegCapabilities;
_hardwareCapabilities = hardwareCapabilities; _hardwareCapabilities = hardwareCapabilities;
_logger = logger; _logger = logger;
} }
@ -194,8 +196,9 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
// _logger.LogDebug("After deinterlace: {PixelFormat}", currentState.PixelFormat); // _logger.LogDebug("After deinterlace: {PixelFormat}", currentState.PixelFormat);
currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState); currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState);
// _logger.LogDebug("After scale: {PixelFormat}", currentState.PixelFormat); // _logger.LogDebug("After scale: {PixelFormat}", currentState.PixelFormat);
bool isHdrTonemap = videoStream.ColorParams.IsHdr;
currentState = SetTonemap(videoInputFile, videoStream, ffmpegState, desiredState, currentState); currentState = SetTonemap(videoInputFile, videoStream, ffmpegState, desiredState, currentState);
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); currentState = SetPad(videoInputFile, videoStream, ffmpegState, desiredState, currentState, isHdrTonemap);
// _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat); // _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat);
currentState = SetCrop(videoInputFile, desiredState, currentState); currentState = SetCrop(videoInputFile, desiredState, currentState);
SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps); SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps);
@ -311,7 +314,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
bool usesVppQsv = bool usesVppQsv =
videoInputFile.FilterSteps.Any(f => videoInputFile.FilterSteps.Any(f =>
f is QsvFormatFilter or ScaleQsvFilter or DeinterlaceQsvFilter or TonemapQsvFilter); f is QsvFormatFilter or ScaleQsvFilter or DeinterlaceQsvFilter or TonemapQsvFilter or PadQsvFilter);
// if we have no filters, check whether we need to convert pixel format // if we have no filters, check whether we need to convert pixel format
// since qsv doesn't seem to like doing that at the encoder // since qsv doesn't seem to like doing that at the encoder
@ -598,17 +601,33 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
} }
} }
private static FrameState SetPad( private FrameState SetPad(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
FrameState currentState) FrameState currentState,
bool isHdrTonemap)
{ {
if (desiredState.CroppedSize.IsNone && currentState.PaddedSize != desiredState.PaddedSize) if (desiredState.CroppedSize.IsNone && currentState.PaddedSize != desiredState.PaddedSize)
{ {
var padStep = new PadFilter(currentState, desiredState.PaddedSize); if (desiredState.PadMode is FFmpegFilterMode.Software
currentState = padStep.NextState(currentState); || isHdrTonemap
videoInputFile.FilterSteps.Add(padStep); || !_ffmpegCapabilities.HasFilterOption(FFmpegKnownFilter.VppQsv, "pad_w"))
{
var padStep = new PadFilter(currentState, desiredState.PaddedSize);
currentState = padStep.NextState(currentState);
videoInputFile.FilterSteps.Add(padStep);
}
else
{
var padStep = new PadQsvFilter(
currentState,
desiredState.PaddedSize,
ffmpegState.QsvExtraHardwareFrames);
currentState = padStep.NextState(currentState);
videoInputFile.FilterSteps.Add(padStep);
}
} }
return currentState; return currentState;

Loading…
Cancel
Save