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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user