Browse Source

nvidia improvements (#628)

pull/629/head
Jason Dove 4 years ago committed by GitHub
parent
commit
87deaa6f3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 207
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  3. 49
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  4. 203
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  5. 18
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  6. 3
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

3
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 - 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 - 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 bug with unknown pixel format
- Fix playback of interlaced mpeg2video on NVIDIA
### Added ### Added
- Include `Series` category tag for all episodes in XMLTV - 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 ### Changed
- Intermittent watermarks will now fade in and out - Intermittent watermarks will now fade in and out
- Show collection name in some playout build error messages - 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 ## [0.4.0-alpha] - 2022-01-29
### Fixed ### Fixed

207
ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs

@ -304,6 +304,213 @@ namespace ErsatzTV.Core.Tests.FFmpeg
filter.VideoLabel.Should().Be(expectedVideoLabel); 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<List<FadePoint>> 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<FFmpegComplexFilter> 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] [Test]
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")] [TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]

49
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -37,8 +37,26 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Assert.Pass(); Assert.Pass();
} }
public enum Padding
{
NoPadding,
WithPadding
}
private class TestData private class TestData
{ {
public static Padding[] Paddings =
{
Padding.NoPadding,
Padding.WithPadding
};
public static VideoScanKind[] VideoScanKinds =
{
VideoScanKind.Progressive,
VideoScanKind.Interlaced
};
public static string[] InputCodecs = public static string[] InputCodecs =
{ {
"h264", "h264",
@ -51,9 +69,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
{ {
"yuv420p", "yuv420p",
"yuv420p10le", "yuv420p10le",
"yuvj420p", // "yuvj420p",
"yuv444p", // "yuv444p",
"yuv444p10le" // "yuv444p10le"
}; };
public static Resolution[] Resolutions = public static Resolution[] Resolutions =
@ -115,27 +133,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg
string inputPixelFormat, string inputPixelFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))] [ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution, Resolution profileResolution,
[Values(true, false)] [ValueSource(typeof(TestData), nameof(TestData.Paddings))]
bool pad, Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
VideoScanKind videoScanKind,
// [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec, [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec,
[ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
{ {
string name = GetStringSha256Hash( 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"); string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file)) 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 = string 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}"; $"-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 var p1 = new Process
{ {
StartInfo = new ProcessStartInfo StartInfo = new ProcessStartInfo

203
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -25,6 +25,7 @@ namespace ErsatzTV.Core.FFmpeg
private Option<List<FadePoint>> _maybeFadePoints = None; private Option<List<FadePoint>> _maybeFadePoints = None;
private Option<int> _watermarkIndex; private Option<int> _watermarkIndex;
private string _pixelFormat; private string _pixelFormat;
private string _videoDecoder;
private string _videoEncoder; private string _videoEncoder;
private Option<string> _subtitle; private Option<string> _subtitle;
private bool _boxBlur; private bool _boxBlur;
@ -75,6 +76,12 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegComplexFilterBuilder WithDecoder(string decoder)
{
_videoDecoder = decoder;
return this;
}
public FFmpegComplexFilterBuilder WithInputPixelFormat(Option<string> maybePixelFormat) public FFmpegComplexFilterBuilder WithInputPixelFormat(Option<string> maybePixelFormat)
{ {
foreach (string pixelFormat in maybePixelFormat) foreach (string pixelFormat in maybePixelFormat)
@ -145,20 +152,32 @@ namespace ErsatzTV.Core.FFmpeg
string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a"); string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a");
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None); HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
bool isHardwareDecode = acceleration switch bool isHardwareDecode = acceleration switch
{ {
HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4", HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4",
// we need an initial hwupload_cuda when only padding with these pixel formats // we need an initial hwupload_cuda when only padding with these pixel formats
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome => HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome =>
!isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"), !isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"),
HardwareAccelerationKind.Nvenc => !isSong, HardwareAccelerationKind.Nvenc => !isSong &&
(string.IsNullOrWhiteSpace(_videoDecoder) ||
_videoDecoder.Contains("cuvid")),
HardwareAccelerationKind.Qsv => !isSong, HardwareAccelerationKind.Qsv => !isSong,
HardwareAccelerationKind.VideoToolbox => false, HardwareAccelerationKind.VideoToolbox => false,
_ => 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<string>(); var audioFilterQueue = new List<string>();
var videoFilterQueue = new List<string>(); var videoFilterQueue = new List<string>();
var watermarkPreprocess = new List<string>(); var watermarkPreprocess = new List<string>();
@ -215,15 +234,17 @@ namespace ErsatzTV.Core.FFmpeg
if (_deinterlace) if (_deinterlace)
{ {
string filter = acceleration switch Option<string> maybeFilter = acceleration switch
{ {
HardwareAccelerationKind.Qsv => "deinterlace_qsv", HardwareAccelerationKind.Qsv => "deinterlace_qsv",
HardwareAccelerationKind.Nvenc when !usesHardwareFilters && _pixelFormat.Contains("p10le") =>
"hwupload_cuda,yadif_cuda",
HardwareAccelerationKind.Nvenc => "yadif_cuda", HardwareAccelerationKind.Nvenc => "yadif_cuda",
HardwareAccelerationKind.Vaapi => "deinterlace_vaapi", HardwareAccelerationKind.Vaapi => "deinterlace_vaapi",
_ => "yadif=1" _ => "yadif=1"
}; };
if (!string.IsNullOrWhiteSpace(filter)) foreach (string filter in maybeFilter)
{ {
videoFilterQueue.Add(filter); videoFilterQueue.Add(filter);
} }
@ -243,67 +264,64 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add("format=nv12|vaapi,hwupload"); 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 scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome;
bool usesSoftwareFilters = _padToSize.IsSome || _watermark.IsSome; bool usesSoftwareFilters = _padToSize.IsSome || _watermark.IsSome;
bool hasFadePoints = _maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0;
if (scaleOrPad && _boxBlur == false) var softwareFilterQueue = new List<string>();
{
videoFilterQueue.Add("setsar=1");
}
if (usesSoftwareFilters) if (usesSoftwareFilters)
{ {
if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters)) if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters))
{ {
videoFilterQueue.Add("hwdownload"); Option<string> maybeFormat = acceleration switch
string format = acceleration switch
{ {
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi", HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
HardwareAccelerationKind.Nvenc when _padToSize.IsNone || nvencDeinterlace => None,
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" => HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
"format=p010le,format=nv12", "format=p010le,format=nv12",
HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p", HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p",
_ when isSong => "format=yuv420p", _ when isSong => "format=yuv420p",
_ => "format=nv12" _ => "format=nv12"
}; };
videoFilterQueue.Add(format);
foreach (string format in maybeFormat)
{
softwareFilterQueue.Add("hwdownload");
softwareFilterQueue.Add(format);
}
if (nvencDeinterlace)
{
softwareFilterQueue.Add("hwdownload");
}
} }
if (_boxBlur) if (_boxBlur)
{ {
videoFilterQueue.Add("boxblur=40"); softwareFilterQueue.Add("boxblur=40");
} }
if (videoOnly) if (videoOnly)
{ {
videoFilterQueue.Add("deband"); softwareFilterQueue.Add("deband");
} }
foreach (ChannelWatermark watermark in _watermark) foreach (ChannelWatermark watermark in _watermark)
{ {
if (watermark.Opacity != 100 || _maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0) Option<string> maybeFormats = acceleration switch
{
// 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)
{ {
const string FORMATS = "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8"; watermarkPreprocess.Add($"format={formats}");
watermarkPreprocess.Add($"format={FORMATS}");
} }
double horizontalMargin = Math.Round(watermark.HorizontalMarginPercent / 100.0 * _resolution.Width); 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())); 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 watermarkOverlay += "," + acceleration switch
{ {
@ -351,15 +378,93 @@ namespace ErsatzTV.Core.FFmpeg
} }
} }
} }
string outputPixelFormat = null;
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
{
switch (acceleration, _videoEncoder, _pixelFormat)
{
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv420p10le"):
outputPixelFormat = "yuv420p";
break;
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv444p10le"):
outputPixelFormat = "yuv444p";
break;
}
}
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")); _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) foreach (string subtitle in _subtitle)
{ {
videoFilterQueue.Add(subtitle); videoFilterQueue.Add(subtitle);
} }
string outputPixelFormat = null;
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None && if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None &&
string.IsNullOrWhiteSpace(watermarkOverlay)) string.IsNullOrWhiteSpace(watermarkOverlay))
@ -372,19 +477,6 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add(upload); videoFilterQueue.Add(upload);
} }
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
{
switch (acceleration, _videoEncoder, _pixelFormat)
{
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv420p10le"):
outputPixelFormat = "yuv420p";
break;
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv444p10le"):
outputPixelFormat = "yuv444p";
break;
}
}
bool hasAudioFilters = audioFilterQueue.Any(); bool hasAudioFilters = audioFilterQueue.Any();
if (hasAudioFilters) if (hasAudioFilters)
{ {
@ -447,6 +539,9 @@ namespace ErsatzTV.Core.FFmpeg
case (true, HardwareAccelerationKind.Nvenc): case (true, HardwareAccelerationKind.Nvenc):
complexFilter.Append(",hwupload_cuda"); complexFilter.Append(",hwupload_cuda");
break; 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): case (_, HardwareAccelerationKind.Qsv):
complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64"); complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64");
break; break;

18
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -286,7 +286,8 @@ namespace ErsatzTV.Core.FFmpeg
string audioPath, string audioPath,
string decoder, string decoder,
Option<string> codec, Option<string> codec,
Option<string> pixelFormat) Option<string> pixelFormat,
bool deinterlace)
{ {
if (audioPath == videoPath) if (audioPath == videoPath)
{ {
@ -306,6 +307,15 @@ namespace ErsatzTV.Core.FFmpeg
{ {
_arguments.Add("-c:v"); _arguments.Add("-c:v");
_arguments.Add(decoder); _arguments.Add(decoder);
if (decoder == "mpeg2_cuvid" && deinterlace)
{
_arguments.Add("-deint");
_arguments.Add("2");
}
_complexFilterBuilder = _complexFilterBuilder
.WithDecoder(decoder);
} }
_complexFilterBuilder = _complexFilterBuilder _complexFilterBuilder = _complexFilterBuilder
@ -633,14 +643,14 @@ namespace ErsatzTV.Core.FFmpeg
} }
}); });
_arguments.Add("-map");
_arguments.Add(videoLabel);
foreach (string _ in audioPath) foreach (string _ in audioPath)
{ {
_arguments.Add("-map"); _arguments.Add("-map");
_arguments.Add(audioLabel); _arguments.Add(audioLabel);
} }
_arguments.Add("-map");
_arguments.Add(videoLabel);
return this; return this;
} }

3
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -114,7 +114,8 @@ namespace ErsatzTV.Core.FFmpeg
audioPath, audioPath,
playbackSettings.VideoDecoder, playbackSettings.VideoDecoder,
videoStream.Codec, videoStream.Codec,
videoStream.PixelFormat) videoStream.PixelFormat,
playbackSettings.Deinterlace)
.WithWatermark(watermarkOptions, maybeFadePoints, channel.FFmpegProfile.Resolution) .WithWatermark(watermarkOptions, maybeFadePoints, channel.FFmpegProfile.Resolution)
.WithFrameRate(playbackSettings.FrameRate) .WithFrameRate(playbackSettings.FrameRate)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)

Loading…
Cancel
Save