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:
Jason Dove
2021-03-06 22:07:28 +00:00
committed by GitHub
parent 51cdb372b9
commit b13b2b9805
29 changed files with 2322 additions and 81 deletions
+1
View File
@@ -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;
}
+67 -68
View File
@@ -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);
}
}
+3 -4
View File
@@ -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();