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 @@ |
|||||||
|
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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||||
|
|
||||||
public class DecoderH264Cuvid : DecoderBase |
public class DecoderH264Cuvid : CuvidDecoder |
||||||
{ |
{ |
||||||
private readonly FFmpegState _ffmpegState; |
public DecoderH264Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||||
|
|
||||||
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) |
|
||||||
{ |
{ |
||||||
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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||||
|
|
||||||
public class DecoderHevcCuvid : DecoderBase |
public class DecoderHevcCuvid : CuvidDecoder |
||||||
{ |
{ |
||||||
private readonly FFmpegState _ffmpegState; |
public DecoderHevcCuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||||
|
|
||||||
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) |
|
||||||
{ |
{ |
||||||
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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||||
|
|
||||||
public class DecoderMpeg2Cuvid : DecoderBase |
public class DecoderMpeg2Cuvid : CuvidDecoder |
||||||
{ |
{ |
||||||
private readonly bool _contentIsInterlaced; |
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; |
_contentIsInterlaced = contentIsInterlaced; |
||||||
} |
} |
||||||
|
|
||||||
public override string Name => "mpeg2_cuvid"; |
public override string Name => "mpeg2_cuvid"; |
||||||
|
|
||||||
protected override FrameDataLocation OutputFrameDataLocation => |
protected override FrameDataLocation OutputFrameDataLocation => |
||||||
_contentIsInterlaced || _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.None |
_contentIsInterlaced || HardwareAccelerationMode == HardwareAccelerationMode.None |
||||||
? FrameDataLocation.Software |
? FrameDataLocation.Software |
||||||
: FrameDataLocation.Hardware; |
: 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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||||
|
|
||||||
public class DecoderMpeg4Cuvid : DecoderBase |
public class DecoderMpeg4Cuvid : CuvidDecoder |
||||||
{ |
{ |
||||||
private readonly FFmpegState _ffmpegState; |
public DecoderMpeg4Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||||
|
{ |
||||||
public DecoderMpeg4Cuvid(FFmpegState ffmpegState) => _ffmpegState = ffmpegState; |
} |
||||||
|
|
||||||
public override string Name => "mpeg4_cuvid"; |
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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||||
|
|
||||||
public class DecoderVc1Cuvid : DecoderBase |
public class DecoderVc1Cuvid : CuvidDecoder |
||||||
{ |
{ |
||||||
private readonly FFmpegState _ffmpegState; |
public DecoderVc1Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||||
|
|
||||||
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) |
|
||||||
{ |
{ |
||||||
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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||||
|
|
||||||
public class DecoderVp9Cuvid : DecoderBase |
public class DecoderVp9Cuvid : CuvidDecoder |
||||||
{ |
{ |
||||||
private readonly FFmpegState _ffmpegState; |
public DecoderVp9Cuvid(HardwareAccelerationMode hardwareAccelerationMode) : base(hardwareAccelerationMode) |
||||||
|
|
||||||
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) |
|
||||||
{ |
{ |
||||||
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
namespace ErsatzTV.FFmpeg.Filter; |
using ErsatzTV.FFmpeg.Format; |
||||||
|
|
||||||
|
namespace ErsatzTV.FFmpeg.Filter; |
||||||
|
|
||||||
public class OverlaySubtitleFilter : BaseFilter |
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; |
public override FrameState NextState(FrameState currentState) => currentState; |
||||||
} |
} |
||||||
|
|||||||
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
namespace ErsatzTV.FFmpeg.Pipeline; |
||||||
|
|
||||||
|
public record PipelineContext( |
||||||
|
HardwareAccelerationMode HardwareAccelerationMode, |
||||||
|
bool HasWatermark, |
||||||
|
bool HasSubtitleOverlay, |
||||||
|
bool HasSubtitleText, |
||||||
|
bool ShouldDeinterlace, |
||||||
|
bool Is10BitOutput); |
||||||
|
|
||||||
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
namespace ErsatzTV.FFmpeg; |
||||||
|
|
||||||
|
public enum ScanKind |
||||||
|
{ |
||||||
|
Unknown = 0, |
||||||
|
Progressive = 1, |
||||||
|
Interlaced = 2 |
||||||
|
} |
||||||
Loading…
Reference in new issue