From f739199b67216bab3312d9ce77967e428ddad182 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sat, 6 Mar 2021 12:56:29 -0600 Subject: [PATCH] lots of progress with nvenc, qsv and vaapi remain untested --- .../FFmpeg/FFmpegComplexFilterBuilderTests.cs | 340 ++++++++++++++++++ ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs | 4 + .../FFmpeg/FFmpegComplexFilterBuilder.cs | 144 ++++++++ ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs | 96 +---- ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs | 4 +- ErsatzTV/Pages/FFmpegEditor.razor | 2 +- .../FFmpegProfileEditViewModelValidator.cs | 4 +- .../ViewModels/FFmpegProfileEditViewModel.cs | 17 +- docker-compose.yml | 29 -- docker/Dockerfile | 40 +++ docker/docker-compose.nvidia.yml | 15 + docker/docker-compose.vaapi.yml | 8 + docker/docker-compose.yml | 16 + docker/nvidia/Dockerfile | 40 +++ Dockerfile => docker/vaapi/Dockerfile | 7 +- 15 files changed, 634 insertions(+), 132 deletions(-) create mode 100644 ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs create mode 100644 ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs create mode 100644 ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs delete mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.nvidia.yml create mode 100644 docker/docker-compose.vaapi.yml create mode 100644 docker/docker-compose.yml create mode 100644 docker/nvidia/Dockerfile rename Dockerfile => docker/vaapi/Dockerfile (85%) diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs new file mode 100644 index 000000000..015099c13 --- /dev/null +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs @@ -0,0 +1,340 @@ +using System; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.FFmpeg; +using FluentAssertions; +using LanguageExt; +using NUnit.Framework; + +namespace ErsatzTV.Core.Tests.FFmpeg +{ + [TestFixture] + public class FFmpegComplexFilterBuilderTests + { + [TestFixture] + public class Build + { + [Test] + public void Should_Return_None_With_No_Filters() + { + var builder = new FFmpegComplexFilterBuilder(); + + Option result = builder.Build(); + + result.IsNone.Should().BeTrue(); + } + + [Test] + public void Should_Return_Audio_Filter_With_AudioDuration() + { + var duration = TimeSpan.FromMinutes(54); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithAlignedAudio(duration); + + Option result = builder.Build(); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be($"[0:a]apad=whole_dur={duration.TotalMilliseconds}ms[a]"); + filter.AudioLabel.Should().Be("[a]"); + filter.VideoLabel.Should().Be("0:v"); + }); + } + + [Test] + public void Should_Return_Audio_And_Video_Filter() + { + var duration = TimeSpan.FromMinutes(54); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithAlignedAudio(duration) + .WithDeinterlace(true); + + Option result = builder.Build(); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be( + $"[0:a]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:v]yadif=1[v]"); + filter.AudioLabel.Should().Be("[a]"); + filter.VideoLabel.Should().Be("[v]"); + }); + } + + [Test] + [TestCase(true, false, false, "[0:v]yadif=1[v]", "[v]")] + [TestCase(true, true, false, "[0:v]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] + [TestCase(true, false, true, "[0:v]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] + [TestCase( + true, + true, + true, + "[0:v]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", + "[v]")] + [TestCase(false, true, false, "[0:v]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] + [TestCase(false, false, true, "[0:v]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] + [TestCase( + false, + true, + true, + "[0:v]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", + "[v]")] + public void Should_Return_Software_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithDeinterlace(deinterlace); + + if (scale) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); + } + + if (pad) + { + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:a"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + [TestCase(true, false, false, "[0:v]deinterlace_qsv[v]", "[v]")] + [TestCase( + true, + true, + false, + "[0:v]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,setsar=1,hwupload[v]", + "[v]")] + [TestCase( + true, + false, + true, + "[0:v]deinterlace_qsv,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + true, + true, + true, + "[0:v]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase(false, true, false, "[0:v]scale_qsv=w=1920:h=1000,hwdownload,setsar=1,hwupload[v]", "[v]")] + [TestCase( + false, + false, + true, + "[0:v]hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + false, + true, + true, + "[0:v]scale_qsv=w=1920:h=1000,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + public void Should_Return_QSV_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Qsv) + .WithDeinterlace(deinterlace); + + if (scale) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); + } + + if (pad) + { + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:a"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + // TODO: get yadif_cuda working in docker + // [TestCase(true, false, false, "[0:v]yadif_cuda[v]", "[v]")] + // [TestCase( + // true, + // true, + // false, + // "[0:v]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]", + // "[v]")] + // [TestCase( + // true, + // false, + // true, + // "[0:v]yadif_cuda,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + // "[v]")] + // [TestCase( + // true, + // true, + // true, + // "[0:v]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + // "[v]")] + [TestCase(true, false, false, "[0:v][v]", "[v]")] + [TestCase( + true, + true, + false, + "[0:v]scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]", + "[v]")] + [TestCase( + true, + false, + true, + "[0:v]hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + true, + true, + true, + "[0:v]scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + false, + true, + false, + "[0:v]scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]", + "[v]")] + [TestCase( + false, + false, + true, + "[0:v]hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + false, + true, + true, + "[0:v]scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + public void Should_Return_NVENC_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) + .WithDeinterlace(deinterlace); + + if (scale) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); + } + + if (pad) + { + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:a"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + [TestCase(true, false, false, "[0:v]deinterlace_vaapi[v]", "[v]")] + [TestCase( + true, + true, + false, + "[0:v]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,setsar=1,hwupload[v]", + "[v]")] + [TestCase( + true, + false, + true, + "[0:v]deinterlace_vaapi,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + true, + true, + true, + "[0:v]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase(false, true, false, "[0:v]scale_vaapi=w=1920:h=1000,hwdownload,setsar=1,hwupload[v]", "[v]")] + [TestCase( + false, + false, + true, + "[0:v]hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + false, + true, + true, + "[0:v]scale_vaapi=w=1920:h=1000,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + public void Should_Return_VAAPI_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Vaapi) + .WithDeinterlace(deinterlace); + + if (scale) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); + } + + if (pad) + { + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:a"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + } + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs new file mode 100644 index 000000000..45b60f887 --- /dev/null +++ b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Core.FFmpeg +{ + public record FFmpegComplexFilter(string ComplexFilter, string VideoLabel, string AudioLabel); +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs new file mode 100644 index 000000000..0b6a36b1f --- /dev/null +++ b/ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.FFmpeg; +using LanguageExt; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Core.FFmpeg +{ + public class FFmpegComplexFilterBuilder + { + private Option _audioDuration = None; + private bool _deinterlace; + private Option _hardwareAccelerationKind = None; + private Option _padToSize = None; + private Option _scaleToSize = None; + + public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind) + { + _hardwareAccelerationKind = Some(hardwareAccelerationKind); + return this; + } + + public FFmpegComplexFilterBuilder WithScaling(IDisplaySize scaleToSize) + { + _scaleToSize = Some(scaleToSize); + return this; + } + + public FFmpegComplexFilterBuilder WithBlackBars(IDisplaySize padToSize) + { + _padToSize = Some(padToSize); + return this; + } + + public FFmpegComplexFilterBuilder WithDeinterlace(bool deinterlace) + { + _deinterlace = deinterlace; + return this; + } + + public FFmpegComplexFilterBuilder WithAlignedAudio(Option audioDuration) + { + _audioDuration = audioDuration; + return this; + } + + public Option Build() + { + var complexFilter = new StringBuilder(); + + var videoLabel = "0:v"; + var audioLabel = "0:a"; + + HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None); + + _audioDuration.IfSome( + audioDuration => + { + complexFilter.Append($"[{audioLabel}]"); + complexFilter.Append($"apad=whole_dur={audioDuration.TotalMilliseconds}ms"); + audioLabel = "[a]"; + complexFilter.Append(audioLabel); + }); + + var filterQueue = new List(); + + if (_deinterlace) + { + string filter = acceleration switch + { + HardwareAccelerationKind.Qsv => "deinterlace_qsv", + HardwareAccelerationKind.Nvenc => "", // TODO: yadif_cuda support in docker + HardwareAccelerationKind.Vaapi => "deinterlace_vaapi", + _ => "yadif=1" + }; + + if (!string.IsNullOrWhiteSpace(filter)) + { + filterQueue.Add(filter); + } + } + + _scaleToSize.IfSome( + size => + { + string filter = acceleration switch + { + HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", + HardwareAccelerationKind.Nvenc => $"scale_npp={size.Width}:{size.Height}:format=yuv420p", + HardwareAccelerationKind.Vaapi => $"scale_vaapi=w={size.Width}:h={size.Height}", + _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" + }; + + if (!string.IsNullOrWhiteSpace(filter)) + { + filterQueue.Add(filter); + } + }); + + if (_scaleToSize.IsSome || _padToSize.IsSome) + { + if (acceleration != HardwareAccelerationKind.None) + { + filterQueue.Add("hwdownload"); + if (_scaleToSize.IsNone && acceleration == HardwareAccelerationKind.Nvenc) + { + filterQueue.Add("format=nv12"); + } + } + + filterQueue.Add("setsar=1"); + } + + _padToSize.IfSome(size => filterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2")); + + if ((_scaleToSize.IsSome || _padToSize.IsSome) && acceleration != HardwareAccelerationKind.None) + { + filterQueue.Add("hwupload"); + } + + if (filterQueue.Any()) + { + // TODO: any audio filter + if (_audioDuration.IsSome) + { + complexFilter.Append(";"); + } + + complexFilter.Append($"[{videoLabel}]"); + complexFilter.Append(string.Join(",", filterQueue)); + videoLabel = "[v]"; + complexFilter.Append(videoLabel); + } + + var filterResult = complexFilter.ToString(); + return string.IsNullOrWhiteSpace(filterResult) + ? Option.None + : new FFmpegComplexFilter(filterResult, videoLabel, audioLabel); + } + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 4cbad9a3f..88e8b5039 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -21,7 +21,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; @@ -39,9 +38,8 @@ namespace ErsatzTV.Core.FFmpeg }; private readonly List _arguments = new(); - private readonly Queue _audioFilters = new(); private readonly string _ffmpegPath; - private readonly Queue _videoFilters = new(); + private FFmpegComplexFilterBuilder _complexFilterBuilder = new(); public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath; @@ -76,6 +74,8 @@ namespace ErsatzTV.Core.FFmpeg break; } + _complexFilterBuilder = _complexFilterBuilder.WithHardwareAcceleration(hwAccel); + return this; } @@ -289,85 +289,44 @@ namespace ErsatzTV.Core.FFmpeg return this; } - public FFmpegProcessBuilder WithScaling( - IDisplaySize displaySize, - HardwareAccelerationKind hwAccel, - string algorithm) + public FFmpegProcessBuilder WithScaling(IDisplaySize displaySize) { - _videoFilters.Enqueue( - hwAccel switch - { - HardwareAccelerationKind.Qsv => $"scale_qsv=w={displaySize.Width}:h={displaySize.Height}", - HardwareAccelerationKind.Nvenc => $"scale_cuda={displaySize.Width}:{displaySize.Height}", - HardwareAccelerationKind.Vaapi => - $"hwupload,scale_vaapi=w={displaySize.Width}:h={displaySize.Height}:format=nv12", - _ => $"scale={displaySize.Width}:{displaySize.Height}:flags={algorithm}" - }); - + _complexFilterBuilder = _complexFilterBuilder.WithScaling(displaySize); return this; } public FFmpegProcessBuilder WithBlackBars(IDisplaySize displaySize) { - _videoFilters.Enqueue($"pad={displaySize.Width}:{displaySize.Height}:(ow-iw)/2:(oh-ih)/2"); + _complexFilterBuilder = _complexFilterBuilder.WithBlackBars(displaySize); return this; } public FFmpegProcessBuilder WithAlignedAudio(Option audioDuration) { - audioDuration.IfSome(duration => _audioFilters.Enqueue($"apad=whole_dur={duration.TotalMilliseconds}ms")); + _complexFilterBuilder = _complexFilterBuilder.WithAlignedAudio(audioDuration); return this; } - public FFmpegProcessBuilder WithDeinterlace(bool deinterlace, string algorithm = "yadif=1") + public FFmpegProcessBuilder WithDeinterlace(bool deinterlace) { - if (deinterlace) - { - _videoFilters.Enqueue(algorithm); - } - - return this; - } - - public FFmpegProcessBuilder WithSAR() - { - // TODO: minsiz? - _videoFilters.Enqueue("setsar=1"); + _complexFilterBuilder = _complexFilterBuilder.WithDeinterlace(deinterlace); return this; } public FFmpegProcessBuilder WithFilterComplex() { - var complexFilter = new StringBuilder(); var videoLabel = "0:v"; var audioLabel = "0:a"; - bool hasVideoFilters = _videoFilters.Any(); - if (hasVideoFilters) - { - (string filter, string finalLabel) = GenerateVideoFilter(_videoFilters); - complexFilter.Append(filter); - videoLabel = finalLabel; - } - if (_audioFilters.Any()) - { - if (hasVideoFilters) + Option maybeFilter = _complexFilterBuilder.Build(); + maybeFilter.IfSome( + filter => { - complexFilter.Append(';'); - } - - (string filter, string finalLabel) = GenerateAudioFilter(_audioFilters); - complexFilter.Append(filter); - audioLabel = finalLabel; - } - - var complex = complexFilter.ToString(); - - if (!string.IsNullOrWhiteSpace(complex)) - { - _arguments.Add("-filter_complex"); - _arguments.Add(complex); - } + _arguments.Add("-filter_complex"); + _arguments.Add(filter.ComplexFilter); + videoLabel = filter.VideoLabel; + audioLabel = filter.AudioLabel; + }); _arguments.Add("-map"); _arguments.Add(videoLabel); @@ -406,26 +365,5 @@ namespace ErsatzTV.Core.FFmpeg StartInfo = startInfo }; } - - private FilterResult GenerateVideoFilter(Queue filterQueue) => - GenerateFilter(filterQueue, "null", 'v'); - - private FilterResult GenerateAudioFilter(Queue filterQueue) => - GenerateFilter(filterQueue, "anull", 'a'); - - private static FilterResult GenerateFilter(Queue filterQueue, string nullFilter, char av) - { - var filter = new StringBuilder(); - var index = 0; - filter.Append($"[0:{av}]{nullFilter}[{av}{index}]"); - while (filterQueue.TryDequeue(out string result)) - { - filter.Append($";[{av}{index}]{result}[{av}{++index}]"); - } - - return new FilterResult(filter.ToString(), $"[{av}{index}]"); - } - - private record FilterResult(string Filter, string FinalLabel); } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index d85d07b9b..d93b7fae3 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -40,8 +40,7 @@ namespace ErsatzTV.Core.FFmpeg scaledSize => { builder = builder.WithDeinterlace(playbackSettings.Deinterlace) - .WithScaling(scaledSize, playbackSettings.HardwareAcceleration, playbackSettings.ScalingAlgorithm) - .WithSAR(); + .WithScaling(scaledSize); scaledSize = scaledSize.PadToEven(); if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize)) @@ -58,7 +57,6 @@ namespace ErsatzTV.Core.FFmpeg { builder = builder .WithDeinterlace(playbackSettings.Deinterlace) - .WithSAR() .WithBlackBars(channel.FFmpegProfile.Resolution) .WithAlignedAudio(playbackSettings.AudioDuration) .WithFilterComplex(); diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index e359f8c66..825044302 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -79,7 +79,7 @@ Normalization - + diff --git a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs index 61becc454..7bf5fb22b 100644 --- a/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs +++ b/ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs @@ -45,8 +45,8 @@ namespace ErsatzTV.Validators RuleFor(x => x.VideoCodec).Must(c => NvencEncoders.Contains(c)) .WithMessage("NVENC codec is required (h264_nvenc, hevc_nvenc)"); - RuleFor(x => x.NormalizeResolution).Must(x => x == false) - .WithMessage("Resolution normalization (scaling) is not yet supported with NVENC"); + // RuleFor(x => x.NormalizeResolution).Must(x => x == false) + // .WithMessage("Resolution normalization (scaling) is not yet supported with NVENC"); }); When( diff --git a/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs b/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs index 5ac313e25..a18e082e9 100644 --- a/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs +++ b/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs @@ -7,8 +7,6 @@ namespace ErsatzTV.ViewModels { public class FFmpegProfileEditViewModel { - private HardwareAccelerationKind _hardwareAcceleration; - public FFmpegProfileEditViewModel() { } @@ -51,20 +49,7 @@ namespace ErsatzTV.ViewModels public ResolutionViewModel Resolution { get; set; } public int ThreadCount { get; set; } public bool Transcode { get; set; } - - public HardwareAccelerationKind HardwareAcceleration - { - get => _hardwareAcceleration; - set - { - _hardwareAcceleration = value; - if (_hardwareAcceleration == HardwareAccelerationKind.Nvenc) - { - NormalizeResolution = false; - } - } - } - + public HardwareAccelerationKind HardwareAcceleration { get; set; } public int VideoBitrate { get; set; } public int VideoBufferSize { get; set; } public string VideoCodec { get; set; } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a5f2ab3bd..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: "3.1" - -services: - ersatztv: - build: - context: . - args: - INFO_VERSION: "docker-compose-develop" - ports: - - "8409:8409" - volumes: - - ersatztv:/root/.local/share/ersatztv - #- /media/shared:/media/shared:ro - -# uncomment for vaapi support -# devices: -# - /dev/dri/renderD128:/dev/dri/renderD128 - -# uncomment for nvenc support -# environment: -# NVIDIA_VISIBLE_DEVICES: all -# NVIDIA_DRIVER_CAPABILITIES: all -# deploy: -# resources: -# reservations: -# devices: -# - capabilities: [ gpu ] -volumes: - ersatztv: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..f924b4cf3 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,40 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS dotnet-runtime + +FROM jrottenberg/ffmpeg:4.3-ubuntu2004 AS runtime-base +COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet +RUN apt-get update && apt-get install -y libicu-dev + +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +RUN apt-get update && apt-get install -y ca-certificates +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY *.sln . +COPY ErsatzTV/*.csproj ./ErsatzTV/ +COPY generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/*.csproj ./generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/ +COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/ +COPY ErsatzTV.CommandLine/*.csproj ./ErsatzTV.CommandLine/ +COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/ +COPY ErsatzTV.Core.Tests/*.csproj ./ErsatzTV.Core.Tests/ +COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/ +RUN dotnet restore -r linux-x64 + +# copy everything else and build app +COPY ErsatzTV/. ./ErsatzTV/ +COPY generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/. ./generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/ +COPY ErsatzTV.Application/. ./ErsatzTV.Application/ +COPY ErsatzTV.CommandLine/. ./ErsatzTV.CommandLine/ +COPY ErsatzTV.Core/. ./ErsatzTV.Core/ +COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/ +COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/ +WORKDIR /source/ErsatzTV +ARG INFO_VERSION="unknown" +RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:InformationalVersion=${INFO_VERSION} + +# final stage/image +FROM runtime-base +WORKDIR /app +EXPOSE 8409 +COPY --from=build /app ./ +ENTRYPOINT ["./ErsatzTV"] diff --git a/docker/docker-compose.nvidia.yml b/docker/docker-compose.nvidia.yml new file mode 100644 index 000000000..b44716dc7 --- /dev/null +++ b/docker/docker-compose.nvidia.yml @@ -0,0 +1,15 @@ +version: "3.1" + +services: + ersatztv: + build: + context: .. + dockerfile: docker/nvidia/Dockerfile + environment: + NVIDIA_VISIBLE_DEVICES: all + NVIDIA_DRIVER_CAPABILITIES: compute,utility,video + deploy: + resources: + reservations: + devices: + - capabilities: [ gpu ] diff --git a/docker/docker-compose.vaapi.yml b/docker/docker-compose.vaapi.yml new file mode 100644 index 000000000..a00c42a51 --- /dev/null +++ b/docker/docker-compose.vaapi.yml @@ -0,0 +1,8 @@ +version: "3.1" + +services: + ersatztv: + build: + dockerfile: docker/vaapi/Dockerfile + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..a1d8b5eaf --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.1" + +services: + ersatztv: + build: + context: .. + dockerfile: docker/Dockerfile + args: + INFO_VERSION: "docker-compose-develop" + ports: + - "8409:8409" + volumes: + - ersatztv:/root/.local/share/ersatztv + #- /media/shared:/media/shared:ro +volumes: + ersatztv: diff --git a/docker/nvidia/Dockerfile b/docker/nvidia/Dockerfile new file mode 100644 index 000000000..935e4d27f --- /dev/null +++ b/docker/nvidia/Dockerfile @@ -0,0 +1,40 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS dotnet-runtime + +FROM jrottenberg/ffmpeg:4.3-nvidia1804 AS runtime-base +COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet +RUN apt-get update && apt-get install -y libicu-dev + +# https://hub.docker.com/_/microsoft-dotnet +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +RUN apt-get update && apt-get install -y ca-certificates +WORKDIR /source + +# copy csproj and restore as distinct layers +COPY *.sln . +COPY ErsatzTV/*.csproj ./ErsatzTV/ +COPY generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/*.csproj ./generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/ +COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/ +COPY ErsatzTV.CommandLine/*.csproj ./ErsatzTV.CommandLine/ +COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/ +COPY ErsatzTV.Core.Tests/*.csproj ./ErsatzTV.Core.Tests/ +COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/ +RUN dotnet restore -r linux-x64 + +# copy everything else and build app +COPY ErsatzTV/. ./ErsatzTV/ +COPY generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/. ./generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/ +COPY ErsatzTV.Application/. ./ErsatzTV.Application/ +COPY ErsatzTV.CommandLine/. ./ErsatzTV.CommandLine/ +COPY ErsatzTV.Core/. ./ErsatzTV.Core/ +COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/ +COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/ +WORKDIR /source/ErsatzTV +ARG INFO_VERSION="unknown" +RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:InformationalVersion=${INFO_VERSION} + +# final stage/image +FROM runtime-base +WORKDIR /app +EXPOSE 8409 +COPY --from=build /app ./ +ENTRYPOINT ["./ErsatzTV"] diff --git a/Dockerfile b/docker/vaapi/Dockerfile similarity index 85% rename from Dockerfile rename to docker/vaapi/Dockerfile index ce9994890..694ce37a0 100644 --- a/Dockerfile +++ b/docker/vaapi/Dockerfile @@ -1,5 +1,8 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS runtime-base -RUN apt-get update && apt-get install -y ffmpeg i965-va-driver +FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS dotnet-runtime + +FROM jrottenberg/ffmpeg:4.3-vaapi1804 AS runtime-base +COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet +RUN apt-get update && apt-get install -y libicu-dev # https://hub.docker.com/_/microsoft-dotnet FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build