mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* fix subtitles test and nvidia subtitles * new ffmpeg pipelines; software and nvidia * partial qsv * fix qsv * fix software pipeline * add vaapi pipeline * fix qsv 10-bit h264 output * nvidia fixes * properly disable 10-bit h264 hardware encoders * more nvidia fixes * add video toolbox pipelinepull/1046/head
73 changed files with 3869 additions and 564 deletions
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public abstract class CuvidDecoder : DecoderBase |
||||
{ |
||||
protected CuvidDecoder(HardwareAccelerationMode hardwareAccelerationMode) |
||||
{ |
||||
HardwareAccelerationMode = hardwareAccelerationMode; |
||||
} |
||||
|
||||
public HardwareAccelerationMode HardwareAccelerationMode { get; set; } |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
HardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
if (HardwareAccelerationMode != HardwareAccelerationMode.None) |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
} |
||||
else |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(InputBitDepth(inputFile) == 10 ? "p010le" : "nv12"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
@ -1,33 +1,10 @@
@@ -1,33 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderH264Cuvid : DecoderBase |
||||
public class DecoderH264Cuvid : CuvidDecoder |
||||
{ |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public DecoderH264Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; |
||||
|
||||
public override string Name => "h264_cuvid"; |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
public DecoderH264Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
if (_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None) |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
} |
||||
else |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(InputBitDepth(inputFile) == 10 ? "p010le" : "nv12"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public override string Name => "h264_cuvid"; |
||||
} |
||||
|
||||
@ -1,33 +1,10 @@
@@ -1,33 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderHevcCuvid : DecoderBase |
||||
public class DecoderHevcCuvid : CuvidDecoder |
||||
{ |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public DecoderHevcCuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; |
||||
|
||||
public override string Name => "hevc_cuvid"; |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
public DecoderHevcCuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
if (_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None) |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
} |
||||
else |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(InputBitDepth(inputFile) == 10 ? "p010le" : "nv12"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public override string Name => "hevc_cuvid"; |
||||
} |
||||
|
||||
@ -1,38 +1,19 @@
@@ -1,38 +1,19 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderMpeg2Cuvid : DecoderBase |
||||
public class DecoderMpeg2Cuvid : CuvidDecoder |
||||
{ |
||||
private readonly bool _contentIsInterlaced; |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public DecoderMpeg2Cuvid(FFmpegState ffmpegState, bool contentIsInterlaced) |
||||
public DecoderMpeg2Cuvid(HardwareAccelerationMode hardwareAccelerationMode, bool contentIsInterlaced) |
||||
: base(hardwareAccelerationMode) |
||||
{ |
||||
_ffmpegState = ffmpegState; |
||||
_contentIsInterlaced = contentIsInterlaced; |
||||
} |
||||
|
||||
public override string Name => "mpeg2_cuvid"; |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
_contentIsInterlaced || _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
||||
_contentIsInterlaced || HardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
if (_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None) |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
} |
||||
else |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(InputBitDepth(inputFile) == 10 ? "p010le" : "nv12"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
@ -1,33 +1,12 @@
@@ -1,33 +1,12 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderMpeg4Cuvid : DecoderBase |
||||
public class DecoderMpeg4Cuvid : CuvidDecoder |
||||
{ |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public DecoderMpeg4Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; |
||||
public DecoderMpeg4Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||
{ |
||||
} |
||||
|
||||
public override string Name => "mpeg4_cuvid"; |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
if (_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None) |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
} |
||||
else |
||||
{ |
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(InputBitDepth(inputFile) == 10 ? "p010le" : "nv12"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
@ -1,25 +1,10 @@
@@ -1,25 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderVc1Cuvid : DecoderBase |
||||
public class DecoderVc1Cuvid : CuvidDecoder |
||||
{ |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public DecoderVc1Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; |
||||
|
||||
public override string Name => "vc1_cuvid"; |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
public DecoderVc1Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public override string Name => "vc1_cuvid"; |
||||
} |
||||
|
||||
@ -1,25 +1,10 @@
@@ -1,25 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderVp9Cuvid : DecoderBase |
||||
public class DecoderVp9Cuvid : CuvidDecoder |
||||
{ |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public DecoderVp9Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; |
||||
|
||||
public override string Name => "vp9_cuvid"; |
||||
|
||||
protected override FrameDataLocation OutputFrameDataLocation => |
||||
_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
||||
? FrameDataLocation.Software |
||||
: FrameDataLocation.Hardware; |
||||
|
||||
public override IList<string> InputOptions(InputFile inputFile) |
||||
public DecoderVp9Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||
{ |
||||
IList<string> result = base.InputOptions(inputFile); |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add(_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.None ? "cuda" : "nv12"); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public override string Name => "vp9_cuvid"; |
||||
} |
||||
|
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Cuda; |
||||
|
||||
public class CudaFormatFilter : BaseFilter |
||||
{ |
||||
private readonly IPixelFormat _pixelFormat; |
||||
|
||||
public CudaFormatFilter(IPixelFormat pixelFormat) => _pixelFormat = pixelFormat; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with { PixelFormat = Some(_pixelFormat) }; |
||||
|
||||
public override string Filter => $"scale_cuda=format={_pixelFormat.FFmpegName}"; |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Cuda; |
||||
|
||||
public class CudaHardwareDownloadFilter : BaseFilter |
||||
{ |
||||
private readonly Option<IPixelFormat> _maybePixelFormat; |
||||
|
||||
public CudaHardwareDownloadFilter(Option<IPixelFormat> maybePixelFormat) => _maybePixelFormat = maybePixelFormat; |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
var hwdownload = "hwdownload"; |
||||
foreach (IPixelFormat pixelFormat in _maybePixelFormat) |
||||
{ |
||||
if (!string.IsNullOrWhiteSpace(pixelFormat.FFmpegName)) |
||||
{ |
||||
hwdownload += $",format={pixelFormat.FFmpegName}"; |
||||
|
||||
if (pixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
hwdownload += $",format={pf.FFmpegName}"; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return hwdownload; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = currentState with |
||||
{ |
||||
FrameDataLocation = FrameDataLocation.Software |
||||
}; |
||||
|
||||
foreach (IPixelFormat pixelFormat in _maybePixelFormat) |
||||
{ |
||||
if (pixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
result = result with { PixelFormat = AvailablePixelFormats.ForPixelFormat(nv12.Name, null) }; |
||||
} |
||||
else |
||||
{ |
||||
result = result with { PixelFormat = Some(pixelFormat) }; |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class HardwareUploadCudaFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
|
||||
public HardwareUploadCudaFilter(FrameState currentState) |
||||
{ |
||||
_currentState = currentState; |
||||
} |
||||
|
||||
public override string Filter => _currentState.FrameDataLocation switch |
||||
{ |
||||
FrameDataLocation.Hardware => string.Empty, |
||||
_ => "hwupload_cuda" |
||||
}; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => |
||||
currentState with { FrameDataLocation = FrameDataLocation.Hardware }; |
||||
} |
||||
@ -0,0 +1,252 @@
@@ -0,0 +1,252 @@
|
||||
using ErsatzTV.FFmpeg.Environment; |
||||
using ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class NewComplexFilter : IPipelineStep |
||||
{ |
||||
private readonly Option<AudioInputFile> _maybeAudioInputFile; |
||||
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile; |
||||
private readonly PipelineContext _pipelineContext; |
||||
private readonly FilterChain _filterChain; |
||||
private readonly Option<VideoInputFile> _maybeVideoInputFile; |
||||
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile; |
||||
private readonly List<string> _outputOptions; |
||||
private readonly IList<string> _arguments; |
||||
|
||||
public NewComplexFilter( |
||||
Option<VideoInputFile> maybeVideoInputFile, |
||||
Option<AudioInputFile> maybeAudioInputFile, |
||||
Option<WatermarkInputFile> maybeWatermarkInputFile, |
||||
Option<SubtitleInputFile> maybeSubtitleInputFile, |
||||
PipelineContext pipelineContext, |
||||
FilterChain filterChain) |
||||
{ |
||||
_maybeVideoInputFile = maybeVideoInputFile; |
||||
_maybeAudioInputFile = maybeAudioInputFile; |
||||
_maybeWatermarkInputFile = maybeWatermarkInputFile; |
||||
_maybeSubtitleInputFile = maybeSubtitleInputFile; |
||||
_pipelineContext = pipelineContext; |
||||
_filterChain = filterChain; |
||||
|
||||
_outputOptions = new List<string>(); |
||||
|
||||
_arguments = Arguments(); |
||||
} |
||||
|
||||
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>(); |
||||
public IList<string> GlobalOptions => Array.Empty<string>(); |
||||
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>(); |
||||
public IList<string> FilterOptions => _arguments; |
||||
public IList<string> OutputOptions => _outputOptions; |
||||
|
||||
public FrameState NextState(FrameState currentState) => currentState; |
||||
|
||||
// for testing
|
||||
public FilterChain FilterChain => _filterChain; |
||||
|
||||
private List<string> Arguments() |
||||
{ |
||||
var audioLabel = "0:a"; |
||||
var videoLabel = "0:v"; |
||||
string? watermarkLabel = null; |
||||
string? subtitleLabel = null; |
||||
|
||||
var result = new List<string>(); |
||||
|
||||
string audioFilterComplex = string.Empty; |
||||
string videoFilterComplex = string.Empty; |
||||
string watermarkFilterComplex = string.Empty; |
||||
string watermarkOverlayFilterComplex = string.Empty; |
||||
string subtitleFilterComplex = string.Empty; |
||||
string subtitleOverlayFilterComplex = string.Empty; |
||||
string pixelFormatFilterComplex = string.Empty; |
||||
|
||||
var distinctPaths = new List<string>(); |
||||
foreach ((string path, _) in _maybeVideoInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path)) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach ((string path, _) in _maybeAudioInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path) || |
||||
// use audio as a separate input with vaapi/qsv
|
||||
_pipelineContext.HardwareAccelerationMode is HardwareAccelerationMode.Vaapi |
||||
or HardwareAccelerationMode.Qsv) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach ((string path, _) in _maybeWatermarkInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path)) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach ((string path, _) in _maybeSubtitleInputFile) |
||||
{ |
||||
if (!distinctPaths.Contains(path)) |
||||
{ |
||||
distinctPaths.Add(path); |
||||
} |
||||
} |
||||
|
||||
foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(videoInputFile.Path); |
||||
foreach ((int index, _, _) in videoInputFile.Streams) |
||||
{ |
||||
videoLabel = $"{inputIndex}:{index}"; |
||||
if (_filterChain.VideoFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
videoFilterComplex += $"[{inputIndex}:{index}]"; |
||||
videoFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.VideoFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[v]"; |
||||
videoFilterComplex += videoLabel; |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach (WatermarkInputFile watermarkInputFile in _maybeWatermarkInputFile) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(watermarkInputFile.Path); |
||||
foreach ((int index, _, _) in watermarkInputFile.Streams) |
||||
{ |
||||
watermarkLabel = $"{inputIndex}:{index}"; |
||||
if (_filterChain.WatermarkFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
watermarkFilterComplex += $"[{inputIndex}:{index}]"; |
||||
watermarkFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.WatermarkFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
watermarkLabel = "[wm]"; |
||||
watermarkFilterComplex += watermarkLabel; |
||||
} |
||||
else |
||||
{ |
||||
watermarkLabel = $"[{watermarkLabel}]"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => !s.Copy)) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); |
||||
foreach ((int index, _, _) in subtitleInputFile.Streams) |
||||
{ |
||||
subtitleLabel = $"{inputIndex}:{index}"; |
||||
if (_filterChain.SubtitleFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
subtitleFilterComplex += $"[{inputIndex}:{index}]"; |
||||
subtitleFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.SubtitleFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
subtitleLabel = "[st]"; |
||||
subtitleFilterComplex += subtitleLabel; |
||||
} |
||||
else |
||||
{ |
||||
subtitleLabel = $"[{subtitleLabel}]"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// overlay subtitle
|
||||
if (!string.IsNullOrWhiteSpace(subtitleLabel) && _filterChain.SubtitleOverlayFilterSteps.Any()) |
||||
{ |
||||
subtitleOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(subtitleLabel)}"; |
||||
subtitleOverlayFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.SubtitleOverlayFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[vst]"; |
||||
subtitleOverlayFilterComplex += videoLabel; |
||||
} |
||||
|
||||
// overlay watermark
|
||||
if (!string.IsNullOrWhiteSpace(watermarkLabel) && _filterChain.WatermarkOverlayFilterSteps.Any()) |
||||
{ |
||||
watermarkOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(watermarkLabel)}"; |
||||
watermarkOverlayFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.WatermarkOverlayFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[vwm]"; |
||||
watermarkOverlayFilterComplex += videoLabel; |
||||
} |
||||
|
||||
// pixel format
|
||||
if (_filterChain.PixelFormatFilterSteps.Any()) |
||||
{ |
||||
pixelFormatFilterComplex += $"{ProperLabel(videoLabel)}"; |
||||
pixelFormatFilterComplex += string.Join( |
||||
",", |
||||
_filterChain.PixelFormatFilterSteps.Select(f => f.Filter) |
||||
.Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[vpf]"; |
||||
pixelFormatFilterComplex += videoLabel; |
||||
} |
||||
|
||||
foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) |
||||
{ |
||||
int inputIndex = distinctPaths.LastIndexOf(audioInputFile.Path); |
||||
foreach ((int index, _, _) in audioInputFile.Streams) |
||||
{ |
||||
audioLabel = $"{inputIndex}:{index}"; |
||||
if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
audioFilterComplex += $"[{inputIndex}:{index}]"; |
||||
audioFilterComplex += string.Join( |
||||
",", |
||||
audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
audioLabel = "[a]"; |
||||
audioFilterComplex += audioLabel; |
||||
} |
||||
} |
||||
} |
||||
|
||||
var filterComplex = string.Join( |
||||
";", |
||||
new[] |
||||
{ |
||||
audioFilterComplex, |
||||
videoFilterComplex, |
||||
subtitleFilterComplex, |
||||
watermarkFilterComplex, |
||||
subtitleOverlayFilterComplex, |
||||
watermarkOverlayFilterComplex, |
||||
pixelFormatFilterComplex |
||||
}.Where(s => !string.IsNullOrWhiteSpace(s))); |
||||
|
||||
if (!string.IsNullOrWhiteSpace(filterComplex)) |
||||
{ |
||||
result.AddRange(new[] { "-filter_complex", filterComplex }); |
||||
} |
||||
|
||||
result.AddRange(new[] { "-map", audioLabel, "-map", videoLabel }); |
||||
|
||||
foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter(s => s.Copy)) |
||||
{ |
||||
int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); |
||||
foreach ((int index, _, _) in subtitleInputFile.Streams) |
||||
{ |
||||
subtitleLabel = $"{inputIndex}:{index}"; |
||||
result.AddRange(new[] { "-map", subtitleLabel }); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private string ProperLabel(string label) => label.StartsWith("[") ? label : $"[{label}]"; |
||||
} |
||||
@ -1,7 +1,13 @@
@@ -1,7 +1,13 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class OverlaySubtitleFilter : BaseFilter |
||||
{ |
||||
public override string Filter => "overlay=x=(W-w)/2:y=(H-h)/2"; |
||||
private readonly IPixelFormat _outputPixelFormat; |
||||
|
||||
public OverlaySubtitleFilter(IPixelFormat outputPixelFormat) => _outputPixelFormat = outputPixelFormat; |
||||
|
||||
public override string Filter => $"overlay=x=(W-w)/2:y=(H-h)/2:format={(_outputPixelFormat.BitDepth == 10 ? '1' : '0')}"; |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
} |
||||
|
||||
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter.Qsv; |
||||
|
||||
public class HardwareUploadQsvFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
private readonly FFmpegState _ffmpegState; |
||||
|
||||
public HardwareUploadQsvFilter(FrameState currentState, FFmpegState ffmpegState) |
||||
{ |
||||
_currentState = currentState; |
||||
_ffmpegState = ffmpegState; |
||||
} |
||||
|
||||
public override string Filter => _currentState.FrameDataLocation switch |
||||
{ |
||||
FrameDataLocation.Hardware => string.Empty, |
||||
_ => $"hwupload=extra_hw_frames={_ffmpegState.QsvExtraHardwareFrames}", |
||||
}; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => |
||||
currentState with { FrameDataLocation = FrameDataLocation.Hardware }; |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Qsv; |
||||
|
||||
public class QsvFormatFilter : BaseFilter |
||||
{ |
||||
private readonly IPixelFormat _pixelFormat; |
||||
|
||||
public QsvFormatFilter(IPixelFormat pixelFormat) => _pixelFormat = pixelFormat; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with { PixelFormat = Some(_pixelFormat) }; |
||||
|
||||
public override string Filter => $"vpp_qsv=format={_pixelFormat.FFmpegName}"; |
||||
} |
||||
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
public class HardwareUploadVaapiFilter : BaseFilter |
||||
{ |
||||
private readonly bool _setFormat; |
||||
|
||||
public HardwareUploadVaapiFilter(bool setFormat) => _setFormat = setFormat; |
||||
|
||||
public override string Filter => _setFormat switch |
||||
{ |
||||
false => "hwupload", |
||||
true => "format=nv12|p010le|vaapi,hwupload" |
||||
}; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => |
||||
currentState with { FrameDataLocation = FrameDataLocation.Hardware }; |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
public class OverlaySubtitleVaapiFilter : BaseFilter |
||||
{ |
||||
public override string Filter => "overlay_vaapi"; |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
public class SubtitleScaleVaapiFilter : BaseFilter |
||||
{ |
||||
private readonly FrameSize _paddedSize; |
||||
|
||||
public SubtitleScaleVaapiFilter(FrameSize paddedSize) |
||||
{ |
||||
_paddedSize = paddedSize; |
||||
} |
||||
|
||||
public override string Filter => |
||||
$"scale_vaapi={_paddedSize.Width}:{_paddedSize.Height}:force_original_aspect_ratio=decrease"; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
public class VaapiFormatFilter : BaseFilter |
||||
{ |
||||
private readonly IPixelFormat _pixelFormat; |
||||
|
||||
public VaapiFormatFilter(IPixelFormat pixelFormat) => _pixelFormat = pixelFormat; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with { PixelFormat = Some(_pixelFormat) }; |
||||
|
||||
public override string Filter => $"scale_vaapi=format={_pixelFormat.FFmpegName}"; |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.FFmpeg; |
||||
|
||||
public record FilterChain( |
||||
List<IPipelineFilterStep> VideoFilterSteps, |
||||
List<IPipelineFilterStep> WatermarkFilterSteps, |
||||
List<IPipelineFilterStep> SubtitleFilterSteps, |
||||
List<IPipelineFilterStep> WatermarkOverlayFilterSteps, |
||||
List<IPipelineFilterStep> SubtitleOverlayFilterSteps, |
||||
List<IPipelineFilterStep> PixelFormatFilterSteps) |
||||
{ |
||||
public static readonly FilterChain Empty = new( |
||||
new List<IPipelineFilterStep>(), |
||||
new List<IPipelineFilterStep>(), |
||||
new List<IPipelineFilterStep>(), |
||||
new List<IPipelineFilterStep>(), |
||||
new List<IPipelineFilterStep>(), |
||||
new List<IPipelineFilterStep>()); |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatArgb : IPixelFormat |
||||
{ |
||||
public string Name => PixelFormat.ARGB; |
||||
public string FFmpegName => FFmpegFormat.ARGB; |
||||
public int BitDepth => 8; |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatQsv : IPixelFormat |
||||
{ |
||||
public PixelFormatQsv(string name) => Name = name; |
||||
|
||||
public string Name { get; } |
||||
|
||||
public string FFmpegName => "qsv"; |
||||
public int BitDepth => throw new NotSupportedException("This is probably an issue"); |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public interface IPipelineBuilder |
||||
{ |
||||
FFmpegPipeline Resize(string outputFile, FrameSize scaledSize); |
||||
FFmpegPipeline Concat(ConcatInputFile concatInputFile, FFmpegState ffmpegState); |
||||
FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState); |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public interface IPipelineBuilderFactory |
||||
{ |
||||
Task<IPipelineBuilder> GetBuilder( |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
string ffmpegPath); |
||||
} |
||||
@ -0,0 +1,562 @@
@@ -0,0 +1,562 @@
|
||||
using ErsatzTV.FFmpeg.Capabilities; |
||||
using ErsatzTV.FFmpeg.Decoder; |
||||
using ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
using ErsatzTV.FFmpeg.Encoder; |
||||
using ErsatzTV.FFmpeg.Encoder.Nvenc; |
||||
using ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Filter.Cuda; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.Option; |
||||
using ErsatzTV.FFmpeg.Option.HardwareAcceleration; |
||||
using ErsatzTV.FFmpeg.State; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public class NvidiaPipelineBuilder : SoftwarePipelineBuilder |
||||
{ |
||||
private readonly IHardwareCapabilities _hardwareCapabilities; |
||||
private readonly ILogger _logger; |
||||
|
||||
public NvidiaPipelineBuilder( |
||||
IHardwareCapabilities hardwareCapabilities, |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
ILogger logger) : base( |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
logger) |
||||
{ |
||||
_hardwareCapabilities = hardwareCapabilities; |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected override FFmpegState SetAccelState( |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
bool canDecode = _hardwareCapabilities.CanDecode(videoStream.Codec, videoStream.PixelFormat); |
||||
bool canEncode = _hardwareCapabilities.CanEncode(desiredState.VideoFormat, desiredState.PixelFormat); |
||||
|
||||
// mpeg2_cuvid seems to have issues when yadif_cuda is used, so just use software decoding
|
||||
if (context.ShouldDeinterlace && videoStream.Codec == VideoFormat.Mpeg2Video) |
||||
{ |
||||
canDecode = false; |
||||
} |
||||
|
||||
if (canDecode || canEncode) |
||||
{ |
||||
pipelineSteps.Add(new CudaHardwareAccelerationOption()); |
||||
} |
||||
|
||||
// disable hw accel if decoder/encoder isn't supported
|
||||
return ffmpegState with |
||||
{ |
||||
DecoderHardwareAccelerationMode = canDecode |
||||
? HardwareAccelerationMode.Nvenc |
||||
: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode = canEncode |
||||
? HardwareAccelerationMode.Nvenc |
||||
: HardwareAccelerationMode.None |
||||
}; |
||||
} |
||||
|
||||
protected override void SetDecoder( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch |
||||
{ |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new DecoderHevcCuvid(HardwareAccelerationMode.Nvenc), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new DecoderH264Cuvid(HardwareAccelerationMode.Nvenc), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg2Video) => new DecoderMpeg2Cuvid( |
||||
HardwareAccelerationMode.Nvenc, |
||||
context.ShouldDeinterlace), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Vc1) => new DecoderVc1Cuvid(HardwareAccelerationMode.Nvenc), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Vp9) => new DecoderVp9Cuvid(HardwareAccelerationMode.Nvenc), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg4) => |
||||
new DecoderMpeg4Cuvid(HardwareAccelerationMode.Nvenc), |
||||
|
||||
_ => GetSoftwareDecoder(videoStream) |
||||
}; |
||||
|
||||
foreach (IDecoder decoder in maybeDecoder) |
||||
{ |
||||
videoInputFile.AddOption(decoder); |
||||
} |
||||
} |
||||
|
||||
protected override FilterChain SetVideoFilters( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var watermarkOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
var subtitleOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
|
||||
FrameState currentState = desiredState with |
||||
{ |
||||
ScaledSize = videoStream.FrameSize, |
||||
PaddedSize = videoStream.FrameSize, |
||||
|
||||
PixelFormat = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Nvenc && videoStream.BitDepth == 8 |
||||
? videoStream.PixelFormat.Map(pf => (IPixelFormat)new PixelFormatNv12(pf.Name)) |
||||
: videoStream.PixelFormat, |
||||
|
||||
IsAnamorphic = videoStream.IsAnamorphic, |
||||
FrameDataLocation = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Nvenc |
||||
? FrameDataLocation.Hardware |
||||
: FrameDataLocation.Software |
||||
}; |
||||
|
||||
// if (context.HasSubtitleOverlay || context.HasWatermark)
|
||||
// {
|
||||
// IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone(
|
||||
// context.Is10BitOutput ? new PixelFormatYuv420P10Le() : new PixelFormatYuv420P());
|
||||
// desiredState = desiredState with { PixelFormat = Some(pixelFormat) };
|
||||
// }
|
||||
|
||||
currentState = SetDeinterlace(videoInputFile, context, currentState); |
||||
currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState); |
||||
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); |
||||
|
||||
if (currentState.BitDepth == 8 && context.HasSubtitleOverlay || context.HasWatermark) |
||||
{ |
||||
Option<IPixelFormat> desiredPixelFormat = Some((IPixelFormat)new PixelFormatYuv420P()); |
||||
|
||||
if (desiredPixelFormat != currentState.PixelFormat) |
||||
{ |
||||
if (currentState.FrameDataLocation == FrameDataLocation.Software) |
||||
{ |
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
var filter = new PixelFormatFilter(pixelFormat); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
var filter = new ScaleCudaFilter( |
||||
currentState with { PixelFormat = Some(pixelFormat) }, |
||||
currentState.ScaledSize, |
||||
currentState.PaddedSize, |
||||
false); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// need to upload for any sort of overlay
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Software && |
||||
currentState.BitDepth == 8 && (context.HasSubtitleOverlay || context.HasWatermark)) |
||||
{ |
||||
var hardwareUpload = new HardwareUploadCudaFilter(currentState); |
||||
currentState = hardwareUpload.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(hardwareUpload); |
||||
} |
||||
|
||||
currentState = SetSubtitle( |
||||
videoInputFile, |
||||
subtitleInputFile, |
||||
context, |
||||
ffmpegState, |
||||
currentState, |
||||
desiredState, |
||||
fontsFolder, |
||||
subtitleOverlayFilterSteps); |
||||
|
||||
currentState = SetWatermark( |
||||
videoStream, |
||||
watermarkInputFile, |
||||
context, |
||||
ffmpegState, |
||||
desiredState, |
||||
currentState, |
||||
watermarkOverlayFilterSteps); |
||||
|
||||
// after everything else is done, apply the encoder
|
||||
if (pipelineSteps.OfType<IEncoder>().All(e => e.Kind != StreamKind.Video)) |
||||
{ |
||||
Option<IEncoder> maybeEncoder = |
||||
(ffmpegState.EncoderHardwareAccelerationMode, desiredState.VideoFormat) switch |
||||
{ |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc(), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc(), |
||||
|
||||
(_, _) => GetSoftwareEncoder(currentState, desiredState) |
||||
}; |
||||
|
||||
foreach (IEncoder encoder in maybeEncoder) |
||||
{ |
||||
pipelineSteps.Add(encoder); |
||||
videoInputFile.FilterSteps.Add(encoder); |
||||
} |
||||
} |
||||
|
||||
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat( |
||||
videoStream, |
||||
desiredState.PixelFormat, |
||||
ffmpegState, |
||||
currentState, |
||||
context, |
||||
pipelineSteps); |
||||
|
||||
return new FilterChain( |
||||
videoInputFile.FilterSteps, |
||||
watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
subtitleInputFile.Map(st => st.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
watermarkOverlayFilterSteps, |
||||
subtitleOverlayFilterSteps, |
||||
pixelFormatFilterSteps); |
||||
} |
||||
|
||||
private List<IPipelineFilterStep> SetPixelFormat( |
||||
VideoStream videoStream, |
||||
Option<IPixelFormat> desiredPixelFormat, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var result = new List<IPipelineFilterStep>(); |
||||
|
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
IPixelFormat format = pixelFormat; |
||||
|
||||
if (pixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
format = pf; |
||||
} |
||||
} |
||||
|
||||
if (!videoStream.ColorParams.IsBt709) |
||||
{ |
||||
_logger.LogDebug("Adding colorspace filter"); |
||||
var colorspace = new ColorspaceFilter(videoStream, format, currentState.FrameDataLocation); |
||||
|
||||
currentState = colorspace.NextState(currentState); |
||||
result.Add(colorspace); |
||||
} |
||||
|
||||
if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) |
||||
{ |
||||
_logger.LogDebug("Using software encoder"); |
||||
|
||||
if ((context.HasSubtitleOverlay || context.HasWatermark) && |
||||
currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
_logger.LogDebug( |
||||
"HasSubtitleOverlay || HasWatermark && FrameDataLocation == FrameDataLocation.Hardware"); |
||||
|
||||
var hardwareDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
result.Add(hardwareDownload); |
||||
} |
||||
} |
||||
|
||||
if (currentState.PixelFormat.Map(f => f.FFmpegName) != format.FFmpegName) |
||||
{ |
||||
_logger.LogDebug( |
||||
"Format {A} doesn't equal {B}", |
||||
currentState.PixelFormat.Map(f => f.FFmpegName), |
||||
format.FFmpegName); |
||||
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
result.Add(new CudaFormatFilter(format)); |
||||
} |
||||
else |
||||
{ |
||||
pipelineSteps.Add(new PixelFormatOutputOption(format)); |
||||
} |
||||
} |
||||
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware && |
||||
ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) |
||||
{ |
||||
var hardwareDownload = new CudaHardwareDownloadFilter(Some(format)); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
result.Add(hardwareDownload); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private FrameState SetWatermark( |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState, |
||||
List<IPipelineFilterStep> watermarkOverlayFilterSteps) |
||||
{ |
||||
if (context.HasWatermark) |
||||
{ |
||||
WatermarkInputFile watermark = watermarkInputFile.Head(); |
||||
|
||||
foreach (VideoStream watermarkStream in watermark.VideoStreams) |
||||
{ |
||||
if (watermarkStream.StillImage == false) |
||||
{ |
||||
watermark.AddOption(new DoNotIgnoreLoopInputOption()); |
||||
} |
||||
else if (watermark.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)) |
||||
{ |
||||
// looping is required to fade a static image in and out
|
||||
watermark.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); |
||||
} |
||||
} |
||||
|
||||
if (watermark.DesiredState.Size == WatermarkSize.Scaled) |
||||
{ |
||||
watermark.FilterSteps.Add( |
||||
new WatermarkScaleFilter(watermark.DesiredState, currentState.PaddedSize)); |
||||
} |
||||
|
||||
if (watermark.DesiredState.Opacity != 100) |
||||
{ |
||||
watermark.FilterSteps.Add(new WatermarkOpacityFilter(watermark.DesiredState)); |
||||
} |
||||
|
||||
watermark.FilterSteps.Add(new PixelFormatFilter(new PixelFormatYuva420P())); |
||||
|
||||
foreach (List<WatermarkFadePoint> fadePoints in watermark.DesiredState.MaybeFadePoints) |
||||
{ |
||||
watermark.FilterSteps.AddRange(fadePoints.Map(fp => new WatermarkFadeFilter(fp))); |
||||
} |
||||
|
||||
watermark.FilterSteps.Add( |
||||
new HardwareUploadCudaFilter(currentState with { FrameDataLocation = FrameDataLocation.Software })); |
||||
|
||||
var watermarkFilter = new OverlayWatermarkCudaFilter( |
||||
watermark.DesiredState, |
||||
desiredState.PaddedSize, |
||||
videoStream.SquarePixelFrameSize(currentState.PaddedSize), |
||||
_logger); |
||||
watermarkOverlayFilterSteps.Add(watermarkFilter); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetSubtitle( |
||||
VideoInputFile videoInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineFilterStep> subtitleOverlayFilterSteps) |
||||
{ |
||||
foreach (SubtitleInputFile subtitle in subtitleInputFile) |
||||
{ |
||||
if (context.HasSubtitleText) |
||||
{ |
||||
videoInputFile.AddOption(new CopyTimestampInputOption()); |
||||
|
||||
if (videoInputFile.FilterSteps.Count == 0 && videoInputFile.InputOptions.OfType<CuvidDecoder>().Any()) |
||||
{ |
||||
// change the hw accel output to software so the explicit download isn't needed
|
||||
foreach (CuvidDecoder decoder in videoInputFile.InputOptions.OfType<CuvidDecoder>()) |
||||
{ |
||||
decoder.HardwareAccelerationMode = HardwareAccelerationMode.None; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
var downloadFilter = new HardwareDownloadFilter(currentState); |
||||
currentState = downloadFilter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(downloadFilter); |
||||
} |
||||
|
||||
var subtitlesFilter = new SubtitlesFilter(fontsFolder, subtitle); |
||||
currentState = subtitlesFilter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(subtitlesFilter); |
||||
|
||||
if (context.HasWatermark) |
||||
{ |
||||
var subtitleHardwareUpload = new HardwareUploadCudaFilter(currentState); |
||||
currentState = subtitleHardwareUpload.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(subtitleHardwareUpload); |
||||
} |
||||
} |
||||
else if (context.HasSubtitleOverlay) |
||||
{ |
||||
var pixelFormatFilter = new PixelFormatFilter(new PixelFormatYuva420P()); |
||||
subtitle.FilterSteps.Add(pixelFormatFilter); |
||||
|
||||
if (currentState.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 8) |
||||
{ |
||||
var subtitleHardwareUpload = new HardwareUploadCudaFilter( |
||||
currentState with { FrameDataLocation = FrameDataLocation.Software }); |
||||
subtitle.FilterSteps.Add(subtitleHardwareUpload); |
||||
|
||||
// only scale if scaling or padding was used for main video stream
|
||||
if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter or PadFilter)) |
||||
{ |
||||
var scaleFilter = new SubtitleScaleNppFilter( |
||||
currentState, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize); |
||||
subtitle.FilterSteps.Add(scaleFilter); |
||||
} |
||||
|
||||
var subtitlesFilter = new OverlaySubtitleCudaFilter(); |
||||
subtitleOverlayFilterSteps.Add(subtitlesFilter); |
||||
} |
||||
else |
||||
{ |
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
var cudaDownload = new CudaHardwareDownloadFilter(currentState.PixelFormat); |
||||
currentState = cudaDownload.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(cudaDownload); |
||||
} |
||||
|
||||
// only scale if scaling or padding was used for main video stream
|
||||
if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or ScaleCudaFilter or PadFilter)) |
||||
{ |
||||
var scaleFilter = new ScaleImageFilter(desiredState.PaddedSize); |
||||
subtitle.FilterSteps.Add(scaleFilter); |
||||
} |
||||
|
||||
var subtitlesFilter = |
||||
new OverlaySubtitleFilter(desiredState.PixelFormat.IfNone(new PixelFormatYuv420P())); |
||||
subtitleOverlayFilterSteps.Add(subtitlesFilter); |
||||
|
||||
// overlay produces YUVA420P10, so we need to strip the alpha
|
||||
if (currentState.BitDepth == 10) |
||||
{ |
||||
subtitleOverlayFilterSteps.Add(new PixelFormatFilter(new PixelFormatYuv420P10Le())); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetPad( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
if (currentState.PaddedSize != desiredState.PaddedSize) |
||||
{ |
||||
IPipelineFilterStep padStep = new PadFilter(currentState, desiredState.PaddedSize); |
||||
currentState = padStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(padStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetScale( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
IPipelineFilterStep scaleStep; |
||||
|
||||
if (currentState.ScaledSize != desiredState.ScaledSize && ffmpegState is |
||||
{ |
||||
DecoderHardwareAccelerationMode: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None |
||||
} && context is { HasWatermark: false, HasSubtitleOverlay: false, ShouldDeinterlace: false }) |
||||
{ |
||||
scaleStep = new ScaleFilter( |
||||
currentState, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize, |
||||
videoStream.IsAnamorphicEdgeCase); |
||||
} |
||||
else |
||||
{ |
||||
scaleStep = new ScaleCudaFilter( |
||||
currentState with |
||||
{ |
||||
PixelFormat = !context.Is10BitOutput && (context.HasWatermark || |
||||
context.HasSubtitleOverlay || |
||||
context.ShouldDeinterlace || |
||||
(desiredState.ScaledSize != desiredState.PaddedSize) || |
||||
context.HasSubtitleText || |
||||
ffmpegState is |
||||
{ |
||||
DecoderHardwareAccelerationMode: HardwareAccelerationMode.Nvenc, |
||||
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None |
||||
}) |
||||
? desiredState.PixelFormat.Map(pf => (IPixelFormat)new PixelFormatNv12(pf.Name)) |
||||
: Option<IPixelFormat>.None |
||||
}, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize, |
||||
videoStream.IsAnamorphicEdgeCase); |
||||
} |
||||
|
||||
if (!string.IsNullOrWhiteSpace(scaleStep.Filter)) |
||||
{ |
||||
currentState = scaleStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(scaleStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetDeinterlace(VideoInputFile videoInputFile, PipelineContext context, FrameState currentState) |
||||
{ |
||||
if (context.ShouldDeinterlace) |
||||
{ |
||||
if (currentState.FrameDataLocation == FrameDataLocation.Software) |
||||
{ |
||||
var filter = new YadifFilter(currentState); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
else |
||||
{ |
||||
var filter = new YadifCudaFilter(currentState); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
} |
||||
@ -0,0 +1,605 @@
@@ -0,0 +1,605 @@
|
||||
using System.Diagnostics; |
||||
using ErsatzTV.FFmpeg.Decoder; |
||||
using ErsatzTV.FFmpeg.Encoder; |
||||
using ErsatzTV.FFmpeg.Environment; |
||||
using ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.Option; |
||||
using ErsatzTV.FFmpeg.Option.Metadata; |
||||
using ErsatzTV.FFmpeg.OutputFormat; |
||||
using ErsatzTV.FFmpeg.Protocol; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public abstract class PipelineBuilderBase : IPipelineBuilder |
||||
{ |
||||
private readonly HardwareAccelerationMode _hardwareAccelerationMode; |
||||
private readonly Option<VideoInputFile> _videoInputFile; |
||||
private readonly Option<AudioInputFile> _audioInputFile; |
||||
private readonly Option<WatermarkInputFile> _watermarkInputFile; |
||||
private readonly Option<SubtitleInputFile> _subtitleInputFile; |
||||
private readonly string _reportsFolder; |
||||
private readonly string _fontsFolder; |
||||
private readonly ILogger _logger; |
||||
|
||||
protected PipelineBuilderBase( |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
ILogger logger) |
||||
{ |
||||
_hardwareAccelerationMode = hardwareAccelerationMode; |
||||
_videoInputFile = videoInputFile; |
||||
_audioInputFile = audioInputFile; |
||||
_watermarkInputFile = watermarkInputFile; |
||||
_subtitleInputFile = subtitleInputFile; |
||||
_reportsFolder = reportsFolder; |
||||
_fontsFolder = fontsFolder; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public FFmpegPipeline Resize(string outputFile, FrameSize scaledSize) |
||||
{ |
||||
var pipelineSteps = new List<IPipelineStep> |
||||
{ |
||||
new NoStandardInputOption(), |
||||
new HideBannerOption(), |
||||
new NoStatsOption(), |
||||
new LoglevelErrorOption() |
||||
}; |
||||
|
||||
IPipelineFilterStep scaleStep = new ScaleImageFilter(scaledSize); |
||||
_videoInputFile.Iter(f => f.FilterSteps.Add(scaleStep)); |
||||
|
||||
pipelineSteps.Add(new VideoFilter(new[] { scaleStep })); |
||||
pipelineSteps.Add(scaleStep); |
||||
pipelineSteps.Add(new FileNameOutputOption(outputFile)); |
||||
|
||||
return new FFmpegPipeline(pipelineSteps); |
||||
} |
||||
|
||||
public FFmpegPipeline Concat(ConcatInputFile concatInputFile, FFmpegState ffmpegState) |
||||
{ |
||||
var pipelineSteps = new List<IPipelineStep> |
||||
{ |
||||
new NoStandardInputOption(), |
||||
new HideBannerOption(), |
||||
new NoStatsOption(), |
||||
new LoglevelErrorOption(), |
||||
new StandardFormatFlags(), |
||||
new NoDemuxDecodeDelayOutputOption(), |
||||
new FastStartOutputOption(), |
||||
new ClosedGopOutputOption() |
||||
}; |
||||
|
||||
concatInputFile.AddOption(new ConcatInputFormat()); |
||||
concatInputFile.AddOption(new RealtimeInputOption()); |
||||
concatInputFile.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); |
||||
|
||||
foreach (int threadCount in ffmpegState.ThreadCount) |
||||
{ |
||||
pipelineSteps.Insert(0, new ThreadCountOption(threadCount)); |
||||
} |
||||
|
||||
pipelineSteps.Add(new NoSceneDetectOutputOption(0)); |
||||
pipelineSteps.Add(new EncoderCopyAll()); |
||||
|
||||
if (ffmpegState.DoNotMapMetadata) |
||||
{ |
||||
pipelineSteps.Add(new DoNotMapMetadataOutputOption()); |
||||
} |
||||
|
||||
pipelineSteps.AddRange( |
||||
ffmpegState.MetadataServiceProvider.Map(sp => new MetadataServiceProviderOutputOption(sp))); |
||||
|
||||
pipelineSteps.AddRange(ffmpegState.MetadataServiceName.Map(sn => new MetadataServiceNameOutputOption(sn))); |
||||
|
||||
pipelineSteps.Add(new OutputFormatMpegTs()); |
||||
pipelineSteps.Add(new PipeProtocol()); |
||||
|
||||
if (ffmpegState.SaveReport) |
||||
{ |
||||
pipelineSteps.Add(new FFReportVariable(_reportsFolder, concatInputFile)); |
||||
} |
||||
|
||||
return new FFmpegPipeline(pipelineSteps); |
||||
} |
||||
|
||||
public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) |
||||
{ |
||||
var pipelineSteps = new List<IPipelineStep> |
||||
{ |
||||
new NoStandardInputOption(), |
||||
new HideBannerOption(), |
||||
new NoStatsOption(), |
||||
new LoglevelErrorOption(), |
||||
new StandardFormatFlags(), |
||||
new NoDemuxDecodeDelayOutputOption(), |
||||
new FastStartOutputOption(), |
||||
new ClosedGopOutputOption() |
||||
}; |
||||
|
||||
Debug.Assert(_videoInputFile.IsSome, "Pipeline builder requires exactly one video input file"); |
||||
VideoInputFile videoInputFile = _videoInputFile.Head(); |
||||
|
||||
var allVideoStreams = _videoInputFile.SelectMany(f => f.VideoStreams).ToList(); |
||||
Debug.Assert(allVideoStreams.Count == 1, "Pipeline builder requires exactly one video stream"); |
||||
VideoStream videoStream = allVideoStreams.Head(); |
||||
|
||||
var context = new PipelineContext( |
||||
HardwareAccelerationMode: _hardwareAccelerationMode, |
||||
HasWatermark: _watermarkInputFile.IsSome, |
||||
HasSubtitleOverlay: _subtitleInputFile.Map(s => s is { IsImageBased: true, Copy: false }).IfNone(false), |
||||
HasSubtitleText: _subtitleInputFile.Map(s => s is { IsImageBased: false }).IfNone(false), |
||||
ShouldDeinterlace: desiredState.Deinterlaced, |
||||
Is10BitOutput: desiredState.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10); |
||||
|
||||
SetThreadCount(ffmpegState, desiredState, pipelineSteps); |
||||
SetSceneDetect(videoStream, ffmpegState, desiredState, pipelineSteps); |
||||
SetFFReport(ffmpegState, pipelineSteps); |
||||
SetStreamSeek(ffmpegState, videoInputFile, context, pipelineSteps); |
||||
SetTimeLimit(ffmpegState, pipelineSteps); |
||||
|
||||
FilterChain filterChain = FilterChain.Empty; |
||||
|
||||
if (desiredState.VideoFormat == VideoFormat.Copy) |
||||
{ |
||||
pipelineSteps.Add(new EncoderCopyVideo()); |
||||
} |
||||
else |
||||
{ |
||||
filterChain = BuildVideoPipeline( |
||||
videoInputFile, |
||||
videoStream, |
||||
ffmpegState, |
||||
desiredState, |
||||
context, |
||||
pipelineSteps); |
||||
} |
||||
|
||||
if (_audioInputFile.IsNone) |
||||
{ |
||||
pipelineSteps.Add(new EncoderCopyAudio()); |
||||
} |
||||
else |
||||
{ |
||||
foreach (AudioInputFile audioInputFile in _audioInputFile) |
||||
{ |
||||
BuildAudioPipeline(audioInputFile, pipelineSteps); |
||||
} |
||||
} |
||||
|
||||
SetDoNotMapMetadata(ffmpegState, pipelineSteps); |
||||
SetMetadataServiceProvider(ffmpegState, pipelineSteps); |
||||
SetMetadataServiceName(ffmpegState, pipelineSteps); |
||||
SetMetadataAudioLanguage(ffmpegState, pipelineSteps); |
||||
SetOutputFormat(ffmpegState, desiredState, pipelineSteps, videoStream); |
||||
|
||||
var complexFilter = new NewComplexFilter( |
||||
_videoInputFile, |
||||
_audioInputFile, |
||||
_watermarkInputFile, |
||||
_subtitleInputFile, |
||||
context, |
||||
filterChain); |
||||
|
||||
pipelineSteps.Add(complexFilter); |
||||
|
||||
return new FFmpegPipeline(pipelineSteps); |
||||
} |
||||
|
||||
protected Option<IDecoder> LogUnknownDecoder( |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
string videoFormat, |
||||
string pixelFormat) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Unable to determine decoder for {AccelMode} - {VideoFormat} - {PixelFormat}; may have playback issues", |
||||
hardwareAccelerationMode, |
||||
videoFormat, |
||||
pixelFormat); |
||||
return Option<IDecoder>.None; |
||||
} |
||||
|
||||
protected Option<IEncoder> LogUnknownEncoder(HardwareAccelerationMode hardwareAccelerationMode, string videoFormat) |
||||
{ |
||||
_logger.LogWarning( |
||||
"Unable to determine video encoder for {AccelMode} - {VideoFormat}; may have playback issues", |
||||
hardwareAccelerationMode, |
||||
videoFormat); |
||||
return Option<IEncoder>.None; |
||||
} |
||||
|
||||
private static void SetOutputFormat( |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
List<IPipelineStep> pipelineSteps, |
||||
VideoStream videoStream) |
||||
{ |
||||
switch (ffmpegState.OutputFormat) |
||||
{ |
||||
case OutputFormatKind.MpegTs: |
||||
pipelineSteps.Add(new OutputFormatMpegTs()); |
||||
pipelineSteps.Add(new PipeProtocol()); |
||||
break; |
||||
case OutputFormatKind.Hls: |
||||
foreach (string playlistPath in ffmpegState.HlsPlaylistPath) |
||||
{ |
||||
foreach (string segmentTemplate in ffmpegState.HlsSegmentTemplate) |
||||
{ |
||||
pipelineSteps.Add( |
||||
new OutputFormatHls( |
||||
desiredState, |
||||
videoStream.FrameRate, |
||||
segmentTemplate, |
||||
playlistPath)); |
||||
} |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
private static void SetMetadataAudioLanguage(FFmpegState ffmpegState, List<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (string desiredAudioLanguage in ffmpegState.MetadataAudioLanguage) |
||||
{ |
||||
pipelineSteps.Add(new MetadataAudioLanguageOutputOption(desiredAudioLanguage)); |
||||
} |
||||
} |
||||
|
||||
private static void SetMetadataServiceName(FFmpegState ffmpegState, List<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (string desiredServiceName in ffmpegState.MetadataServiceName) |
||||
{ |
||||
pipelineSteps.Add(new MetadataServiceNameOutputOption(desiredServiceName)); |
||||
} |
||||
} |
||||
|
||||
private static void SetMetadataServiceProvider(FFmpegState ffmpegState, List<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (string desiredServiceProvider in ffmpegState.MetadataServiceProvider) |
||||
{ |
||||
pipelineSteps.Add(new MetadataServiceProviderOutputOption(desiredServiceProvider)); |
||||
} |
||||
} |
||||
|
||||
private static void SetDoNotMapMetadata(FFmpegState ffmpegState, List<IPipelineStep> pipelineSteps) |
||||
{ |
||||
if (ffmpegState.DoNotMapMetadata) |
||||
{ |
||||
pipelineSteps.Add(new DoNotMapMetadataOutputOption()); |
||||
} |
||||
} |
||||
|
||||
private void BuildAudioPipeline(AudioInputFile audioInputFile, IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
// always need to specify audio codec so ffmpeg doesn't default to a codec we don't want
|
||||
foreach (IEncoder step in AvailableEncoders.ForAudioFormat(audioInputFile.DesiredState, _logger)) |
||||
{ |
||||
pipelineSteps.Add(step); |
||||
} |
||||
|
||||
SetAudioChannels(audioInputFile, pipelineSteps); |
||||
SetAudioBitrate(audioInputFile, pipelineSteps); |
||||
SetAudioBufferSize(audioInputFile, pipelineSteps); |
||||
SetAudioSampleRate(audioInputFile, pipelineSteps); |
||||
SetAudioLoudness(audioInputFile); |
||||
SetAudioPad(audioInputFile); |
||||
} |
||||
|
||||
private void SetAudioPad(AudioInputFile audioInputFile) |
||||
{ |
||||
foreach (TimeSpan desiredDuration in audioInputFile.DesiredState.AudioDuration) |
||||
{ |
||||
_audioInputFile.Iter(f => f.FilterSteps.Add(new AudioPadFilter(desiredDuration))); |
||||
} |
||||
} |
||||
|
||||
private void SetAudioLoudness(AudioInputFile audioInputFile) |
||||
{ |
||||
if (audioInputFile.DesiredState.NormalizeLoudness) |
||||
{ |
||||
_audioInputFile.Iter(f => f.FilterSteps.Add(new NormalizeLoudnessFilter())); |
||||
} |
||||
} |
||||
|
||||
private static void SetAudioSampleRate(AudioInputFile audioInputFile, IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredSampleRate in audioInputFile.DesiredState.AudioSampleRate) |
||||
{ |
||||
pipelineSteps.Add(new AudioSampleRateOutputOption(desiredSampleRate)); |
||||
} |
||||
} |
||||
|
||||
private static void SetAudioBufferSize(AudioInputFile audioInputFile, IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredBufferSize in audioInputFile.DesiredState.AudioBufferSize) |
||||
{ |
||||
pipelineSteps.Add(new AudioBufferSizeOutputOption(desiredBufferSize)); |
||||
} |
||||
} |
||||
|
||||
private static void SetAudioBitrate(AudioInputFile audioInputFile, IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredBitrate in audioInputFile.DesiredState.AudioBitrate) |
||||
{ |
||||
pipelineSteps.Add(new AudioBitrateOutputOption(desiredBitrate)); |
||||
} |
||||
} |
||||
|
||||
private static void SetAudioChannels(AudioInputFile audioInputFile, IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (AudioStream audioStream in audioInputFile.AudioStreams.HeadOrNone()) |
||||
{ |
||||
foreach (int desiredAudioChannels in audioInputFile.DesiredState.AudioChannels) |
||||
{ |
||||
pipelineSteps.Add( |
||||
new AudioChannelsOutputOption( |
||||
audioInputFile.DesiredState.AudioFormat, |
||||
audioStream.Channels, |
||||
desiredAudioChannels)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected abstract FFmpegState SetAccelState( |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps); |
||||
|
||||
private FilterChain BuildVideoPipeline( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
if (_subtitleInputFile.Map(s => s.Copy) == Some(true)) |
||||
{ |
||||
pipelineSteps.Add(new EncoderCopySubtitle()); |
||||
} |
||||
|
||||
ffmpegState = SetAccelState(videoStream, ffmpegState, desiredState, context, pipelineSteps); |
||||
|
||||
SetDecoder(videoInputFile, videoStream, ffmpegState, context, pipelineSteps); |
||||
|
||||
SetStillImageInfiniteLoop(videoInputFile, videoStream, ffmpegState); |
||||
SetRealtimeInput(videoInputFile, desiredState); |
||||
SetInfiniteLoop(videoInputFile, videoStream, ffmpegState, desiredState); |
||||
SetFrameRateOutput(desiredState, pipelineSteps); |
||||
SetVideoTrackTimescaleOutput(desiredState, pipelineSteps); |
||||
SetVideoBitrateOutput(desiredState, pipelineSteps); |
||||
SetVideoBufferSizeOutput(desiredState, pipelineSteps); |
||||
|
||||
FilterChain filterChain = SetVideoFilters( |
||||
videoInputFile, |
||||
videoStream, |
||||
_watermarkInputFile, |
||||
_subtitleInputFile, |
||||
context, |
||||
ffmpegState, |
||||
desiredState, |
||||
_fontsFolder, |
||||
pipelineSteps); |
||||
|
||||
SetOutputTsOffset(ffmpegState, desiredState, pipelineSteps); |
||||
|
||||
return filterChain; |
||||
} |
||||
|
||||
protected abstract void SetDecoder( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps); |
||||
|
||||
protected Option<IDecoder> GetSoftwareDecoder(VideoStream videoStream) => |
||||
videoStream.Codec switch |
||||
{ |
||||
VideoFormat.Hevc => new DecoderHevc(), |
||||
VideoFormat.H264 => new DecoderH264(), |
||||
VideoFormat.Mpeg1Video => new DecoderMpeg1Video(), |
||||
VideoFormat.Mpeg2Video => new DecoderMpeg2Video(), |
||||
VideoFormat.Vc1 => new DecoderVc1(), |
||||
VideoFormat.MsMpeg4V2 => new DecoderMsMpeg4V2(), |
||||
VideoFormat.MsMpeg4V3 => new DecoderMsMpeg4V3(), |
||||
VideoFormat.Mpeg4 => new DecoderMpeg4(), |
||||
VideoFormat.Vp9 => new DecoderVp9(), |
||||
|
||||
VideoFormat.Undetermined => new DecoderImplicit(), |
||||
VideoFormat.Copy => new DecoderImplicit(), |
||||
VideoFormat.GeneratedImage => new DecoderImplicit(), |
||||
|
||||
_ => LogUnknownDecoder( |
||||
HardwareAccelerationMode.None, |
||||
videoStream.Codec, |
||||
videoStream.PixelFormat.Match(pf => pf.Name, () => string.Empty)) |
||||
}; |
||||
|
||||
protected Option<IEncoder> GetSoftwareEncoder(FrameState currentState, FrameState desiredState) => |
||||
desiredState.VideoFormat switch |
||||
{ |
||||
VideoFormat.Hevc => new EncoderLibx265( |
||||
currentState with { FrameDataLocation = FrameDataLocation.Software }), |
||||
VideoFormat.H264 => new EncoderLibx264(), |
||||
VideoFormat.Mpeg2Video => new EncoderMpeg2Video(), |
||||
|
||||
VideoFormat.Copy => new EncoderCopyVideo(), |
||||
VideoFormat.Undetermined => new EncoderImplicitVideo(), |
||||
|
||||
_ => LogUnknownEncoder(HardwareAccelerationMode.None, desiredState.VideoFormat) |
||||
}; |
||||
|
||||
protected abstract FilterChain SetVideoFilters( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineStep> pipelineSteps); |
||||
|
||||
private static void SetOutputTsOffset(FFmpegState ffmpegState, FrameState desiredState, ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
if (ffmpegState.PtsOffset > 0) |
||||
{ |
||||
foreach (int videoTrackTimeScale in desiredState.VideoTrackTimeScale) |
||||
{ |
||||
pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset, videoTrackTimeScale)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static void SetVideoBufferSizeOutput(FrameState desiredState, ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredBufferSize in desiredState.VideoBufferSize) |
||||
{ |
||||
pipelineSteps.Add(new VideoBufferSizeOutputOption(desiredBufferSize)); |
||||
} |
||||
} |
||||
|
||||
private static void SetVideoBitrateOutput(FrameState desiredState, ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredBitrate in desiredState.VideoBitrate) |
||||
{ |
||||
pipelineSteps.Add(new VideoBitrateOutputOption(desiredBitrate)); |
||||
} |
||||
} |
||||
|
||||
private static void SetVideoTrackTimescaleOutput(FrameState desiredState, ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredTimeScale in desiredState.VideoTrackTimeScale) |
||||
{ |
||||
pipelineSteps.Add(new VideoTrackTimescaleOutputOption(desiredTimeScale)); |
||||
} |
||||
} |
||||
|
||||
private static void SetFrameRateOutput(FrameState desiredState, ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (int desiredFrameRate in desiredState.FrameRate) |
||||
{ |
||||
pipelineSteps.Add(new FrameRateOutputOption(desiredFrameRate)); |
||||
} |
||||
} |
||||
|
||||
private void SetInfiniteLoop( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState) |
||||
{ |
||||
if (desiredState.InfiniteLoop) |
||||
{ |
||||
_audioInputFile.Iter( |
||||
a => a.AddOption(new InfiniteLoopInputOption(ffmpegState.EncoderHardwareAccelerationMode))); |
||||
|
||||
if (!videoStream.StillImage) |
||||
{ |
||||
videoInputFile.AddOption(new InfiniteLoopInputOption(ffmpegState.EncoderHardwareAccelerationMode)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void SetRealtimeInput(VideoInputFile videoInputFile, FrameState desiredState) |
||||
{ |
||||
if (desiredState.Realtime) |
||||
{ |
||||
_audioInputFile.Iter(a => a.AddOption(new RealtimeInputOption())); |
||||
videoInputFile.AddOption(new RealtimeInputOption()); |
||||
} |
||||
} |
||||
|
||||
private static void SetStillImageInfiniteLoop( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState) |
||||
{ |
||||
if (videoStream.StillImage) |
||||
{ |
||||
videoInputFile.AddOption(new InfiniteLoopInputOption(ffmpegState.EncoderHardwareAccelerationMode)); |
||||
} |
||||
} |
||||
|
||||
private void SetThreadCount(FFmpegState ffmpegState, FrameState desiredState, IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
if (ffmpegState.Start.Exists(s => s > TimeSpan.Zero) && desiredState.Realtime) |
||||
{ |
||||
_logger.LogInformation( |
||||
"Forcing {Threads} ffmpeg thread due to buggy combination of stream seek and realtime output", |
||||
1); |
||||
|
||||
pipelineSteps.Insert(0, new ThreadCountOption(1)); |
||||
} |
||||
else |
||||
{ |
||||
foreach (int threadCount in ffmpegState.ThreadCount) |
||||
{ |
||||
pipelineSteps.Insert(0, new ThreadCountOption(threadCount)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static void SetSceneDetect( |
||||
// ReSharper disable once SuggestBaseTypeForParameter
|
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
// -sc_threshold 0 is unsupported with mpeg2video
|
||||
if (videoStream.Codec == VideoFormat.Mpeg2Video || desiredState.VideoFormat == VideoFormat.Mpeg2Video || |
||||
ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox) |
||||
{ |
||||
pipelineSteps.Add(new NoSceneDetectOutputOption(1_000_000_000)); |
||||
} |
||||
else |
||||
{ |
||||
pipelineSteps.Add(new NoSceneDetectOutputOption(0)); |
||||
} |
||||
} |
||||
|
||||
private void SetFFReport(FFmpegState ffmpegState, ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
if (ffmpegState.SaveReport) |
||||
{ |
||||
pipelineSteps.Add(new FFReportVariable(_reportsFolder, None)); |
||||
} |
||||
} |
||||
|
||||
private void SetStreamSeek( |
||||
FFmpegState ffmpegState, |
||||
VideoInputFile videoInputFile, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (TimeSpan desiredStart in ffmpegState.Start.Filter(s => s > TimeSpan.Zero)) |
||||
{ |
||||
var option = new StreamSeekInputOption(desiredStart); |
||||
_audioInputFile.Iter(a => a.AddOption(option)); |
||||
videoInputFile.AddOption(option); |
||||
|
||||
// need to seek text subtitle files
|
||||
if (context.HasSubtitleText) |
||||
{ |
||||
pipelineSteps.Add(new StreamSeekFilterOption(desiredStart)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static void SetTimeLimit(FFmpegState ffmpegState, List<IPipelineStep> pipelineSteps) |
||||
{ |
||||
pipelineSteps.AddRange(ffmpegState.Finish.Map(finish => new TimeLimitOutputOption(finish))); |
||||
} |
||||
} |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
using ErsatzTV.FFmpeg.Capabilities; |
||||
using ErsatzTV.FFmpeg.Runtime; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public class PipelineBuilderFactory : IPipelineBuilderFactory |
||||
{ |
||||
private readonly IRuntimeInfo _runtimeInfo; |
||||
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory; |
||||
private readonly ILogger<PipelineBuilderFactory> _logger; |
||||
|
||||
public PipelineBuilderFactory( |
||||
IRuntimeInfo runtimeInfo, |
||||
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory, |
||||
ILogger<PipelineBuilderFactory> logger) |
||||
{ |
||||
_runtimeInfo = runtimeInfo; |
||||
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<IPipelineBuilder> GetBuilder( |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
string ffmpegPath) => hardwareAccelerationMode switch |
||||
{ |
||||
HardwareAccelerationMode.Nvenc => new NvidiaPipelineBuilder( |
||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hardwareAccelerationMode), |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
_logger), |
||||
HardwareAccelerationMode.Vaapi => new VaapiPipelineBuilder( |
||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hardwareAccelerationMode), |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
_logger), |
||||
HardwareAccelerationMode.Qsv => new QsvPipelineBuilder( |
||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hardwareAccelerationMode), |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
_logger), |
||||
HardwareAccelerationMode.VideoToolbox => new VideoToolboxPipelineBuilder( |
||||
await _hardwareCapabilitiesFactory.GetHardwareCapabilities(ffmpegPath, hardwareAccelerationMode), |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
_logger), |
||||
_ => new SoftwarePipelineBuilder( |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
_logger) |
||||
}; |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public record PipelineContext( |
||||
HardwareAccelerationMode HardwareAccelerationMode, |
||||
bool HasWatermark, |
||||
bool HasSubtitleOverlay, |
||||
bool HasSubtitleText, |
||||
bool ShouldDeinterlace, |
||||
bool Is10BitOutput); |
||||
|
||||
@ -0,0 +1,518 @@
@@ -0,0 +1,518 @@
|
||||
using ErsatzTV.FFmpeg.Capabilities; |
||||
using ErsatzTV.FFmpeg.Decoder; |
||||
using ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
using ErsatzTV.FFmpeg.Encoder; |
||||
using ErsatzTV.FFmpeg.Encoder.Qsv; |
||||
using ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Filter.Qsv; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.Option; |
||||
using ErsatzTV.FFmpeg.Option.HardwareAcceleration; |
||||
using ErsatzTV.FFmpeg.State; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public class QsvPipelineBuilder : SoftwarePipelineBuilder |
||||
{ |
||||
private readonly IHardwareCapabilities _hardwareCapabilities; |
||||
private readonly ILogger _logger; |
||||
|
||||
public QsvPipelineBuilder( |
||||
IHardwareCapabilities hardwareCapabilities, |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
ILogger logger) : base( |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
logger) |
||||
{ |
||||
_hardwareCapabilities = hardwareCapabilities; |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected override FFmpegState SetAccelState( |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
bool canDecode = _hardwareCapabilities.CanDecode(videoStream.Codec, videoStream.PixelFormat); |
||||
bool canEncode = _hardwareCapabilities.CanEncode(desiredState.VideoFormat, desiredState.PixelFormat); |
||||
|
||||
pipelineSteps.Add(new QsvHardwareAccelerationOption(ffmpegState.VaapiDevice)); |
||||
|
||||
// 10-bit hevc/h264 qsv decoders have issues, so use software
|
||||
if (canDecode && videoStream.Codec is VideoFormat.Hevc or VideoFormat.H264 && |
||||
videoStream.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10) |
||||
{ |
||||
canDecode = false; |
||||
} |
||||
|
||||
// disable hw accel if decoder/encoder isn't supported
|
||||
return ffmpegState with |
||||
{ |
||||
DecoderHardwareAccelerationMode = canDecode |
||||
? HardwareAccelerationMode.Qsv |
||||
: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode = canEncode |
||||
? HardwareAccelerationMode.Qsv |
||||
: HardwareAccelerationMode.None |
||||
}; |
||||
} |
||||
|
||||
protected override void SetDecoder( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch |
||||
{ |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new DecoderHevcQsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264) => new DecoderH264Qsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Mpeg2Video) => new DecoderMpeg2Qsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Vc1) => new DecoderVc1Qsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Vp9) => new DecoderVp9Qsv(), |
||||
|
||||
_ => GetSoftwareDecoder(videoStream) |
||||
}; |
||||
|
||||
foreach (IDecoder decoder in maybeDecoder) |
||||
{ |
||||
videoInputFile.AddOption(decoder); |
||||
} |
||||
} |
||||
|
||||
protected override FilterChain SetVideoFilters( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var watermarkOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
var subtitleOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
|
||||
FrameState currentState = desiredState with |
||||
{ |
||||
ScaledSize = videoStream.FrameSize, |
||||
PaddedSize = videoStream.FrameSize, |
||||
|
||||
// consider hardware frames to be wrapped in nv12
|
||||
PixelFormat = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv |
||||
? videoStream.PixelFormat.Map(pf => (IPixelFormat)new PixelFormatNv12(pf.Name)) |
||||
: videoStream.PixelFormat, |
||||
|
||||
IsAnamorphic = videoStream.IsAnamorphic, |
||||
FrameDataLocation = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Qsv |
||||
? FrameDataLocation.Hardware |
||||
: FrameDataLocation.Software |
||||
}; |
||||
|
||||
// easier to use nv12 for overlay
|
||||
if (context.HasSubtitleOverlay || context.HasWatermark) |
||||
{ |
||||
IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone( |
||||
context.Is10BitOutput ? new PixelFormatYuv420P10Le() : new PixelFormatYuv420P()); |
||||
desiredState = desiredState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; |
||||
} |
||||
|
||||
// _logger.LogDebug("After decode: {PixelFormat}", currentState.PixelFormat);
|
||||
currentState = SetDeinterlace(videoInputFile, context, ffmpegState, currentState); |
||||
// _logger.LogDebug("After deinterlace: {PixelFormat}", currentState.PixelFormat);
|
||||
currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState); |
||||
// _logger.LogDebug("After scale: {PixelFormat}", currentState.PixelFormat);
|
||||
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); |
||||
// _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat);
|
||||
|
||||
// need to download for any sort of overlay
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware && |
||||
(context.HasSubtitleOverlay || context.HasWatermark)) |
||||
{ |
||||
var hardwareDownload = new HardwareDownloadFilter(currentState); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(hardwareDownload); |
||||
} |
||||
|
||||
currentState = SetSubtitle( |
||||
videoInputFile, |
||||
subtitleInputFile, |
||||
context, |
||||
ffmpegState, |
||||
currentState, |
||||
desiredState, |
||||
fontsFolder, |
||||
subtitleOverlayFilterSteps); |
||||
|
||||
currentState = SetWatermark( |
||||
videoStream, |
||||
watermarkInputFile, |
||||
context, |
||||
ffmpegState, |
||||
desiredState, |
||||
currentState, |
||||
watermarkOverlayFilterSteps); |
||||
|
||||
// after everything else is done, apply the encoder
|
||||
if (pipelineSteps.OfType<IEncoder>().All(e => e.Kind != StreamKind.Video)) |
||||
{ |
||||
Option<IEncoder> maybeEncoder = |
||||
(ffmpegState.EncoderHardwareAccelerationMode, desiredState.VideoFormat) switch |
||||
{ |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264) => new EncoderH264Qsv(), |
||||
|
||||
(_, _) => GetSoftwareEncoder(currentState, desiredState) |
||||
}; |
||||
|
||||
foreach (IEncoder encoder in maybeEncoder) |
||||
{ |
||||
pipelineSteps.Add(encoder); |
||||
videoInputFile.FilterSteps.Add(encoder); |
||||
} |
||||
} |
||||
|
||||
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat( |
||||
videoStream, |
||||
desiredState.PixelFormat, |
||||
ffmpegState, |
||||
currentState, |
||||
context, |
||||
pipelineSteps); |
||||
|
||||
return new FilterChain( |
||||
videoInputFile.FilterSteps, |
||||
watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
subtitleInputFile.Map(st => st.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
watermarkOverlayFilterSteps, |
||||
subtitleOverlayFilterSteps, |
||||
pixelFormatFilterSteps); |
||||
} |
||||
|
||||
private List<IPipelineFilterStep> SetPixelFormat( |
||||
VideoStream videoStream, |
||||
Option<IPixelFormat> desiredPixelFormat, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var result = new List<IPipelineFilterStep>(); |
||||
|
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
IPixelFormat format = pixelFormat; |
||||
|
||||
if (pixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
format = pf; |
||||
} |
||||
} |
||||
|
||||
IPixelFormat formatForDownload = pixelFormat; |
||||
|
||||
if (!videoStream.ColorParams.IsBt709) |
||||
{ |
||||
_logger.LogDebug("Adding colorspace filter"); |
||||
var colorspace = new ColorspaceFilter(videoStream, format, currentState.FrameDataLocation); |
||||
|
||||
// force nv12 if we're still in hardware
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
if (formatForDownload is not PixelFormatNv12) |
||||
{ |
||||
formatForDownload = new PixelFormatNv12(pixelFormat.Name); |
||||
} |
||||
} |
||||
|
||||
currentState = colorspace.NextState(currentState); |
||||
result.Add(colorspace); |
||||
} |
||||
|
||||
if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) |
||||
{ |
||||
_logger.LogDebug("Using software encoder"); |
||||
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
_logger.LogDebug("FrameDataLocation == FrameDataLocation.Hardware"); |
||||
|
||||
var hardwareDownload = |
||||
new HardwareDownloadFilter(currentState with { PixelFormat = Some(formatForDownload) }); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
result.Add(hardwareDownload); |
||||
} |
||||
} |
||||
|
||||
if (currentState.PixelFormat.Map(f => f.FFmpegName) != format.FFmpegName) |
||||
{ |
||||
_logger.LogDebug( |
||||
"Format {A} doesn't equal {B}", |
||||
currentState.PixelFormat.Map(f => f.FFmpegName), |
||||
format.FFmpegName); |
||||
|
||||
// remind qsv that it uses qsv
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware && |
||||
result.Count == 1 && result[0] is ColorspaceFilter colorspace) |
||||
{ |
||||
if (colorspace.Filter.StartsWith("setparams=")) |
||||
{ |
||||
result.Insert(0, new QsvFormatFilter(new PixelFormatQsv(format.Name))); |
||||
} |
||||
} |
||||
|
||||
// qsv encoders don't like yuv420p
|
||||
format = format switch |
||||
{ |
||||
PixelFormatYuv420P => new PixelFormatNv12(PixelFormat.YUV420P), |
||||
_ => format |
||||
}; |
||||
|
||||
pipelineSteps.Add(new PixelFormatOutputOption(format)); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private FrameState SetWatermark( |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState, |
||||
List<IPipelineFilterStep> watermarkOverlayFilterSteps) |
||||
{ |
||||
if (context.HasWatermark) |
||||
{ |
||||
WatermarkInputFile watermark = watermarkInputFile.Head(); |
||||
|
||||
foreach (VideoStream watermarkStream in watermark.VideoStreams) |
||||
{ |
||||
if (watermarkStream.StillImage == false) |
||||
{ |
||||
watermark.AddOption(new DoNotIgnoreLoopInputOption()); |
||||
} |
||||
else if (watermark.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)) |
||||
{ |
||||
// looping is required to fade a static image in and out
|
||||
watermark.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); |
||||
} |
||||
} |
||||
|
||||
if (watermark.DesiredState.Size == WatermarkSize.Scaled) |
||||
{ |
||||
watermark.FilterSteps.Add( |
||||
new WatermarkScaleFilter(watermark.DesiredState, currentState.PaddedSize)); |
||||
} |
||||
|
||||
if (watermark.DesiredState.Opacity != 100) |
||||
{ |
||||
watermark.FilterSteps.Add(new WatermarkOpacityFilter(watermark.DesiredState)); |
||||
} |
||||
|
||||
IPixelFormat pixelFormat = context.Is10BitOutput |
||||
? new PixelFormatNv12(FFmpegFormat.P010LE) |
||||
: new PixelFormatNv12(FFmpegFormat.YUVA420P); |
||||
|
||||
watermark.FilterSteps.Add(new PixelFormatFilter(pixelFormat)); |
||||
|
||||
foreach (List<WatermarkFadePoint> fadePoints in watermark.DesiredState.MaybeFadePoints) |
||||
{ |
||||
watermark.FilterSteps.AddRange(fadePoints.Map(fp => new WatermarkFadeFilter(fp))); |
||||
} |
||||
|
||||
foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) |
||||
{ |
||||
IPixelFormat pf = desiredPixelFormat; |
||||
if (desiredPixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat availablePixelFormat in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
pf = availablePixelFormat; |
||||
} |
||||
} |
||||
|
||||
var watermarkFilter = new OverlayWatermarkFilter( |
||||
watermark.DesiredState, |
||||
desiredState.PaddedSize, |
||||
videoStream.SquarePixelFrameSize(currentState.PaddedSize), |
||||
pf, |
||||
_logger); |
||||
watermarkOverlayFilterSteps.Add(watermarkFilter); |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetSubtitle( |
||||
VideoInputFile videoInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineFilterStep> subtitleOverlayFilterSteps) |
||||
{ |
||||
foreach (SubtitleInputFile subtitle in subtitleInputFile) |
||||
{ |
||||
if (context.HasSubtitleText) |
||||
{ |
||||
videoInputFile.AddOption(new CopyTimestampInputOption()); |
||||
|
||||
// if (videoInputFile.FilterSteps.Count == 0 && videoInputFile.InputOptions.OfType<CuvidDecoder>().Any())
|
||||
// {
|
||||
// // change the hw accel output to software so the explicit download isn't needed
|
||||
// foreach (CuvidDecoder decoder in videoInputFile.InputOptions.OfType<CuvidDecoder>())
|
||||
// {
|
||||
// decoder.HardwareAccelerationMode = HardwareAccelerationMode.None;
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
var downloadFilter = new HardwareDownloadFilter(currentState); |
||||
currentState = downloadFilter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(downloadFilter); |
||||
// }
|
||||
|
||||
var subtitlesFilter = new SubtitlesFilter(fontsFolder, subtitle); |
||||
currentState = subtitlesFilter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(subtitlesFilter); |
||||
} |
||||
else if (context.HasSubtitleOverlay) |
||||
{ |
||||
IPixelFormat pixelFormat = new PixelFormatYuva420P(); |
||||
|
||||
var pixelFormatFilter = new PixelFormatFilter(pixelFormat); |
||||
subtitle.FilterSteps.Add(pixelFormatFilter); |
||||
|
||||
foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) |
||||
{ |
||||
IPixelFormat pf = desiredPixelFormat; |
||||
if (desiredPixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat availablePixelFormat in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
pf = availablePixelFormat; |
||||
} |
||||
} |
||||
|
||||
var subtitlesFilter = new OverlaySubtitleFilter(pf); |
||||
subtitleOverlayFilterSteps.Add(subtitlesFilter); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetPad( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
if (currentState.PaddedSize != desiredState.PaddedSize) |
||||
{ |
||||
IPipelineFilterStep padStep = new PadFilter( |
||||
currentState, |
||||
desiredState.PaddedSize); |
||||
currentState = padStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(padStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetScale( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
IPipelineFilterStep scaleStep; |
||||
|
||||
if (currentState.ScaledSize != desiredState.ScaledSize && ffmpegState is |
||||
{ |
||||
DecoderHardwareAccelerationMode: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None |
||||
} && context is { HasWatermark: false, HasSubtitleOverlay: false, ShouldDeinterlace: false }) |
||||
{ |
||||
scaleStep = new ScaleFilter( |
||||
currentState, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize, |
||||
videoStream.IsAnamorphicEdgeCase); |
||||
} |
||||
else |
||||
{ |
||||
scaleStep = new ScaleQsvFilter( |
||||
currentState with |
||||
{ |
||||
PixelFormat = //context.HasWatermark ||
|
||||
//context.HasSubtitleOverlay ||
|
||||
// (desiredState.ScaledSize != desiredState.PaddedSize) ||
|
||||
// context.HasSubtitleText ||
|
||||
ffmpegState is |
||||
{ |
||||
DecoderHardwareAccelerationMode: HardwareAccelerationMode.Nvenc, |
||||
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None |
||||
} |
||||
? desiredState.PixelFormat.Map(pf => (IPixelFormat)new PixelFormatNv12(pf.Name)) |
||||
: Option<IPixelFormat>.None |
||||
}, |
||||
desiredState.ScaledSize, |
||||
ffmpegState.QsvExtraHardwareFrames, |
||||
videoStream.IsAnamorphicEdgeCase, |
||||
videoStream.SampleAspectRatio); |
||||
} |
||||
|
||||
if (!string.IsNullOrWhiteSpace(scaleStep.Filter)) |
||||
{ |
||||
currentState = scaleStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(scaleStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetDeinterlace( |
||||
VideoInputFile videoInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState) |
||||
{ |
||||
if (context.ShouldDeinterlace) |
||||
{ |
||||
var filter = new DeinterlaceQsvFilter(currentState, ffmpegState.QsvExtraHardwareFrames); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
} |
||||
@ -0,0 +1,318 @@
@@ -0,0 +1,318 @@
|
||||
using ErsatzTV.FFmpeg.Decoder; |
||||
using ErsatzTV.FFmpeg.Encoder; |
||||
using ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.Option; |
||||
using ErsatzTV.FFmpeg.State; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public class SoftwarePipelineBuilder : PipelineBuilderBase |
||||
{ |
||||
private readonly ILogger _logger; |
||||
|
||||
public SoftwarePipelineBuilder( |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
ILogger logger) : base( |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
logger) |
||||
{ |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected override FFmpegState SetAccelState( |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) => ffmpegState with |
||||
{ |
||||
DecoderHardwareAccelerationMode = HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode = HardwareAccelerationMode.None |
||||
}; |
||||
|
||||
protected override void SetDecoder( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
foreach (IDecoder decoder in GetSoftwareDecoder(videoStream)) |
||||
{ |
||||
videoInputFile.AddOption(decoder); |
||||
} |
||||
} |
||||
|
||||
protected virtual Option<IEncoder> GetEncoder( |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
FrameState desiredState) |
||||
{ |
||||
return GetSoftwareEncoder(currentState, desiredState); |
||||
} |
||||
|
||||
protected override FilterChain SetVideoFilters( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var watermarkOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
var subtitleOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
|
||||
FrameState currentState = desiredState with |
||||
{ |
||||
PixelFormat = videoStream.PixelFormat, |
||||
FrameDataLocation = FrameDataLocation.Software, |
||||
IsAnamorphic = videoStream.IsAnamorphic, |
||||
ScaledSize = videoStream.FrameSize, |
||||
PaddedSize = videoStream.FrameSize |
||||
}; |
||||
|
||||
SetDeinterlace(videoInputFile, context, currentState); |
||||
|
||||
currentState = SetScale(videoInputFile, videoStream, desiredState, currentState); |
||||
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); |
||||
SetSubtitle(videoInputFile, subtitleInputFile, context, desiredState, fontsFolder, subtitleOverlayFilterSteps); |
||||
SetWatermark( |
||||
videoStream, |
||||
watermarkInputFile, |
||||
context, |
||||
ffmpegState, |
||||
desiredState, |
||||
currentState, |
||||
watermarkOverlayFilterSteps); |
||||
|
||||
// after everything else is done, apply the encoder
|
||||
if (pipelineSteps.OfType<IEncoder>().All(e => e.Kind != StreamKind.Video)) |
||||
{ |
||||
foreach (IEncoder encoder in GetEncoder(ffmpegState, currentState, desiredState)) |
||||
{ |
||||
pipelineSteps.Add(encoder); |
||||
videoInputFile.FilterSteps.Add(encoder); |
||||
} |
||||
} |
||||
|
||||
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat( |
||||
videoStream, |
||||
desiredState.PixelFormat, |
||||
currentState, |
||||
pipelineSteps); |
||||
|
||||
return new FilterChain( |
||||
videoInputFile.FilterSteps, |
||||
watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
subtitleInputFile.Map(st => st.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
watermarkOverlayFilterSteps, |
||||
subtitleOverlayFilterSteps, |
||||
pixelFormatFilterSteps); |
||||
} |
||||
|
||||
protected virtual List<IPipelineFilterStep> SetPixelFormat( |
||||
VideoStream videoStream, |
||||
Option<IPixelFormat> desiredPixelFormat, |
||||
FrameState currentState, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var result = new List<IPipelineFilterStep>(); |
||||
|
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
if (!videoStream.ColorParams.IsBt709) |
||||
{ |
||||
_logger.LogDebug("Adding colorspace filter"); |
||||
var colorspace = new ColorspaceFilter(videoStream, pixelFormat); |
||||
currentState = colorspace.NextState(currentState); |
||||
result.Add(colorspace); |
||||
} |
||||
|
||||
if (currentState.PixelFormat.Map(f => f.FFmpegName) != pixelFormat.FFmpegName) |
||||
{ |
||||
_logger.LogDebug( |
||||
"Format {A} doesn't equal {B}", |
||||
currentState.PixelFormat.Map(f => f.FFmpegName), |
||||
pixelFormat.FFmpegName); |
||||
|
||||
pipelineSteps.Add(new PixelFormatOutputOption(pixelFormat)); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private void SetWatermark( |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState, |
||||
List<IPipelineFilterStep> watermarkOverlayFilterSteps) |
||||
{ |
||||
if (context.HasWatermark) |
||||
{ |
||||
WatermarkInputFile watermark = watermarkInputFile.Head(); |
||||
|
||||
watermark.FilterSteps.Add( |
||||
new WatermarkPixelFormatFilter(ffmpegState, watermark.DesiredState, context.Is10BitOutput)); |
||||
|
||||
foreach (VideoStream watermarkStream in watermark.VideoStreams) |
||||
{ |
||||
if (watermarkStream.StillImage == false) |
||||
{ |
||||
watermark.AddOption(new DoNotIgnoreLoopInputOption()); |
||||
} |
||||
else if (watermark.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)) |
||||
{ |
||||
// looping is required to fade a static image in and out
|
||||
watermark.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); |
||||
} |
||||
} |
||||
|
||||
if (watermark.DesiredState.Size == WatermarkSize.Scaled) |
||||
{ |
||||
watermark.FilterSteps.Add( |
||||
new WatermarkScaleFilter(watermark.DesiredState, currentState.PaddedSize)); |
||||
} |
||||
|
||||
if (watermark.DesiredState.Opacity != 100) |
||||
{ |
||||
watermark.FilterSteps.Add(new WatermarkOpacityFilter(watermark.DesiredState)); |
||||
} |
||||
|
||||
foreach (List<WatermarkFadePoint> fadePoints in watermark.DesiredState.MaybeFadePoints) |
||||
{ |
||||
watermark.FilterSteps.AddRange(fadePoints.Map(fp => new WatermarkFadeFilter(fp))); |
||||
} |
||||
|
||||
foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) |
||||
{ |
||||
IPixelFormat pf = desiredPixelFormat; |
||||
if (desiredPixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat availablePixelFormat in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
pf = availablePixelFormat; |
||||
} |
||||
} |
||||
|
||||
var watermarkFilter = new OverlayWatermarkFilter( |
||||
watermark.DesiredState, |
||||
desiredState.PaddedSize, |
||||
videoStream.SquarePixelFrameSize(currentState.PaddedSize), |
||||
pf, |
||||
_logger); |
||||
watermarkOverlayFilterSteps.Add(watermarkFilter); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static void SetSubtitle( |
||||
VideoInputFile videoInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineFilterStep> subtitleOverlayFilterSteps) |
||||
{ |
||||
foreach (SubtitleInputFile subtitle in subtitleInputFile) |
||||
{ |
||||
if (context.HasSubtitleText) |
||||
{ |
||||
videoInputFile.AddOption(new CopyTimestampInputOption()); |
||||
|
||||
var subtitlesFilter = new SubtitlesFilter(fontsFolder, subtitle); |
||||
videoInputFile.FilterSteps.Add(subtitlesFilter); |
||||
} |
||||
else if (context.HasSubtitleOverlay) |
||||
{ |
||||
// only scale if scaling or padding was used for main video stream
|
||||
if (videoInputFile.FilterSteps.Any(s => s is ScaleFilter or PadFilter)) |
||||
{ |
||||
var scaleFilter = new ScaleImageFilter(desiredState.PaddedSize); |
||||
subtitle.FilterSteps.Add(scaleFilter); |
||||
} |
||||
|
||||
foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) |
||||
{ |
||||
IPixelFormat pf = desiredPixelFormat; |
||||
if (desiredPixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat availablePixelFormat in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
pf = availablePixelFormat; |
||||
} |
||||
} |
||||
|
||||
var subtitlesFilter = new OverlaySubtitleFilter(pf); |
||||
subtitleOverlayFilterSteps.Add(subtitlesFilter); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static FrameState SetPad( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
if (currentState.PaddedSize != desiredState.PaddedSize) |
||||
{ |
||||
IPipelineFilterStep padStep = new PadFilter(currentState, desiredState.PaddedSize); |
||||
currentState = padStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(padStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetScale( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
if (videoStream.FrameSize != desiredState.ScaledSize) |
||||
{ |
||||
IPipelineFilterStep scaleStep = new ScaleFilter( |
||||
currentState, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize, |
||||
videoStream.IsAnamorphicEdgeCase); |
||||
|
||||
currentState = scaleStep.NextState(currentState); |
||||
|
||||
videoInputFile.FilterSteps.Add(scaleStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static void SetDeinterlace(VideoInputFile videoInputFile, PipelineContext context, FrameState currentState) |
||||
{ |
||||
if (context.ShouldDeinterlace) |
||||
{ |
||||
videoInputFile.FilterSteps.Add(new YadifFilter(currentState)); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,546 @@
@@ -0,0 +1,546 @@
|
||||
using ErsatzTV.FFmpeg.Capabilities; |
||||
using ErsatzTV.FFmpeg.Decoder; |
||||
using ErsatzTV.FFmpeg.Encoder; |
||||
using ErsatzTV.FFmpeg.Encoder.Vaapi; |
||||
using ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.Option; |
||||
using ErsatzTV.FFmpeg.Option.HardwareAcceleration; |
||||
using ErsatzTV.FFmpeg.State; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public class VaapiPipelineBuilder : SoftwarePipelineBuilder |
||||
{ |
||||
private readonly IHardwareCapabilities _hardwareCapabilities; |
||||
private readonly ILogger _logger; |
||||
|
||||
public VaapiPipelineBuilder( |
||||
IHardwareCapabilities hardwareCapabilities, |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
ILogger logger) : base( |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
logger) |
||||
{ |
||||
_hardwareCapabilities = hardwareCapabilities; |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected override FFmpegState SetAccelState( |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
bool canDecode = _hardwareCapabilities.CanDecode(videoStream.Codec, videoStream.PixelFormat); |
||||
bool canEncode = _hardwareCapabilities.CanEncode(desiredState.VideoFormat, desiredState.PixelFormat); |
||||
|
||||
foreach (string vaapiDevice in ffmpegState.VaapiDevice) |
||||
{ |
||||
pipelineSteps.Add(new VaapiHardwareAccelerationOption(vaapiDevice)); |
||||
} |
||||
|
||||
// use software decoding with an extensive pipeline
|
||||
if (context.HasSubtitleOverlay && context.HasWatermark) |
||||
{ |
||||
canDecode = false; |
||||
} |
||||
|
||||
// disable hw accel if decoder/encoder isn't supported
|
||||
return ffmpegState with |
||||
{ |
||||
DecoderHardwareAccelerationMode = canDecode |
||||
? HardwareAccelerationMode.Vaapi |
||||
: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode = canEncode |
||||
? HardwareAccelerationMode.Vaapi |
||||
: HardwareAccelerationMode.None |
||||
}; |
||||
} |
||||
|
||||
protected override void SetDecoder( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch |
||||
{ |
||||
(HardwareAccelerationMode.Vaapi, _) => new DecoderVaapi(), |
||||
_ => GetSoftwareDecoder(videoStream) |
||||
}; |
||||
|
||||
foreach (IDecoder decoder in maybeDecoder) |
||||
{ |
||||
videoInputFile.AddOption(decoder); |
||||
} |
||||
} |
||||
|
||||
protected override FilterChain SetVideoFilters( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var watermarkOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
var subtitleOverlayFilterSteps = new List<IPipelineFilterStep>(); |
||||
|
||||
FrameState currentState = desiredState with |
||||
{ |
||||
ScaledSize = videoStream.FrameSize, |
||||
PaddedSize = videoStream.FrameSize, |
||||
|
||||
PixelFormat = videoStream.PixelFormat, |
||||
|
||||
IsAnamorphic = videoStream.IsAnamorphic, |
||||
|
||||
FrameDataLocation = ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi |
||||
? FrameDataLocation.Hardware |
||||
: FrameDataLocation.Software |
||||
}; |
||||
|
||||
// easier to use nv12 for overlay
|
||||
if (context.HasSubtitleOverlay || context.HasWatermark) |
||||
{ |
||||
IPixelFormat pixelFormat = desiredState.PixelFormat.IfNone( |
||||
context.Is10BitOutput ? new PixelFormatYuv420P10Le() : new PixelFormatYuv420P()); |
||||
desiredState = desiredState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }; |
||||
} |
||||
|
||||
// _logger.LogDebug("After decode: {PixelFormat}", currentState.PixelFormat);
|
||||
|
||||
currentState = SetDeinterlace(videoInputFile, context, ffmpegState, currentState); |
||||
// _logger.LogDebug("After deinterlace: {PixelFormat}", currentState.PixelFormat);
|
||||
|
||||
currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState); |
||||
// _logger.LogDebug("After scale: {PixelFormat}", currentState.PixelFormat);
|
||||
|
||||
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); |
||||
// _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat);
|
||||
|
||||
// need to upload for hardware overlay
|
||||
bool forceSoftwareOverlay = context.HasSubtitleOverlay && context.HasWatermark; |
||||
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Software && context.HasSubtitleOverlay && |
||||
!forceSoftwareOverlay) |
||||
{ |
||||
var hardwareUpload = new HardwareUploadVaapiFilter(true); |
||||
currentState = hardwareUpload.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(hardwareUpload); |
||||
} |
||||
else if(currentState.FrameDataLocation == FrameDataLocation.Hardware && |
||||
(!context.HasSubtitleOverlay || forceSoftwareOverlay) && |
||||
context.HasWatermark) |
||||
{ |
||||
// download for watermark (or forced software subtitle)
|
||||
var hardwareDownload = new HardwareDownloadFilter(currentState); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(hardwareDownload); |
||||
} |
||||
|
||||
currentState = SetSubtitle( |
||||
videoInputFile, |
||||
subtitleInputFile, |
||||
context, |
||||
forceSoftwareOverlay, |
||||
ffmpegState, |
||||
currentState, |
||||
desiredState, |
||||
fontsFolder, |
||||
subtitleOverlayFilterSteps); |
||||
|
||||
currentState = SetWatermark( |
||||
videoStream, |
||||
watermarkInputFile, |
||||
context, |
||||
ffmpegState, |
||||
desiredState, |
||||
currentState, |
||||
watermarkOverlayFilterSteps); |
||||
|
||||
// after everything else is done, apply the encoder
|
||||
if (pipelineSteps.OfType<IEncoder>().All(e => e.Kind != StreamKind.Video)) |
||||
{ |
||||
Option<IEncoder> maybeEncoder = |
||||
(ffmpegState.EncoderHardwareAccelerationMode, desiredState.VideoFormat) switch |
||||
{ |
||||
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(), |
||||
(HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(), |
||||
|
||||
(_, _) => GetSoftwareEncoder(currentState, desiredState) |
||||
}; |
||||
|
||||
foreach (IEncoder encoder in maybeEncoder) |
||||
{ |
||||
pipelineSteps.Add(encoder); |
||||
videoInputFile.FilterSteps.Add(encoder); |
||||
} |
||||
} |
||||
|
||||
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat( |
||||
videoStream, |
||||
desiredState.PixelFormat, |
||||
ffmpegState, |
||||
currentState, |
||||
context, |
||||
pipelineSteps); |
||||
|
||||
return new FilterChain( |
||||
videoInputFile.FilterSteps, |
||||
watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
subtitleInputFile.Map(st => st.FilterSteps).IfNone(new List<IPipelineFilterStep>()), |
||||
watermarkOverlayFilterSteps, |
||||
subtitleOverlayFilterSteps, |
||||
pixelFormatFilterSteps); |
||||
} |
||||
|
||||
private List<IPipelineFilterStep> SetPixelFormat( |
||||
VideoStream videoStream, |
||||
Option<IPixelFormat> desiredPixelFormat, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var result = new List<IPipelineFilterStep>(); |
||||
|
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
IPixelFormat format = pixelFormat; |
||||
|
||||
if (pixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat pf in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
format = pf; |
||||
} |
||||
} |
||||
|
||||
if (!videoStream.ColorParams.IsBt709) |
||||
{ |
||||
_logger.LogDebug("Adding colorspace filter"); |
||||
var colorspace = new ColorspaceFilter(videoStream, format, currentState.FrameDataLocation); |
||||
currentState = colorspace.NextState(currentState); |
||||
result.Add(colorspace); |
||||
} |
||||
|
||||
if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None) |
||||
{ |
||||
_logger.LogDebug("Using software encoder"); |
||||
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
_logger.LogDebug("FrameDataLocation == FrameDataLocation.Hardware"); |
||||
|
||||
var hardwareDownload = |
||||
new HardwareDownloadFilter(currentState with { PixelFormat = Some(format) }); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
result.Add(hardwareDownload); |
||||
} |
||||
} |
||||
|
||||
if (currentState.PixelFormat.Map(f => f.FFmpegName) != format.FFmpegName) |
||||
{ |
||||
_logger.LogDebug( |
||||
"Format {A} doesn't equal {B}", |
||||
currentState.PixelFormat.Map(f => f.FFmpegName), |
||||
format.FFmpegName); |
||||
|
||||
// NV12 is 8-bit
|
||||
if (format is PixelFormatYuv420P) |
||||
{ |
||||
format = new PixelFormatNv12(format.Name); |
||||
} |
||||
|
||||
if (currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
result.Add(new VaapiFormatFilter(format)); |
||||
} |
||||
else |
||||
{ |
||||
result.Add(new PixelFormatFilter(format)); |
||||
} |
||||
} |
||||
|
||||
if (ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi && |
||||
currentState.FrameDataLocation == FrameDataLocation.Software) |
||||
{ |
||||
bool setFormat = result.All(f => f is not VaapiFormatFilter && f is not PixelFormatFilter); |
||||
result.Add(new HardwareUploadVaapiFilter(setFormat)); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private FrameState SetWatermark( |
||||
VideoStream videoStream, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState, |
||||
List<IPipelineFilterStep> watermarkOverlayFilterSteps) |
||||
{ |
||||
if (context.HasWatermark) |
||||
{ |
||||
WatermarkInputFile watermark = watermarkInputFile.Head(); |
||||
|
||||
foreach (VideoStream watermarkStream in watermark.VideoStreams) |
||||
{ |
||||
if (watermarkStream.StillImage == false) |
||||
{ |
||||
watermark.AddOption(new DoNotIgnoreLoopInputOption()); |
||||
} |
||||
else if (watermark.DesiredState.MaybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)) |
||||
{ |
||||
// looping is required to fade a static image in and out
|
||||
watermark.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); |
||||
} |
||||
} |
||||
|
||||
if (watermark.DesiredState.Size == WatermarkSize.Scaled) |
||||
{ |
||||
watermark.FilterSteps.Add( |
||||
new WatermarkScaleFilter(watermark.DesiredState, currentState.PaddedSize)); |
||||
} |
||||
|
||||
if (watermark.DesiredState.Opacity != 100) |
||||
{ |
||||
watermark.FilterSteps.Add(new WatermarkOpacityFilter(watermark.DesiredState)); |
||||
} |
||||
|
||||
watermark.FilterSteps.Add(new PixelFormatFilter(new PixelFormatYuva420P())); |
||||
|
||||
foreach (List<WatermarkFadePoint> fadePoints in watermark.DesiredState.MaybeFadePoints) |
||||
{ |
||||
watermark.FilterSteps.AddRange(fadePoints.Map(fp => new WatermarkFadeFilter(fp))); |
||||
} |
||||
|
||||
foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat) |
||||
{ |
||||
IPixelFormat pf = desiredPixelFormat; |
||||
if (desiredPixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat availablePixelFormat in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
pf = availablePixelFormat; |
||||
} |
||||
} |
||||
|
||||
var watermarkFilter = new OverlayWatermarkFilter( |
||||
watermark.DesiredState, |
||||
desiredState.PaddedSize, |
||||
videoStream.SquarePixelFrameSize(currentState.PaddedSize), |
||||
pf, |
||||
_logger); |
||||
watermarkOverlayFilterSteps.Add(watermarkFilter); |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetSubtitle( |
||||
VideoInputFile videoInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
PipelineContext context, |
||||
bool forceSoftwareOverlay, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState, |
||||
FrameState desiredState, |
||||
string fontsFolder, |
||||
ICollection<IPipelineFilterStep> subtitleOverlayFilterSteps) |
||||
{ |
||||
foreach (SubtitleInputFile subtitle in subtitleInputFile) |
||||
{ |
||||
if (context.HasSubtitleText) |
||||
{ |
||||
videoInputFile.AddOption(new CopyTimestampInputOption()); |
||||
|
||||
// if (videoInputFile.FilterSteps.Count == 0 && videoInputFile.InputOptions.OfType<CuvidDecoder>().Any())
|
||||
// {
|
||||
// // change the hw accel output to software so the explicit download isn't needed
|
||||
// foreach (CuvidDecoder decoder in videoInputFile.InputOptions.OfType<CuvidDecoder>())
|
||||
// {
|
||||
// decoder.HardwareAccelerationMode = HardwareAccelerationMode.None;
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
var downloadFilter = new HardwareDownloadFilter(currentState); |
||||
currentState = downloadFilter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(downloadFilter); |
||||
// }
|
||||
|
||||
var subtitlesFilter = new SubtitlesFilter(fontsFolder, subtitle); |
||||
currentState = subtitlesFilter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(subtitlesFilter); |
||||
} |
||||
else if (context.HasSubtitleOverlay) |
||||
{ |
||||
var pixelFormatFilter = new PixelFormatFilter(new PixelFormatArgb()); |
||||
subtitle.FilterSteps.Add(pixelFormatFilter); |
||||
|
||||
if (forceSoftwareOverlay) |
||||
{ |
||||
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat) |
||||
{ |
||||
IPixelFormat pf = pixelFormat; |
||||
if (pixelFormat is PixelFormatNv12 nv12) |
||||
{ |
||||
foreach (IPixelFormat format in AvailablePixelFormats.ForPixelFormat(nv12.Name, null)) |
||||
{ |
||||
pf = format; |
||||
} |
||||
} |
||||
|
||||
var subtitlesFilter = new OverlaySubtitleFilter(pf); |
||||
subtitleOverlayFilterSteps.Add(subtitlesFilter); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
var subtitleHardwareUpload = new HardwareUploadVaapiFilter(false); |
||||
subtitle.FilterSteps.Add(subtitleHardwareUpload); |
||||
|
||||
// always scale - shouldn't really be needed outside of transcoding tests, which use picture subtitles
|
||||
// that are too big
|
||||
var scaleFilter = new SubtitleScaleVaapiFilter(desiredState.PaddedSize); |
||||
subtitle.FilterSteps.Add(scaleFilter); |
||||
|
||||
var subtitlesFilter = new OverlaySubtitleVaapiFilter(); |
||||
subtitleOverlayFilterSteps.Add(subtitlesFilter); |
||||
} |
||||
|
||||
if (context.HasWatermark && !forceSoftwareOverlay) |
||||
{ |
||||
// download for watermark
|
||||
var hardwareDownload = new HardwareDownloadFilter(currentState); |
||||
currentState = hardwareDownload.NextState(currentState); |
||||
subtitleOverlayFilterSteps.Add(hardwareDownload); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetPad( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
if (currentState.PaddedSize != desiredState.PaddedSize) |
||||
{ |
||||
IPipelineFilterStep padStep = new PadFilter( |
||||
currentState, |
||||
desiredState.PaddedSize); |
||||
currentState = padStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(padStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetScale( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
FrameState currentState) |
||||
{ |
||||
IPipelineFilterStep scaleStep; |
||||
|
||||
if ((currentState.ScaledSize != desiredState.ScaledSize && ffmpegState is |
||||
{ |
||||
DecoderHardwareAccelerationMode: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None |
||||
} && context is { HasWatermark: false, HasSubtitleOverlay: false, ShouldDeinterlace: false }) || |
||||
ffmpegState.DecoderHardwareAccelerationMode != HardwareAccelerationMode.Vaapi) |
||||
{ |
||||
scaleStep = new ScaleFilter( |
||||
currentState, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize, |
||||
videoStream.IsAnamorphicEdgeCase); |
||||
} |
||||
else |
||||
{ |
||||
scaleStep = new ScaleVaapiFilter( |
||||
currentState with |
||||
{ |
||||
PixelFormat = //context.HasWatermark ||
|
||||
//context.HasSubtitleOverlay ||
|
||||
// (desiredState.ScaledSize != desiredState.PaddedSize) ||
|
||||
// context.HasSubtitleText ||
|
||||
ffmpegState is |
||||
{ |
||||
DecoderHardwareAccelerationMode: HardwareAccelerationMode.Nvenc, |
||||
EncoderHardwareAccelerationMode: HardwareAccelerationMode.None |
||||
} |
||||
? desiredState.PixelFormat.Map(pf => (IPixelFormat)new PixelFormatNv12(pf.Name)) |
||||
: Option<IPixelFormat>.None |
||||
}, |
||||
desiredState.ScaledSize, |
||||
desiredState.PaddedSize, |
||||
videoStream.IsAnamorphicEdgeCase); |
||||
} |
||||
|
||||
if (!string.IsNullOrWhiteSpace(scaleStep.Filter)) |
||||
{ |
||||
currentState = scaleStep.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(scaleStep); |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
|
||||
private static FrameState SetDeinterlace( |
||||
VideoInputFile videoInputFile, |
||||
PipelineContext context, |
||||
FFmpegState ffmpegState, |
||||
FrameState currentState) |
||||
{ |
||||
if (context.ShouldDeinterlace) |
||||
{ |
||||
if (ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi) |
||||
{ |
||||
var filter = new DeinterlaceVaapiFilter(currentState); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
else |
||||
{ |
||||
var filter = new YadifFilter(currentState); |
||||
currentState = filter.NextState(currentState); |
||||
videoInputFile.FilterSteps.Add(filter); |
||||
} |
||||
} |
||||
|
||||
return currentState; |
||||
} |
||||
} |
||||
@ -0,0 +1,130 @@
@@ -0,0 +1,130 @@
|
||||
using ErsatzTV.FFmpeg.Capabilities; |
||||
using ErsatzTV.FFmpeg.Decoder; |
||||
using ErsatzTV.FFmpeg.Encoder; |
||||
using ErsatzTV.FFmpeg.Encoder.VideoToolbox; |
||||
using ErsatzTV.FFmpeg.Filter; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.Option; |
||||
using ErsatzTV.FFmpeg.Option.HardwareAcceleration; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Pipeline; |
||||
|
||||
public class VideoToolboxPipelineBuilder : SoftwarePipelineBuilder |
||||
{ |
||||
private readonly IHardwareCapabilities _hardwareCapabilities; |
||||
private readonly ILogger _logger; |
||||
|
||||
public VideoToolboxPipelineBuilder( |
||||
IHardwareCapabilities hardwareCapabilities, |
||||
HardwareAccelerationMode hardwareAccelerationMode, |
||||
Option<VideoInputFile> videoInputFile, |
||||
Option<AudioInputFile> audioInputFile, |
||||
Option<WatermarkInputFile> watermarkInputFile, |
||||
Option<SubtitleInputFile> subtitleInputFile, |
||||
string reportsFolder, |
||||
string fontsFolder, |
||||
ILogger logger) : base( |
||||
hardwareAccelerationMode, |
||||
videoInputFile, |
||||
audioInputFile, |
||||
watermarkInputFile, |
||||
subtitleInputFile, |
||||
reportsFolder, |
||||
fontsFolder, |
||||
logger) |
||||
{ |
||||
_hardwareCapabilities = hardwareCapabilities; |
||||
_logger = logger; |
||||
} |
||||
|
||||
protected override FFmpegState SetAccelState( |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
FrameState desiredState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
bool canDecode = _hardwareCapabilities.CanDecode(videoStream.Codec, videoStream.PixelFormat); |
||||
bool canEncode = _hardwareCapabilities.CanEncode(desiredState.VideoFormat, desiredState.PixelFormat); |
||||
|
||||
pipelineSteps.Add(new VideoToolboxHardwareAccelerationOption()); |
||||
|
||||
// disable hw accel if decoder/encoder isn't supported
|
||||
return ffmpegState with |
||||
{ |
||||
DecoderHardwareAccelerationMode = canDecode |
||||
? HardwareAccelerationMode.VideoToolbox |
||||
: HardwareAccelerationMode.None, |
||||
EncoderHardwareAccelerationMode = canEncode |
||||
? HardwareAccelerationMode.VideoToolbox |
||||
: HardwareAccelerationMode.None |
||||
}; |
||||
} |
||||
|
||||
protected override void SetDecoder( |
||||
VideoInputFile videoInputFile, |
||||
VideoStream videoStream, |
||||
FFmpegState ffmpegState, |
||||
PipelineContext context, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
Option<IDecoder> maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch |
||||
{ |
||||
(HardwareAccelerationMode.VideoToolbox, _) => new DecoderVideoToolbox(), |
||||
|
||||
_ => GetSoftwareDecoder(videoStream) |
||||
}; |
||||
|
||||
foreach (IDecoder decoder in maybeDecoder) |
||||
{ |
||||
videoInputFile.AddOption(decoder); |
||||
} |
||||
} |
||||
|
||||
protected override Option<IEncoder> GetEncoder(FFmpegState ffmpegState, FrameState currentState, FrameState desiredState) |
||||
{ |
||||
return (ffmpegState.EncoderHardwareAccelerationMode, desiredState.VideoFormat) switch |
||||
{ |
||||
(HardwareAccelerationMode.VideoToolbox, VideoFormat.Hevc) => |
||||
new EncoderHevcVideoToolbox(desiredState.BitDepth), |
||||
(HardwareAccelerationMode.VideoToolbox, VideoFormat.H264) => |
||||
new EncoderH264VideoToolbox(), |
||||
|
||||
_ => GetSoftwareEncoder(currentState, desiredState) |
||||
}; |
||||
} |
||||
|
||||
protected override List<IPipelineFilterStep> SetPixelFormat( |
||||
VideoStream videoStream, |
||||
Option<IPixelFormat> desiredPixelFormat, |
||||
FrameState currentState, |
||||
ICollection<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var result = new List<IPipelineFilterStep>(); |
||||
|
||||
foreach (IPixelFormat pixelFormat in desiredPixelFormat) |
||||
{ |
||||
if (!videoStream.ColorParams.IsBt709) |
||||
{ |
||||
_logger.LogDebug("Adding colorspace filter"); |
||||
var colorspace = new ColorspaceFilter(videoStream, pixelFormat); |
||||
currentState = colorspace.NextState(currentState); |
||||
result.Add(colorspace); |
||||
} |
||||
|
||||
if (currentState.PixelFormat.Map(f => f.FFmpegName) != pixelFormat.FFmpegName) |
||||
{ |
||||
_logger.LogDebug( |
||||
"Format {A} doesn't equal {B}", |
||||
currentState.PixelFormat.Map(f => f.FFmpegName), |
||||
pixelFormat.FFmpegName); |
||||
|
||||
//result.Add(new PixelFormatFilter(pixelFormat));
|
||||
pipelineSteps.Add(new PixelFormatOutputOption(pixelFormat)); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg; |
||||
|
||||
public enum ScanKind |
||||
{ |
||||
Unknown = 0, |
||||
Progressive = 1, |
||||
Interlaced = 2 |
||||
} |
||||
Loading…
Reference in new issue