Browse Source

lots of progress with nvenc, qsv and vaapi remain untested

pull/41/head
Jason Dove 5 years ago
parent
commit
f739199b67
  1. 340
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  2. 4
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs
  3. 144
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  4. 96
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  5. 4
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  6. 2
      ErsatzTV/Pages/FFmpegEditor.razor
  7. 4
      ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs
  8. 17
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs
  9. 29
      docker-compose.yml
  10. 40
      docker/Dockerfile
  11. 15
      docker/docker-compose.nvidia.yml
  12. 8
      docker/docker-compose.vaapi.yml
  13. 16
      docker/docker-compose.yml
  14. 40
      docker/nvidia/Dockerfile
  15. 7
      docker/vaapi/Dockerfile

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

@ -0,0 +1,340 @@ @@ -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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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<FFmpegComplexFilter> 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);
});
}
}
}
}

4
ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Core.FFmpeg
{
public record FFmpegComplexFilter(string ComplexFilter, string VideoLabel, string AudioLabel);
}

144
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -0,0 +1,144 @@ @@ -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<TimeSpan> _audioDuration = None;
private bool _deinterlace;
private Option<HardwareAccelerationKind> _hardwareAccelerationKind = None;
private Option<IDisplaySize> _padToSize = None;
private Option<IDisplaySize> _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<TimeSpan> audioDuration)
{
_audioDuration = audioDuration;
return this;
}
public Option<FFmpegComplexFilter> 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<string>();
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<FFmpegComplexFilter>.None
: new FFmpegComplexFilter(filterResult, videoLabel, audioLabel);
}
}
}

96
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -21,7 +21,6 @@ @@ -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 @@ -39,9 +38,8 @@ namespace ErsatzTV.Core.FFmpeg
};
private readonly List<string> _arguments = new();
private readonly Queue<string> _audioFilters = new();
private readonly string _ffmpegPath;
private readonly Queue<string> _videoFilters = new();
private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath;
@ -76,6 +74,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -76,6 +74,8 @@ namespace ErsatzTV.Core.FFmpeg
break;
}
_complexFilterBuilder = _complexFilterBuilder.WithHardwareAcceleration(hwAccel);
return this;
}
@ -289,85 +289,44 @@ namespace ErsatzTV.Core.FFmpeg @@ -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<TimeSpan> 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<FFmpegComplexFilter> 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 @@ -406,26 +365,5 @@ namespace ErsatzTV.Core.FFmpeg
StartInfo = startInfo
};
}
private FilterResult GenerateVideoFilter(Queue<string> filterQueue) =>
GenerateFilter(filterQueue, "null", 'v');
private FilterResult GenerateAudioFilter(Queue<string> filterQueue) =>
GenerateFilter(filterQueue, "anull", 'a');
private static FilterResult GenerateFilter(Queue<string> 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);
}
}

4
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -40,8 +40,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -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 @@ -58,7 +57,6 @@ namespace ErsatzTV.Core.FFmpeg
{
builder = builder
.WithDeinterlace(playbackSettings.Deinterlace)
.WithSAR()
.WithBlackBars(channel.FFmpegProfile.Resolution)
.WithAlignedAudio(playbackSettings.AudioDuration)
.WithFilterComplex();

2
ErsatzTV/Pages/FFmpegEditor.razor

@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
</MudItem>
<MudItem>
<MudText Typo="Typo.h6">Normalization</MudText>
<MudCheckBox Disabled="@(!_model.Transcode || _model.HardwareAcceleration == HardwareAccelerationKind.Nvenc)" Label="Normalize Resolution" @bind-Checked="@_model.NormalizeResolution" For="@(() => _model.NormalizeResolution)"/>
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Resolution" @bind-Checked="@_model.NormalizeResolution" For="@(() => _model.NormalizeResolution)"/>
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video Codec" @bind-Checked="@_model.NormalizeVideoCodec" For="@(() => _model.NormalizeVideoCodec)"/>
</MudElement>

4
ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs

@ -45,8 +45,8 @@ namespace ErsatzTV.Validators @@ -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(

17
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -7,8 +7,6 @@ namespace ErsatzTV.ViewModels @@ -7,8 +7,6 @@ namespace ErsatzTV.ViewModels
{
public class FFmpegProfileEditViewModel
{
private HardwareAccelerationKind _hardwareAcceleration;
public FFmpegProfileEditViewModel()
{
}
@ -51,20 +49,7 @@ namespace ErsatzTV.ViewModels @@ -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; }

29
docker-compose.yml

@ -1,29 +0,0 @@ @@ -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:

40
docker/Dockerfile

@ -0,0 +1,40 @@ @@ -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"]

15
docker/docker-compose.nvidia.yml

@ -0,0 +1,15 @@ @@ -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 ]

8
docker/docker-compose.vaapi.yml

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
version: "3.1"
services:
ersatztv:
build:
dockerfile: docker/vaapi/Dockerfile
devices:
- /dev/dri/renderD128:/dev/dri/renderD128

16
docker/docker-compose.yml

@ -0,0 +1,16 @@ @@ -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:

40
docker/nvidia/Dockerfile

@ -0,0 +1,40 @@ @@ -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"]

7
Dockerfile → docker/vaapi/Dockerfile

@ -1,5 +1,8 @@ @@ -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
Loading…
Cancel
Save