Browse Source

Hardware-accelerated transcoding (#41)

* add qsv transcoding support

* add basic nvenc support

* add nvenc to docker-compose

* add vaapi hardware acceleration

* add vaapi driver to dockerfile

* raise ffmpeg log level

* lots of progress with nvenc, qsv and vaapi remain untested

* qsv fixes

* code cleanup
pull/1213/head
Jason Dove 5 years ago committed by GitHub
parent
commit
47e0d66bf2
  1. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  2. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  3. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  4. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  5. 2
      ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
  6. 1
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  7. 344
      ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs
  8. 52
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs
  9. 1
      ErsatzTV.Core/Domain/FFmpegProfile.cs
  10. 10
      ErsatzTV.Core/Domain/HardwareAccelerationKind.cs
  11. 4
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilter.cs
  12. 146
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  13. 3
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs
  14. 4
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  15. 135
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  16. 7
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  17. 1494
      ErsatzTV.Infrastructure/Migrations/20210302224925_Add_FFmpegProfileHardwareAcceleration.Designer.cs
  18. 20
      ErsatzTV.Infrastructure/Migrations/20210302224925_Add_FFmpegProfileHardwareAcceleration.cs
  19. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  20. 1
      ErsatzTV.sln.DotSettings
  21. 8
      ErsatzTV/Pages/FFmpegEditor.razor
  22. 40
      ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs
  23. 5
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs
  24. 40
      docker/Dockerfile
  25. 15
      docker/docker-compose.nvidia.yml
  26. 8
      docker/docker-compose.vaapi.yml
  27. 5
      docker/docker-compose.yml
  28. 40
      docker/nvidia/Dockerfile
  29. 7
      docker/vaapi/Dockerfile

2
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
@ -8,6 +9,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -8,6 +9,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
int ResolutionId,
bool NormalizeResolution,
string VideoCodec,

1
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs

@ -41,6 +41,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -41,6 +41,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
Name = name,
ThreadCount = threadCount,
Transcode = request.Transcode,
HardwareAcceleration = request.HardwareAcceleration,
ResolutionId = resolutionId,
NormalizeResolution = request.NormalizeResolution,
VideoCodec = request.VideoCodec,

2
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
@ -9,6 +10,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -9,6 +10,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
int ResolutionId,
bool NormalizeResolution,
string VideoCodec,

1
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs

@ -35,6 +35,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -35,6 +35,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.Transcode = update.Transcode;
p.HardwareAcceleration = update.HardwareAcceleration;
p.ResolutionId = update.ResolutionId;
p.NormalizeResolution = update.NormalizeResolution;
p.VideoCodec = update.VideoCodec;

2
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles
{
@ -7,6 +8,7 @@ namespace ErsatzTV.Application.FFmpegProfiles @@ -7,6 +8,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
ResolutionViewModel Resolution,
bool NormalizeResolution,
string VideoCodec,

1
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -11,6 +11,7 @@ namespace ErsatzTV.Application.FFmpegProfiles @@ -11,6 +11,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
profile.Name,
profile.ThreadCount,
profile.Transcode,
profile.HardwareAcceleration,
Project(profile.Resolution),
profile.NormalizeResolution,
profile.VideoCodec,

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

@ -0,0 +1,344 @@ @@ -0,0 +1,344 @@
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,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
true,
false,
true,
"[0:v]deinterlace_qsv,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
true,
true,
true,
"[0:v]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
false,
true,
false,
"[0:v]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
false,
false,
true,
"[0:v]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
"[v]")]
[TestCase(
false,
true,
true,
"[0:v]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[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,
true,
false,
"[0:v]scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]",
"[v]")]
[TestCase(
true,
false,
true,
"[0:v]hwdownload,format=nv12,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,format=nv12,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);
});
}
}
}
}

52
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs

