@ -25,6 +25,7 @@ namespace ErsatzTV.Core.FFmpeg
@@ -25,6 +25,7 @@ namespace ErsatzTV.Core.FFmpeg
private Option < List < FadePoint > > _ maybeFadePoints = None ;
private Option < int > _ watermarkIndex ;
private string _ pixelFormat ;
private string _ videoDecoder ;
private string _ videoEncoder ;
private Option < string > _ subtitle ;
private bool _ boxBlur ;
@ -75,6 +76,12 @@ namespace ErsatzTV.Core.FFmpeg
@@ -75,6 +76,12 @@ namespace ErsatzTV.Core.FFmpeg
return this ;
}
public FFmpegComplexFilterBuilder WithDecoder ( string decoder )
{
_ videoDecoder = decoder ;
return this ;
}
public FFmpegComplexFilterBuilder WithInputPixelFormat ( Option < string > maybePixelFormat )
{
foreach ( string pixelFormat in maybePixelFormat )
@ -145,20 +152,32 @@ namespace ErsatzTV.Core.FFmpeg
@@ -145,20 +152,32 @@ namespace ErsatzTV.Core.FFmpeg
string audioLabel = audioStreamIndex . Match ( index = > $"{audioInput}:{index}" , ( ) = > "0:a" ) ;
HardwareAccelerationKind acceleration = _ hardwareAccelerationKind . IfNone ( HardwareAccelerationKind . None ) ;
bool isHardwareDecode = acceleration switch
{
HardwareAccelerationKind . Vaapi = > ! isSong & & _ inputCodec ! = "mpeg4" ,
// we need an initial hwupload_cuda when only padding with these pixel formats
HardwareAccelerationKind . Nvenc when _ scaleToSize . IsNone & & _ padToSize . IsSome = >
! isSong & & ! _ pixelFormat . Contains ( "p10le" ) & & ! _ pixelFormat . Contains ( "444" ) ,
HardwareAccelerationKind . Nvenc = > ! isSong ,
HardwareAccelerationKind . Nvenc = > ! isSong & &
( string . IsNullOrWhiteSpace ( _ videoDecoder ) | |
_ videoDecoder . Contains ( "cuvid" ) ) ,
HardwareAccelerationKind . Qsv = > ! isSong ,
HardwareAccelerationKind . VideoToolbox = > false ,
_ = > false
} ;
bool nvencDeinterlace = acceleration = = HardwareAccelerationKind . Nvenc & & _ videoDecoder = = "mpeg2_cuvid" & &
_d einterlace ;
// mpeg2_cuvid will handle deinterlace and is "not" a hardware decode
if ( nvencDeinterlace )
{
_d einterlace = false ;
isHardwareDecode = false ;
}
var audioFilterQueue = new List < string > ( ) ;
var videoFilterQueue = new List < string > ( ) ;
var watermarkPreprocess = new List < string > ( ) ;
@ -215,15 +234,17 @@ namespace ErsatzTV.Core.FFmpeg
@@ -215,15 +234,17 @@ namespace ErsatzTV.Core.FFmpeg
if ( _d einterlace )
{
string f ilter = acceleration switch
Option < string > maybeF ilter = acceleration switch
{
HardwareAccelerationKind . Qsv = > "deinterlace_qsv" ,
HardwareAccelerationKind . Nvenc when ! usesHardwareFilters & & _ pixelFormat . Contains ( "p10le" ) = >
"hwupload_cuda,yadif_cuda" ,
HardwareAccelerationKind . Nvenc = > "yadif_cuda" ,
HardwareAccelerationKind . Vaapi = > "deinterlace_vaapi" ,
_ = > "yadif=1"
} ;
if ( ! string . IsNullOrWhiteSpace ( filter ) )
foreach ( string filter in maybeFilter )
{
videoFilterQueue . Add ( filter ) ;
}
@ -243,67 +264,64 @@ namespace ErsatzTV.Core.FFmpeg
@@ -243,67 +264,64 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue . Add ( "format=nv12|vaapi,hwupload" ) ;
}
_ scaleToSize . IfSome (
size = >
{
string filter = acceleration switch
{
HardwareAccelerationKind . Qsv = > $"scale_qsv=w={size.Width}:h={size.Height}" ,
HardwareAccelerationKind . Nvenc when _ pixelFormat is "yuv420p10le" = >
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}" ,
HardwareAccelerationKind . Nvenc = > $"scale_cuda={size.Width}:{size.Height}" ,
HardwareAccelerationKind . Vaapi = > $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}" ,
_ when videoOnly = > $"scale={size.Width}:{size.Height}:force_original_aspect_ratio=increase,crop={size.Width}:{size.Height}" ,
_ = > $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
} ;
if ( ! string . IsNullOrWhiteSpace ( filter ) )
{
videoFilterQueue . Add ( filter ) ;
}
} ) ;
bool scaleOrPad = _ scaleToSize . IsSome | | _ padToSize . IsSome ;
bool usesSoftwareFilters = _ padToSize . IsSome | | _ watermark . IsSome ;
bool hasFadePoints = _ maybeFadePoints . Map ( fp = > fp . Count ) . IfNone ( 0 ) > 0 ;
if ( scaleOrPad & & _ boxBlur = = false )
{
videoFilterQueue . Add ( "setsar=1" ) ;
}
var softwareFilterQueue = new List < string > ( ) ;
if ( usesSoftwareFilters )
{
if ( acceleration ! = HardwareAccelerationKind . None & & ( isHardwareDecode | | usesHardwareFilters ) )
{
videoFilterQueue . Add ( "hwdownload" ) ;
string format = acceleration switch
Option < string > maybeFormat = acceleration switch
{
HardwareAccelerationKind . Vaapi = > "format=nv12|vaapi" ,
HardwareAccelerationKind . Nvenc when _ padToSize . IsNone | | nvencDeinterlace = > None ,
HardwareAccelerationKind . Nvenc when _ pixelFormat = = "yuv420p10le" = >
"format=p010le,format=nv12" ,
HardwareAccelerationKind . Qsv when isSong = > "format=nv12,format=yuv420p" ,
_ when isSong = > "format=yuv420p" ,
_ = > "format=nv12"
} ;
videoFilterQueue . Add ( format ) ;
foreach ( string format in maybeFormat )
{
softwareFilterQueue . Add ( "hwdownload" ) ;
softwareFilterQueue . Add ( format ) ;
}
if ( nvencDeinterlace )
{
softwareFilterQueue . Add ( "hwdownload" ) ;
}
}
if ( _ boxBlur )
{
videoFilterQueue . Add ( "boxblur=40" ) ;
software FilterQueue. Add ( "boxblur=40" ) ;
}
if ( videoOnly )
{
videoFilterQueue . Add ( "deband" ) ;
software FilterQueue. Add ( "deband" ) ;
}
foreach ( ChannelWatermark watermark in _ watermark )
{
if ( watermark . Opacity ! = 1 0 0 | | _ maybeFadePoints . Map ( fp = > fp . Count ) . IfNone ( 0 ) > 0 )
Option < string > maybeFormats = acceleration switch
{
// overlay_cuda only supports alpha with yuva420p
HardwareAccelerationKind . Nvenc = > "yuva420p" ,
_ when watermark . Opacity ! = 1 0 0 | | hasFadePoints = >
"yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8" ,
_ = > None
} ;
foreach ( string formats in maybeFormats )
{
const string FORMATS = "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8" ;
watermarkPreprocess . Add ( $"format={FORMATS}" ) ;
watermarkPreprocess . Add ( $"format={formats}" ) ;
}
double horizontalMargin = Math . Round ( watermark . HorizontalMarginPercent / 1 0 0.0 * _ resolution . Width ) ;
@ -338,9 +356,18 @@ namespace ErsatzTV.Core.FFmpeg
@@ -338,9 +356,18 @@ namespace ErsatzTV.Core.FFmpeg
watermarkPreprocess . AddRange ( fadePoints . Map ( fp = > fp . ToFilter ( ) ) ) ;
}
watermarkOverlay = $"overlay={position}" ;
if ( acceleration = = HardwareAccelerationKind . Nvenc )
{
watermarkPreprocess . Add ( "hwupload_cuda" ) ;
}
if ( _ maybeFadePoints . Map ( fp = > fp . Count ) . IfNone ( 0 ) > 0 )
watermarkOverlay = acceleration switch
{
HardwareAccelerationKind . Nvenc = > $"overlay_cuda={position}" ,
_ = > $"overlay={position}"
} ;
if ( hasFadePoints & & acceleration ! = HardwareAccelerationKind . Nvenc )
{
watermarkOverlay + = "," + acceleration switch
{
@ -351,15 +378,93 @@ namespace ErsatzTV.Core.FFmpeg
@@ -351,15 +378,93 @@ namespace ErsatzTV.Core.FFmpeg
}
}
}
string outputPixelFormat = null ;
if ( ! usesSoftwareFilters & & string . IsNullOrWhiteSpace ( watermarkOverlay ) )
{
switch ( acceleration , _ videoEncoder , _ pixelFormat )
{
case ( HardwareAccelerationKind . Nvenc , "h264_nvenc" , "yuv420p10le" ) :
outputPixelFormat = "yuv420p" ;
break ;
case ( HardwareAccelerationKind . Nvenc , "h264_nvenc" , "yuv444p10le" ) :
outputPixelFormat = "yuv444p" ;
break ;
}
}
string outputFormat = ( _ videoEncoder , _ pixelFormat ) switch
{
( "hevc_nvenc" , "yuv420p10le" ) = > "p010le" ,
( "h264_nvenc" , "yuv420p10le" ) = > "p010le" ,
_ = > null
} ;
_ scaleToSize . IfSome (
size = >
{
string filter = acceleration switch
{
HardwareAccelerationKind . Qsv = > $"scale_qsv=w={size.Width}:h={size.Height}" ,
HardwareAccelerationKind . Nvenc when _ watermark . IsSome & & _ scaleToSize . IsNone = >
$"format=yuv420p,hwupload_cuda,scale_cuda={size.Width}:{size.Height}" ,
HardwareAccelerationKind . Nvenc when _ watermark . IsSome & & _ padToSize . IsNone = >
$"scale_cuda={size.Width}:{size.Height}" ,
HardwareAccelerationKind . Nvenc when _ watermark . IsNone & & ! string . IsNullOrEmpty ( outputFormat ) = >
$"scale_cuda={size.Width}:{size.Height}:format={outputFormat}" ,
HardwareAccelerationKind . Nvenc when _ pixelFormat is "yuv420p10le" & & usesHardwareFilters = = false = >
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}" ,
HardwareAccelerationKind . Nvenc = > $"scale_cuda={size.Width}:{size.Height}" ,
HardwareAccelerationKind . Vaapi = > $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}" ,
_ when videoOnly = > $"scale={size.Width}:{size.Height}:force_original_aspect_ratio=increase,crop={size.Width}:{size.Height}" ,
_ = > $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
} ;
if ( ! string . IsNullOrWhiteSpace ( filter ) )
{
videoFilterQueue . Add ( filter ) ;
}
} ) ;
if ( scaleOrPad & & _ boxBlur = = false )
{
if ( acceleration = = HardwareAccelerationKind . Nvenc )
{
if ( ! isHardwareDecode & & ! string . IsNullOrWhiteSpace ( outputPixelFormat ) )
{
videoFilterQueue . Add ( $"hwdownload,format={outputPixelFormat}" ) ;
}
}
videoFilterQueue . Add ( "setsar=1" ) ;
}
videoFilterQueue . AddRange ( softwareFilterQueue ) ;
_ padToSize . IfSome ( size = > videoFilterQueue . Add ( $"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2" ) ) ;
if ( acceleration = = HardwareAccelerationKind . Nvenc & & _ watermark . IsSome )
{
if ( _ scaleToSize . IsSome )
{
videoFilterQueue . Add ( "hwdownload,format=nv12,format=yuv420p" ) ;
videoFilterQueue . Add ( "hwupload_cuda" ) ;
}
else if ( _ padToSize . IsNone )
{
videoFilterQueue . Add ( "scale_cuda=format=yuv420p" ) ;
}
else
{
videoFilterQueue . Add ( "format=yuv420p" ) ;
videoFilterQueue . Add ( "hwupload_cuda" ) ;
}
}
foreach ( string subtitle in _ subtitle )
{
videoFilterQueue . Add ( subtitle ) ;
}
string outputPixelFormat = null ;
if ( usesSoftwareFilters & & acceleration ! = HardwareAccelerationKind . None & &
string . IsNullOrWhiteSpace ( watermarkOverlay ) )
@ -372,19 +477,6 @@ namespace ErsatzTV.Core.FFmpeg
@@ -372,19 +477,6 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue . Add ( upload ) ;
}
if ( ! usesSoftwareFilters & & string . IsNullOrWhiteSpace ( watermarkOverlay ) )
{
switch ( acceleration , _ videoEncoder , _ pixelFormat )
{
case ( HardwareAccelerationKind . Nvenc , "h264_nvenc" , "yuv420p10le" ) :
outputPixelFormat = "yuv420p" ;
break ;
case ( HardwareAccelerationKind . Nvenc , "h264_nvenc" , "yuv444p10le" ) :
outputPixelFormat = "yuv444p" ;
break ;
}
}
bool hasAudioFilters = audioFilterQueue . Any ( ) ;
if ( hasAudioFilters )
{
@ -447,6 +539,9 @@ namespace ErsatzTV.Core.FFmpeg
@@ -447,6 +539,9 @@ namespace ErsatzTV.Core.FFmpeg
case ( true , HardwareAccelerationKind . Nvenc ) :
complexFilter . Append ( ",hwupload_cuda" ) ;
break ;
// no need to upload since we're already in the GPU with overlay_cuda
case ( _ , HardwareAccelerationKind . Nvenc ) when scaleOrPad = = false & & _ watermark . IsSome :
break ;
case ( _ , HardwareAccelerationKind . Qsv ) :
complexFilter . Append ( ",format=yuv420p,hwupload=extra_hw_frames=64" ) ;
break ;