2021-08-23 16:50:18 +02:00
using Flowframes.IO ;
using System ;
using System.Collections.Generic ;
2021-10-25 15:30:47 +02:00
using System.Drawing ;
2021-08-23 16:50:18 +02:00
using System.IO ;
using System.Linq ;
2021-09-14 18:28:10 +02:00
using System.Threading.Tasks ;
2021-08-23 16:50:18 +02:00
namespace Flowframes.Media
{
class FfmpegUtils
{
2022-06-06 07:03:27 +02:00
public enum Codec { H264 , H265 , H264Nvenc , H265Nvenc , Av1 , Vp9 , ProRes , AviRaw , Gif }
2021-08-23 16:50:18 +02:00
public static Codec GetCodec ( Interpolate . OutMode mode )
{
if ( mode = = Interpolate . OutMode . VidMp4 | | mode = = Interpolate . OutMode . VidMkv )
{
int mp4MkvEnc = Config . GetInt ( Config . Key . mp4Enc ) ;
if ( mp4MkvEnc = = 0 ) return Codec . H264 ;
if ( mp4MkvEnc = = 1 ) return Codec . H265 ;
if ( mp4MkvEnc = = 2 ) return Codec . H264Nvenc ;
if ( mp4MkvEnc = = 3 ) return Codec . H265Nvenc ;
if ( mp4MkvEnc = = 4 ) return Codec . Av1 ;
}
if ( mode = = Interpolate . OutMode . VidWebm )
return Codec . Vp9 ;
if ( mode = = Interpolate . OutMode . VidProRes )
return Codec . ProRes ;
if ( mode = = Interpolate . OutMode . VidAvi )
return Codec . AviRaw ;
2022-06-06 07:03:27 +02:00
if ( mode = = Interpolate . OutMode . VidGif )
return Codec . Gif ;
2021-08-23 16:50:18 +02:00
return Codec . H264 ;
}
public static string GetEnc ( Codec codec )
{
switch ( codec )
{
case Codec . H264 : return "libx264" ;
case Codec . H265 : return "libx265" ;
case Codec . H264Nvenc : return "h264_nvenc" ;
case Codec . H265Nvenc : return "hevc_nvenc" ;
case Codec . Av1 : return "libsvtav1" ;
case Codec . Vp9 : return "libvpx-vp9" ;
case Codec . ProRes : return "prores_ks" ;
case Codec . AviRaw : return Config . Get ( Config . Key . aviCodec ) ;
2022-06-06 07:03:27 +02:00
case Codec . Gif : return "gif" ;
2021-08-23 16:50:18 +02:00
}
2022-05-31 22:17:22 +02:00
2021-08-23 16:50:18 +02:00
return "libx264" ;
}
2022-06-06 07:03:27 +02:00
public static string [ ] GetEncArgs ( Codec codec , Size res , float fps , bool realtime = false ) // Array contains as many entries as there are encoding passes. If "realtime" is true, force single pass.
2021-08-23 16:50:18 +02:00
{
2021-10-27 11:04:18 +02:00
int keyint = 10 ;
2021-08-23 16:50:18 +02:00
2021-11-24 13:48:23 +01:00
if ( codec = = Codec . H264 )
2021-08-23 16:50:18 +02:00
{
string preset = Config . Get ( Config . Key . ffEncPreset ) . ToLower ( ) . Remove ( " " ) ;
2021-10-27 11:04:18 +02:00
string g = GetKeyIntArg ( fps , keyint ) ;
2021-11-24 13:48:23 +01:00
return new string [ ] { $"-c:v {GetEnc(codec)} -crf {Config.GetInt(Config.Key.h264Crf)} -preset {preset} {g} -pix_fmt {GetPixFmt()}" } ;
2021-08-23 16:50:18 +02:00
}
if ( codec = = Codec . H265 )
{
string preset = Config . Get ( Config . Key . ffEncPreset ) . ToLower ( ) . Remove ( " " ) ;
int crf = Config . GetInt ( Config . Key . h265Crf ) ;
2021-10-27 11:04:18 +02:00
string g = GetKeyIntArg ( fps , keyint ) ;
2021-11-24 13:48:23 +01:00
return new string [ ] { $"-c:v {GetEnc(codec)} {(crf > 0 ? $" - crf { crf } " : " - x265 - params lossless = 1 ")} -preset {preset} {g} -pix_fmt {GetPixFmt()}" } ;
2021-08-23 16:50:18 +02:00
}
if ( codec = = Codec . H264Nvenc )
{
int cq = ( Config . GetInt ( Config . Key . h264Crf ) * 1.1f ) . RoundToInt ( ) ;
2021-11-24 13:48:23 +01:00
return new string [ ] { $"-c:v {GetEnc(codec)} -b:v 0 {(cq > 0 ? $" - cq { cq } - preset p7 " : " - preset lossless ")} -pix_fmt {GetPixFmt()}" } ;
2021-08-23 16:50:18 +02:00
}
if ( codec = = Codec . H265Nvenc )
{
int cq = ( Config . GetInt ( Config . Key . h265Crf ) * 1.1f ) . RoundToInt ( ) ;
2021-11-24 13:48:23 +01:00
return new string [ ] { $"-c:v {GetEnc(codec)} -b:v 0 {(cq > 0 ? $" - cq { cq } - preset p7 " : " - preset lossless ")} -pix_fmt {GetPixFmt()}" } ;
2021-08-23 16:50:18 +02:00
}
if ( codec = = Codec . Av1 )
{
2021-10-25 15:30:47 +02:00
int cq = Config . GetInt ( Config . Key . av1Crf ) ;
2021-10-27 11:04:18 +02:00
string g = GetKeyIntArg ( fps , keyint ) ;
2022-07-04 10:14:54 +02:00
return new string [ ] { $"-c:v {GetEnc(codec)} -b:v 0 -qp {cq} {GetSvtAv1Speed()} {g} -svtav1-params enable-overlays=0:enable-tf=0:scd=0 -pix_fmt {GetPixFmt()}" } ;
2021-08-23 16:50:18 +02:00
}
if ( codec = = Codec . Vp9 )
{
int crf = Config . GetInt ( Config . Key . vp9Crf ) ;
2021-11-24 13:48:23 +01:00
string qualityStr = ( crf > 0 ) ? $"-crf {crf}" : "-lossless 1" ;
2021-10-27 11:04:18 +02:00
string g = GetKeyIntArg ( fps , keyint ) ;
2021-11-24 21:08:29 +01:00
string t = GetTilingArgs ( res , "-tile-columns " , "-tile-rows " ) ;
2022-06-06 07:03:27 +02:00
if ( realtime ) // Force 1-pass
{
return new string [ ] { $"-c:v {GetEnc(codec)} -b:v 0 {qualityStr} {GetVp9Speed()} {t} -row-mt 1 {g} -pix_fmt {GetPixFmt()}" } ;
}
else
{
return new string [ ] {
$"-c:v {GetEnc(codec)} -b:v 0 {qualityStr} {GetVp9Speed()} {t} -row-mt 1 {g} -pass 1 -pix_fmt {GetPixFmt()} -an" ,
$"-c:v {GetEnc(codec)} -b:v 0 {qualityStr} {GetVp9Speed()} {t} -row-mt 1 {g} -pass 2 -pix_fmt {GetPixFmt()}"
} ;
}
2021-08-23 16:50:18 +02:00
}
2021-11-24 13:48:23 +01:00
if ( codec = = Codec . ProRes )
2021-08-23 16:50:18 +02:00
{
2021-11-24 13:48:23 +01:00
return new string [ ] { $"-c:v {GetEnc(codec)} -profile:v {Config.GetInt(Config.Key.proResProfile)} -pix_fmt {GetPixFmt()}" } ;
2021-08-23 16:50:18 +02:00
}
if ( codec = = Codec . AviRaw )
{
2021-11-24 13:48:23 +01:00
return new string [ ] { $"-c:v {GetEnc(codec)} -pix_fmt {Config.Get(Config.Key.aviColors)}" } ;
2021-08-23 16:50:18 +02:00
}
2022-06-06 07:03:27 +02:00
if ( codec = = Codec . Gif )
{
return new string [ ] { $"-c:v {GetEnc(codec)} -gifflags -offsetting" } ;
}
2021-11-24 13:48:23 +01:00
return new string [ 0 ] ;
2021-08-23 16:50:18 +02:00
}
2021-10-25 15:30:47 +02:00
public static string GetTilingArgs ( Size resolution , string colArg , string rowArg )
{
int cols = 0 ;
if ( resolution . Width > = 1920 ) cols = 1 ;
if ( resolution . Width > = 3840 ) cols = 2 ;
if ( resolution . Width > = 7680 ) cols = 3 ;
int rows = 0 ;
2021-11-24 21:08:29 +01:00
if ( resolution . Height > = 1600 ) rows = 1 ;
if ( resolution . Height > = 3200 ) rows = 2 ;
if ( resolution . Height > = 6400 ) rows = 3 ;
2021-10-25 15:30:47 +02:00
2021-11-24 21:08:29 +01:00
Logger . Log ( $"GetTilingArgs: Video resolution is {resolution.Width}x{resolution.Height} - Using 2^{cols} columns, 2^{rows} rows (=> {Math.Pow(2, cols)}x{Math.Pow(2, rows)} = {Math.Pow(2, cols) * Math.Pow(2, rows)} Tiles)" , true ) ;
return $"{(cols > 0 ? colArg+cols : "")} {(rows > 0 ? rowArg + rows : "")}" ;
2021-10-25 15:30:47 +02:00
}
2021-10-27 11:04:18 +02:00
public static string GetKeyIntArg ( float fps , int intervalSeconds , string arg = "-g " )
{
2021-11-24 21:08:29 +01:00
int keyInt = ( fps * intervalSeconds ) . RoundToInt ( ) . Clamp ( 20 , 300 ) ;
2021-10-27 11:04:18 +02:00
return $"{arg}{keyInt}" ;
}
2021-11-24 13:48:23 +01:00
static string GetVp9Speed ( )
2021-08-23 16:50:18 +02:00
{
string preset = Config . Get ( Config . Key . ffEncPreset ) . ToLower ( ) . Remove ( " " ) ;
string arg = "" ;
if ( preset = = "veryslow" ) arg = "0" ;
if ( preset = = "slower" ) arg = "1" ;
if ( preset = = "slow" ) arg = "2" ;
if ( preset = = "medium" ) arg = "3" ;
if ( preset = = "fast" ) arg = "4" ;
if ( preset = = "faster" ) arg = "5" ;
if ( preset = = "veryfast" ) arg = "4 -deadline realtime" ;
return $"-cpu-used {arg}" ;
}
static string GetSvtAv1Speed ( )
{
string preset = Config . Get ( Config . Key . ffEncPreset ) . ToLower ( ) . Remove ( " " ) ;
string arg = "8" ;
2022-04-18 04:30:37 +02:00
if ( preset = = "veryslow" ) arg = "3" ;
if ( preset = = "slower" ) arg = "4" ;
if ( preset = = "slow" ) arg = "5" ;
if ( preset = = "medium" ) arg = "6" ;
if ( preset = = "fast" ) arg = "7" ;
if ( preset = = "faster" ) arg = "8" ;
if ( preset = = "veryfast" ) arg = "9" ;
2021-08-23 16:50:18 +02:00
return $"-preset {arg}" ;
}
2021-11-24 13:48:23 +01:00
static string GetPixFmt ( )
2021-08-23 16:50:18 +02:00
{
switch ( Config . GetInt ( Config . Key . pixFmt ) )
{
case 0 : return "yuv420p" ;
case 1 : return "yuv444p" ;
case 2 : return "yuv420p10le" ;
case 3 : return "yuv444p10le" ;
}
return "yuv420p" ;
}
2021-11-24 13:48:23 +01:00
public static bool ContainerSupportsAllAudioFormats ( Interpolate . OutMode outMode , List < string > codecs )
2021-08-23 16:50:18 +02:00
{
2021-11-24 13:48:23 +01:00
if ( codecs . Count < 1 )
2021-08-23 16:50:18 +02:00
Logger . Log ( $"Warning: ContainerSupportsAllAudioFormats() was called, but codec list has {codecs.Count} entries." , true , false , "ffmpeg" ) ;
foreach ( string format in codecs )
{
if ( ! ContainerSupportsAudioFormat ( outMode , format ) )
return false ;
}
return true ;
}
2021-11-24 13:48:23 +01:00
public static bool ContainerSupportsAudioFormat ( Interpolate . OutMode outMode , string format )
2021-08-23 16:50:18 +02:00
{
bool supported = false ;
string alias = GetAudioExt ( format ) ;
2021-09-27 13:03:23 +02:00
string [ ] formatsMp4 = new string [ ] { "m4a" , "mp3" , "ac3" , "dts" } ;
string [ ] formatsMkv = new string [ ] { "m4a" , "mp3" , "ac3" , "dts" , "ogg" , "mp2" , "wav" , "wma" } ;
2021-08-23 16:50:18 +02:00
string [ ] formatsWebm = new string [ ] { "ogg" } ;
string [ ] formatsProres = new string [ ] { "m4a" , "ac3" , "dts" , "wav" } ;
string [ ] formatsAvi = new string [ ] { "m4a" , "ac3" , "dts" } ;
switch ( outMode )
{
case Interpolate . OutMode . VidMp4 : supported = formatsMp4 . Contains ( alias ) ; break ;
case Interpolate . OutMode . VidMkv : supported = formatsMkv . Contains ( alias ) ; break ;
case Interpolate . OutMode . VidWebm : supported = formatsWebm . Contains ( alias ) ; break ;
case Interpolate . OutMode . VidProRes : supported = formatsProres . Contains ( alias ) ; break ;
case Interpolate . OutMode . VidAvi : supported = formatsAvi . Contains ( alias ) ; break ;
}
Logger . Log ( $"Checking if {outMode} supports audio format '{format}' ({alias}): {supported}" , true , false , "ffmpeg" ) ;
return supported ;
}
public static string GetExt ( Interpolate . OutMode outMode , bool dot = true )
{
string ext = dot ? "." : "" ;
switch ( outMode )
{
case Interpolate . OutMode . VidMp4 : ext + = "mp4" ; break ;
case Interpolate . OutMode . VidMkv : ext + = "mkv" ; break ;
case Interpolate . OutMode . VidWebm : ext + = "webm" ; break ;
case Interpolate . OutMode . VidProRes : ext + = "mov" ; break ;
case Interpolate . OutMode . VidAvi : ext + = "avi" ; break ;
case Interpolate . OutMode . VidGif : ext + = "gif" ; break ;
}
return ext ;
}
2021-11-24 13:48:23 +01:00
public static string GetAudioExt ( string codec )
2021-08-23 16:50:18 +02:00
{
if ( codec . StartsWith ( "pcm_" ) )
return "wav" ;
switch ( codec )
{
case "vorbis" : return "ogg" ;
case "opus" : return "ogg" ;
case "mp2" : return "mp2" ;
case "mp3" : return "mp3" ;
case "aac" : return "m4a" ;
case "ac3" : return "ac3" ;
case "eac3" : return "ac3" ;
case "dts" : return "dts" ;
case "alac" : return "wav" ;
case "flac" : return "wav" ;
case "wmav1" : return "wma" ;
case "wmav2" : return "wma" ;
}
return "unsupported" ;
}
2021-11-24 13:48:23 +01:00
public static async Task < string > GetAudioFallbackArgs ( string videoPath , Interpolate . OutMode outMode , float itsScale )
2021-08-23 16:50:18 +02:00
{
2021-09-13 22:42:33 +02:00
bool opusMp4 = Config . GetBool ( Config . Key . allowOpusInMp4 ) ;
2021-09-14 18:28:10 +02:00
int opusBr = Config . GetInt ( Config . Key . opusBitrate , 128 ) ;
int aacBr = Config . GetInt ( Config . Key . aacBitrate , 160 ) ;
2021-10-20 20:28:21 +02:00
int ac = ( await GetVideoInfo . GetFfprobeInfoAsync ( videoPath , GetVideoInfo . FfprobeMode . ShowStreams , "channels" , 0 ) ) . GetInt ( ) ;
string af = GetAudioFilters ( itsScale ) ;
2021-08-23 16:50:18 +02:00
2021-09-13 22:42:33 +02:00
if ( outMode = = Interpolate . OutMode . VidMkv | | outMode = = Interpolate . OutMode . VidWebm | | ( outMode = = Interpolate . OutMode . VidMp4 & & opusMp4 ) )
2021-10-20 20:28:21 +02:00
return $"-c:a libopus -b:a {(ac > 4 ? $" { opusBr * 2 } " : $" { opusBr } ")}k -ac {(ac > 0 ? $" { ac } " : " 2 ")} {af}" ; // Double bitrate if 5ch or more, ignore ac if <= 0
2021-09-13 22:42:33 +02:00
else
2021-10-20 20:28:21 +02:00
return $"-c:a aac -b:a {(ac > 4 ? $" { aacBr * 2 } " : $" { aacBr } ")}k -aac_coder twoloop -ac {(ac > 0 ? $" { ac } " : " 2 ")} {af}" ;
}
private static string GetAudioFilters ( float itsScale )
{
if ( itsScale = = 0 | | itsScale = = 1 )
return "" ;
2021-11-24 13:48:23 +01:00
if ( itsScale > 4 )
2021-10-20 20:30:27 +02:00
return $"-af atempo=0.5,atempo=0.5,atempo={((1f / itsScale) * 4).ToStringDot()}" ;
2021-10-20 20:28:21 +02:00
else if ( itsScale > 2 )
return $"-af atempo=0.5,atempo={((1f / itsScale) * 2).ToStringDot()}" ;
else
return $"-af atempo={(1f / itsScale).ToStringDot()}" ;
2021-08-23 16:50:18 +02:00
}
public static string GetSubCodecForContainer ( string containerExt )
{
2021-11-24 13:48:23 +01:00
containerExt = containerExt . Remove ( "." ) ;
2021-08-23 16:50:18 +02:00
2022-06-06 07:03:27 +02:00
if ( containerExt = = "mp4" | | containerExt = = "mov" ) return "mov_text" ;
2021-08-23 16:50:18 +02:00
if ( containerExt = = "webm" ) return "webvtt" ;
2021-09-13 22:44:16 +02:00
return "copy" ; // Default: Copy subs
2021-08-23 16:50:18 +02:00
}
public static bool ContainerSupportsSubs ( string containerExt , bool showWarningIfNotSupported = true )
{
containerExt = containerExt . Remove ( "." ) ;
bool supported = ( containerExt = = "mp4" | | containerExt = = "mkv" | | containerExt = = "webm" | | containerExt = = "mov" ) ;
Logger . Log ( $"Subtitles {(supported ? " are supported " : " not supported ")} by {containerExt.ToUpper()}" , true ) ;
if ( showWarningIfNotSupported & & Config . GetBool ( Config . Key . keepSubs ) & & ! supported )
2022-06-06 07:03:27 +02:00
Logger . Log ( $"Warning: {containerExt.ToUpper()} exports do not include subtitles." ) ;
2021-11-24 13:48:23 +01:00
2021-08-23 16:50:18 +02:00
return supported ;
}
2021-11-24 13:48:23 +01:00
public static void CreateConcatFile ( string inputFilesDir , string outputPath , string [ ] validExtensions = null )
2021-08-23 16:50:18 +02:00
{
string concatFileContent = "" ;
string [ ] files = IoUtils . GetFilesSorted ( inputFilesDir ) ;
foreach ( string file in files )
{
if ( validExtensions ! = null & & ! validExtensions . Contains ( Path . GetExtension ( file ) . ToLower ( ) ) )
continue ;
concatFileContent + = $"file '{file.Replace(@" \ ", " / ")}'\n" ;
}
File . WriteAllText ( outputPath , concatFileContent ) ;
}
}
}