@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
[TestFixture]
public class FFmpegPlaybackSettingsCalculatorTests
{
[TestFixture]
public class CalculateSettings
{
private readonly FFmpegPlaybackSettingsCalculator _calculator;
@ -273,6 +274,29 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -273,6 +274,29 @@ namespace ErsatzTV.Core.Tests.FFmpeg
actual.PadToDesiredResolution.Should().BeFalse();
}
[Test]
public void Should_NotPadToDesiredResolution_When_NotNormalizingResolution()
{
FFmpegProfile ffmpegProfile = TestProfile() with
{
NormalizeResolution = false,
Resolution = new Resolution { Width = 1920, Height = 1080 }
};
// not anamorphic
var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
ffmpegProfile,
version,
DateTimeOffset.Now,
DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
}
[Test]
public void Should_SetDesiredVideoCodec_When_ContentIsPadded_ForTransportStream()
{
@ -732,9 +756,33 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -732,9 +756,33 @@ namespace ErsatzTV.Core.Tests.FFmpeg
actual.AudioSampleRate.IfNone(0).Should().Be(48);
}
}
[TestFixture]
public class CalculateSettingsQsv
{
private readonly FFmpegPlaybackSettingsCalculator _calculator;
public CalculateSettingsQsv() => _calculator = new FFmpegPlaybackSettingsCalculator();
[Test]
public void Should_UseHardwareAcceleration()
{
FFmpegProfile ffmpegProfile =
TestProfile() with { HardwareAcceleration = HardwareAccelerationKind.Qsv };
private FFmpegProfile TestProfile() =>
new() { Resolution = new Resolution { Width = 1920, Height = 1080 } };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
ffmpegProfile,
new MediaVersion(),
DateTimeOffset.Now,
DateTimeOffset.Now);
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
}
}
private static FFmpegProfile TestProfile() =>
new() { Resolution = new Resolution { Width = 1920, Height = 1080 } };
}
}

1
ErsatzTV.Core/Domain/FFmpegProfile.cs

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
public string Name { get; set; }
public int ThreadCount { get; set; }
public bool Transcode { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public int ResolutionId { get; set; }
public Resolution Resolution { get; set; }
public bool NormalizeResolution { get; set; }

10
ErsatzTV.Core/Domain/HardwareAccelerationKind.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Domain
{
public enum HardwareAccelerationKind
{
None = 0,
Qsv = 1,
Nvenc = 2,
Vaapi = 3
}
}

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);
}

146
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -0,0 +1,146 @@ @@ -0,0 +1,146 @@
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 ||
acceleration == HardwareAccelerationKind.Qsv)
{
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(
acceleration == HardwareAccelerationKind.Qsv ? "hwupload=extra_hw_frames=64" : "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);
}
}
}

