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
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -8,6 +9,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
string Name,
|
||||
int ThreadCount,
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
int ResolutionId,
|
||||
bool NormalizeResolution,
|
||||
string VideoCodec,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -9,6 +10,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
string Name,
|
||||
int ThreadCount,
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
int ResolutionId,
|
||||
bool NormalizeResolution,
|
||||
string VideoCodec,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Application.Resolutions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
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,
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
profile.Name,
|
||||
profile.ThreadCount,
|
||||
profile.Transcode,
|
||||
profile.HardwareAcceleration,
|
||||
Project(profile.Resolution),
|
||||
profile.NormalizeResolution,
|
||||
profile.VideoCodec,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
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
|
||||
|
||||
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||
}
|
||||
|
||||
private FFmpegProfile TestProfile() =>
|
||||
new() { Resolution = new Resolution { Width = 1920, Height = 1080 } };
|
||||
}
|
||||
|
||||
[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 };
|
||||
|
||||
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 } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum HardwareAccelerationKind
|
||||
{
|
||||
None = 0,
|
||||
Qsv = 1,
|
||||
Nvenc = 2,
|
||||
Vaapi = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public record FFmpegComplexFilter(string ComplexFilter, string VideoLabel, string AudioLabel);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
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; }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(version);
|
||||
if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
|
||||
if (ffmpegProfile.NormalizeResolution && !sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
|
||||
{
|
||||
result.PadToDesiredResolution = true;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
builder = builder
|
||||
.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithSAR()
|
||||
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
|
||||
Generated
+1494
File diff suppressed because it is too large
Load Diff
+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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
Name,
|
||||
ThreadCount,
|
||||
Transcode,
|
||||
HardwareAcceleration,
|
||||
Resolution.Id,
|
||||
NormalizeResolution,
|
||||
VideoCodec,
|
||||
@@ -78,6 +82,7 @@ namespace ErsatzTV.ViewModels
|
||||
Name,
|
||||
ThreadCount,
|
||||
Transcode,
|
||||
HardwareAcceleration,
|
||||
Resolution.Id,
|
||||
NormalizeResolution,
|
||||
VideoCodec,
|
||||
|
||||
@@ -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"]
|
||||
@@ -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 ]
|
||||
@@ -0,0 +1,8 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
ersatztv:
|
||||
build:
|
||||
dockerfile: docker/vaapi/Dockerfile
|
||||
devices:
|
||||
- /dev/dri/renderD128:/dev/dri/renderD128
|
||||
@@ -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:
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user