3
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using LanguageExt;
@ -9,6 +10,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -9,6 +10,8 @@ namespace ErsatzTV.Core.FFmpeg
{
public int ThreadCount { get; set; }
public List<string> FormatFlags { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public string VideoDecoder { get; set; }
public bool RealtimeOutput => true;
public Option<TimeSpan> StreamSeek { get; set; }
public Option<IDisplaySize> ScaledSize { get; set; }

4
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -67,6 +67,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -67,6 +67,8 @@ namespace ErsatzTV.Core.FFmpeg
result.Deinterlace = false;
break;
case StreamingMode.TransportStream:
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;
if (NeedToScale(ffmpegProfile, version))
{
IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, version);
@ -77,7 +79,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -77,7 +79,7 @@ namespace ErsatzTV.Core.FFmpeg
}
IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(version);
if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
if (ffmpegProfile.NormalizeResolution && !sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
{
result.PadToDesiredResolution = true;
}

135
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;
@ -31,10 +30,16 @@ namespace ErsatzTV.Core.FFmpeg @@ -31,10 +30,16 @@ namespace ErsatzTV.Core.FFmpeg
{
internal class FFmpegProcessBuilder
{
private static readonly Dictionary<string, string> QsvMap = new()
{
{ "h264", "h264_qsv" },
{ "hevc", "hevc_qsv" },
{ "mpeg2video", "mpeg2_qsv" }
};
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;
@ -45,6 +50,37 @@ namespace ErsatzTV.Core.FFmpeg @@ -45,6 +50,37 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithHardwareAcceleration(HardwareAccelerationKind hwAccel)
{
switch (hwAccel)
{
case HardwareAccelerationKind.Qsv:
_arguments.Add("-hwaccel");
_arguments.Add("qsv");
_arguments.Add("-init_hw_device");
_arguments.Add("qsv=qsv:MFX_IMPL_hw_any");
break;
case HardwareAccelerationKind.Nvenc:
_arguments.Add("-hwaccel");
_arguments.Add("cuda");
_arguments.Add("-hwaccel_output_format");
_arguments.Add("cuda");
break;
case HardwareAccelerationKind.Vaapi:
_arguments.Add("-hwaccel");
_arguments.Add("vaapi");
_arguments.Add("-vaapi_device");
_arguments.Add("/dev/dri/renderD128");
_arguments.Add("-hwaccel_output_format");
_arguments.Add("vaapi");
break;
}
_complexFilterBuilder = _complexFilterBuilder.WithHardwareAcceleration(hwAccel);
return this;
}
public FFmpegProcessBuilder WithRealtimeOutput(bool realtimeOutput)
{
if (realtimeOutput)
@ -109,6 +145,19 @@ namespace ErsatzTV.Core.FFmpeg @@ -109,6 +145,19 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithInputCodec(string input, HardwareAccelerationKind hwAccel, string codec)
{
if (hwAccel == HardwareAccelerationKind.Qsv && QsvMap.TryGetValue(codec, out string qsvCodec))
{
_arguments.Add("-c:v");
_arguments.Add(qsvCodec);
}
_arguments.Add("-i");
_arguments.Add($"{input}");
return this;
}
public FFmpegProcessBuilder WithFiltergraph(string graph)
{
_arguments.Add("-vf");
@ -242,73 +291,44 @@ namespace ErsatzTV.Core.FFmpeg @@ -242,73 +291,44 @@ namespace ErsatzTV.Core.FFmpeg
return this;
}
public FFmpegProcessBuilder WithScaling(IDisplaySize displaySize, string algorithm)
public FFmpegProcessBuilder WithScaling(IDisplaySize displaySize)
{
_videoFilters.Enqueue($"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);
@ -321,7 +341,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -321,7 +341,7 @@ namespace ErsatzTV.Core.FFmpeg
public FFmpegProcessBuilder WithQuiet()
{
_arguments.AddRange(new[] { "-hide_banner", "-loglevel", "panic", "-nostats" });
_arguments.AddRange(new[] { "-hide_banner", "-loglevel", "error", "-nostats" });
return this;
}
@ -347,26 +367,5 @@ namespace ErsatzTV.Core.FFmpeg @@ -347,26 +367,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);
}
}

7
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -29,18 +29,18 @@ namespace ErsatzTV.Core.FFmpeg @@ -29,18 +29,18 @@ namespace ErsatzTV.Core.FFmpeg
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath)
.WithThreads(playbackSettings.ThreadCount)
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
.WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithSeek(playbackSettings.StreamSeek)
.WithInput(path);
.WithInputCodec(path, playbackSettings.HardwareAcceleration, version.VideoCodec);
playbackSettings.ScaledSize.Match(
scaledSize =>
{
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
.WithScaling(scaledSize, playbackSettings.ScalingAlgorithm)
.WithSAR();
.WithScaling(scaledSize);
scaledSize = scaledSize.PadToEven();
if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize))
@ -57,7 +57,6 @@ namespace ErsatzTV.Core.FFmpeg @@ -57,7 +57,6 @@ namespace ErsatzTV.Core.FFmpeg
{
builder = builder
.WithDeinterlace(playbackSettings.Deinterlace)
.WithSAR()
.WithBlackBars(channel.FFmpegProfile.Resolution)
.WithAlignedAudio(playbackSettings.AudioDuration)
.WithFilterComplex();

1494
ErsatzTV.Infrastructure/Migrations/20210302224925_Add_FFmpegProfileHardwareAcceleration.Designer.cs generated

File diff suppressed because it is too large Load Diff

20
ErsatzTV.Infrastructure/Migrations/20210302224925_Add_FFmpegProfileHardwareAcceleration.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_FFmpegProfileHardwareAcceleration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.AddColumn<int>(
"HardwareAcceleration",
"FFmpegProfile",
"INTEGER",
nullable: false,
defaultValue: 0);
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropColumn(
"HardwareAcceleration",
"FFmpegProfile");
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -231,6 +231,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -231,6 +231,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("AudioVolume")
.HasColumnType("INTEGER");
b.Property<int>("HardwareAcceleration")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");

1
ErsatzTV.sln.DotSettings

@ -39,4 +39,5 @@ @@ -39,4 +39,5 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=probesize/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvshow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vaapi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=yadif/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

8
ErsatzTV/Pages/FFmpegEditor.razor

@ -49,6 +49,14 @@ @@ -49,6 +49,14 @@
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField Disabled="@(!_model.Transcode)" Label="Buffer Size" @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudSelect Disabled="@(!_model.Transcode)" Label="Hardware Acceleration" @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@foreach (HardwareAccelerationKind hwAccel in Enum.GetValues<HardwareAccelerationKind>())
{
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
}
</MudSelect>
</MudElement>
</MudItem>
<MudItem>
<MudText Typo="Typo.h6">Audio</MudText>

40
ErsatzTV/Validators/FFmpegProfileEditViewModelValidator.cs

@ -1,10 +1,16 @@ @@ -1,10 +1,16 @@
using ErsatzTV.ViewModels;
using System.Collections.Generic;
using ErsatzTV.Core.Domain;
using ErsatzTV.ViewModels;
using FluentValidation;
namespace ErsatzTV.Validators
{
public class FFmpegProfileEditViewModelValidator : AbstractValidator<FFmpegProfileEditViewModel>
{
private static readonly List<string> QsvEncoders = new() { "h264_qsv", "hevc_qsv", "mpeg2_qsv" };
private static readonly List<string> NvencEncoders = new() { "h264_nvenc", "hevc_nvenc" };
private static readonly List<string> VaapiEncoders = new() { "h264_vaapi", "hevc_vaapi", "mpeg2_vaapi" };
public FFmpegProfileEditViewModelValidator()
{
RuleFor(x => x.Name).NotEmpty();
@ -23,6 +29,38 @@ namespace ErsatzTV.Validators @@ -23,6 +29,38 @@ namespace ErsatzTV.Validators
RuleFor(x => x.AudioVolume).GreaterThanOrEqualTo(0);
RuleFor(x => x.AudioChannels).GreaterThan(0);
});
When(
x => x.HardwareAcceleration == HardwareAccelerationKind.Qsv,
() =>
{
RuleFor(x => x.VideoCodec).Must(c => QsvEncoders.Contains(c))
.WithMessage("QSV codec is required (h264_qsv, hevc_qsv, mpeg2_qsv)");
});
When(
x => x.HardwareAcceleration == HardwareAccelerationKind.Nvenc,
() =>
{
RuleFor(x => x.VideoCodec).Must(c => NvencEncoders.Contains(c))
.WithMessage("NVENC codec is required (h264_nvenc, hevc_nvenc)");
});
When(
x => x.HardwareAcceleration == HardwareAccelerationKind.Vaapi,
() =>
{
RuleFor(x => x.VideoCodec).Must(c => VaapiEncoders.Contains(c))
.WithMessage("VAAPI codec is required (h264_vaapi, hevc_vaapi, mpeg2_vaapi)");
});
When(
x => x.HardwareAcceleration == HardwareAccelerationKind.None,
() =>
{
RuleFor(x => x.VideoCodec).Must(c => !QsvEncoders.Contains(c) && !NvencEncoders.Contains(c))
.WithMessage("Hardware acceleration is required for this codec");
});
}
}
}

5
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
using ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.Application.FFmpegProfiles.Commands;
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.ViewModels
{
@ -27,6 +28,7 @@ namespace ErsatzTV.ViewModels @@ -27,6 +28,7 @@ namespace ErsatzTV.ViewModels
Resolution = viewModel.Resolution;
ThreadCount = viewModel.ThreadCount;
Transcode = viewModel.Transcode;
HardwareAcceleration = viewModel.HardwareAcceleration;
VideoBitrate = viewModel.VideoBitrate;
VideoBufferSize = viewModel.VideoBufferSize;
VideoCodec = viewModel.VideoCodec;
@ -47,6 +49,7 @@ namespace ErsatzTV.ViewModels @@ -47,6 +49,7 @@ namespace ErsatzTV.ViewModels
public ResolutionViewModel Resolution { get; set; }
public int ThreadCount { get; set; }
public bool Transcode { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; }
public string VideoCodec { get; set; }
@ -56,6 +59,7 @@ namespace ErsatzTV.ViewModels @@ -56,6 +59,7 @@ namespace ErsatzTV.ViewModels
Name,
ThreadCount,
Transcode,
HardwareAcceleration,
Resolution.Id,
NormalizeResolution,
VideoCodec,
@ -78,6 +82,7 @@ namespace ErsatzTV.ViewModels @@ -78,6 +82,7 @@ namespace ErsatzTV.ViewModels
Name,
ThreadCount,
Transcode,
HardwareAcceleration,
Resolution.Id,
NormalizeResolution,
VideoCodec,

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

5
docker-compose.yml → docker/docker-compose.yml

@ -3,13 +3,14 @@ @@ -3,13 +3,14 @@
services:
ersatztv:
build:
context: .
context: ..
dockerfile: docker/Dockerfile
args:
INFO_VERSION: "docker-compose-develop"
ports:
- "8409:8409"
volumes:
- ersatztv:/root/.local/share/ersatztv
#- /media/shared:/media/shared:ro
#- /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
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