diff --git a/FFEncoder.ps1 b/FFEncoder.ps1 index 30cae25..518f421 100644 --- a/FFEncoder.ps1 +++ b/FFEncoder.ps1 @@ -42,16 +42,26 @@ .EXAMPLE ## Scale 2160p video down to 1080p using zscale and spline36 ## .\FFEncoder "$HOME\Videos\Ex.Machina.2014.DTS-HD.2160p.mkv" -Scale zscale -ScaleFilter spline36 -Res 1080p -CRF 18 -o "$HOME\Videos\Ex Machina (2014) DTS-HD 1080p.mkv" + .EXAMPLE + ## Use a Vapoursynth script as input + .\FFEncoder 'in.mkv' -VapourSynthScript "$HOME/script.vpy -CRF 18 -o 'out.mkv'" .INPUTS - HD/FHD/UHD video file + HD/FHD/UHD video file + Vapoursynth Script .OUTPUTS - crop.txt - File used for auto-cropping - 4K HDR encoded video file + Crop file + Log file(s) + Encoded video file .NOTES - For FFEncoder to work, the ffmpeg directory must be in your system PATH (consult your OS documentation for info on how to verify this) + For script binaries to work, they must be included in the system PATH (consult OS documentation for more information): + - ffmpeg + - deew / dee + - mkvmerge + - mkvextract + - x265 Be sure to include an extension at the end of your output file (.mkv, .mp4, .ts, etc.), - or you may be left with a file that will not play + or you may be left with a file that will not play (OS dependent). .PARAMETER Help Displays help information for the script @@ -217,6 +227,22 @@ Deinterlacing filter using yadif. Currently only works with CRF encoding .PARAMETER GenerateReport Generates a user friendly report file with important encoding metrics pulled from the log file. File is saved with a .rep extension + .PARAMETER GenerateMKVTagFile + Generate an XML tag file for MKV containers using the TMDB API. Requires a valid TMDB API key + .PARAMETER CompareVMAF + Switch to enable a VMAF comparison. Mandatory to enable this feature + .PARAMETER EnablePSNR + VMAF option. Enables Peak Signal to Noise Ratio (PSNR) evaluation + .PARAMETER EnableSSIM + VMAF option. Enables Structural Similarity Index Measurement (SSIM) evaluation + .PARAMETER LogFormat + Specify the log format for VMAF. Options: + - json + - csv + - sub + - xml + .PARAMETER VapourSynthScript + Pass a VapourSynth script for filtering. Note that all filtering (including cropping) must be done in the VS script .lINK Check out the full documentation on GitHub - https://github.com/patrickenfuego/FFEncoder .LINK @@ -633,7 +659,7 @@ param ( [Parameter(Mandatory = $false, ParameterSetName = 'VMAF')] [ValidateSet('json', 'xml', 'csv', 'sub')] [Alias('LogType', 'VMAFLog')] - [string]$LogFormat + [string]$LogFormat = 'json' ) ######################################################### diff --git a/README.md b/README.md index 7158dc2..8f9ded8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ FFEncoder is a cross-platform PowerShell script and module that is meant to make - [VMAF Comparison](#vmaf-comparison) - [MKV Tag Generator](#mkv-tag-generator) - [Script Parameters](#script-parameters) + - [Configuration Files](#configuration-files) - [Mandatory](#mandatory) - [Utility](#utility) - [Audio & Subtitles](#audio--subtitles) @@ -51,15 +52,13 @@ Check out the [wiki](https://github.com/patrickenfuego/FFEncoder/wiki) for addit - Mkvtoolnix (optional, but highly recommended) - VapourSynth (optional) -The script requires PowerShell 7.0 or newer on all systems as it utilizes new parallel processing features introduced in this version. Multi-threading prior to PowerShell 7 was prone to memory leaks which persuaded me to make the change. - For users with PowerShell 7.2 or newer, the script uses ANSI output in certain scenarios to enhance the console experience. --- ## Dependency Installation -> You can compile ffmpeg manually from source on all platforms, which allows you to select additional libraries (like Fraunhofer's libfdk AAC encoder). For more information, see [here](https://trac.ffmpeg.org/wiki/CompilationGuide) +> You can compile ffmpeg manually from source on all platforms, which allows you to select additional libraries (like Fraunhofer's libfdk AAC encoder). Some features of this script are unavailable unless these libraries are included. For more information, see [here](https://trac.ffmpeg.org/wiki/CompilationGuide). ### Windows @@ -162,6 +161,12 @@ To use this parameter, you will need a valid TMDB API key. See [the wiki](https: ## Script Parameters +### Configuration Files + +Two configuration files, `ffmpeg.ini` and `encoder.ini`, are included and can be used to set frequently used options not covered by script parameters. These files are located in the `config` directory and are loaded each time the script runs. + +See the wiki for more information. + FFEncoder can accept the following parameters from the command line: ### Mandatory diff --git a/config/encoder.ini b/config/encoder.ini new file mode 100644 index 0000000..c653c44 --- /dev/null +++ b/config/encoder.ini @@ -0,0 +1,8 @@ +; COMMENT +; Encoder specific settings in key=value form + +[x264] +;open-gop=0 + +[x265] +;open-gop=0 \ No newline at end of file diff --git a/config/ffmpeg.ini b/config/ffmpeg.ini new file mode 100644 index 0000000..5eb1933 --- /dev/null +++ b/config/ffmpeg.ini @@ -0,0 +1,9 @@ +; COMMENT + +[Arguments] +; Settings that take arguments. Do not quote arguments: +;-loglevel=panic + +[NoArguments] +; Settings that take no arguments: +;-hide_banner \ No newline at end of file diff --git a/modules/FFTools/FFTools.psd1 b/modules/FFTools/FFTools.psd1 index 48b25a3..03f5636 100644 --- a/modules/FFTools/FFTools.psd1 +++ b/modules/FFTools/FFTools.psd1 @@ -70,7 +70,7 @@ PowerShellVersion = '7.0' # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = 'Invoke-FFMpeg', 'Invoke-TwoPassFFMpeg', 'New-CropFile', 'Measure-CropDimensions', 'Remove-FilePrompt', 'Write-Report', 'Confirm-HDR10Plus', - 'Confirm-DolbyVision', 'Confirm-ScaleFilter', 'Invoke-MkvMerge', 'Invoke-DeeEncoder', 'Read-TimedInput', 'Invoke-VMAF' + 'Confirm-DolbyVision', 'Confirm-ScaleFilter', 'Invoke-MkvMerge', 'Invoke-DeeEncoder', 'Read-TimedInput', 'Invoke-VMAF', 'Read-Config' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. # CmdletsToExport = @() @@ -92,7 +92,7 @@ FileList = 'FFTools.psd1', 'FFTools.psm1', 'Private\Set-AudioPreference.ps1', 'P 'Private\Get-HDRMetadata.ps1', 'Public\Invoke-FFMpeg.ps1', 'Public\Invoke-TwoPassFFMpeg.ps1', 'Public\New-CropFile.ps1', 'Public\Measure-CropDimensions.ps1', 'Public\Invoke-VMAF.ps1', 'Private\ConvertTo-Stereo.ps1', 'Private\Set-PresetParameters.ps1', 'Private\Set-FFMPegArgs.ps1', 'Private\Set-VideoFilter.ps1', 'Private\Set-TestParameters.ps1', 'Private\Confirm-Parameters.ps1', 'Private\Set-DVArgs.ps1', 'Utils\Invoke-DeeEncoder.ps1','Utils\Confirm-ScaleFilter.ps1','Utils\Write-Report.ps1', - 'Utils\Invoke-MkvMerge.ps1', 'Utils\Confirm-HDR10Plus.ps1', 'Utils\Confirm-DolbyVision.ps1', 'Utils\Remove-FilePrompt.ps1' + 'Utils\Invoke-MkvMerge.ps1', 'Utils\Confirm-HDR10Plus.ps1', 'Utils\Confirm-DolbyVision.ps1', 'Utils\Remove-FilePrompt.ps1', 'Utils\Read-Config.ps1' # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ diff --git a/modules/FFTools/FFTools.psm1 b/modules/FFTools/FFTools.psm1 index ea32db1..6b2a728 100644 --- a/modules/FFTools/FFTools.psm1 +++ b/modules/FFTools/FFTools.psm1 @@ -133,7 +133,7 @@ ___________ .__ __ .__ ______________________ '@ # Current script release version -[version]$release = '2.1.0' +[version]$release = '2.2.0' #### End module variables #### @@ -185,7 +185,7 @@ New-Alias -Name cropdim -Value Measure-CropDimensions -Force $ExportModule = @{ Alias = @('iffmpeg', 'cropfile', 'cropdim') Function = @('Invoke-FFmpeg', 'Invoke-TwoPassFFmpeg', 'New-CropFile', 'Measure-CropDimensions', 'Remove-FilePrompt', 'Write-Report', 'Confirm-HDR10Plus', - 'Confirm-DolbyVision', 'Confirm-ScaleFilter', 'Invoke-MkvMerge', 'Invoke-DeeEncoder', 'Read-TimedInput', 'Invoke-VMAF') + 'Confirm-DolbyVision', 'Confirm-ScaleFilter', 'Invoke-MkvMerge', 'Invoke-DeeEncoder', 'Read-TimedInput', 'Invoke-VMAF', 'Read-Config') Variable = @('progressColors', 'warnColors', 'emphasisColors', 'errColors', 'osInfo', 'banner1', 'banner2', 'exitBanner', 'ScriptsDirectory', 'release' ) } diff --git a/modules/FFTools/Private/Set-DVArgs.ps1 b/modules/FFTools/Private/Set-DVArgs.ps1 index 93273aa..07780b4 100644 --- a/modules/FFTools/Private/Set-DVArgs.ps1 +++ b/modules/FFTools/Private/Set-DVArgs.ps1 @@ -96,7 +96,7 @@ function Set-DVArgs { [int[]]$VBV, [Parameter(Mandatory = $false)] - [array]$FFMpegExtra, + [Generic.List[object]]$FFMpegExtra, [Parameter(Mandatory = $false)] [hashtable]$EncoderExtra, @@ -457,13 +457,6 @@ function Set-DVArgs { #> if ($x265ExtraArray) { $x265BaseArray.AddRange($x265ExtraArray) } - - switch ($x265ExtraArray) { - { $_ -notcontains '--open-gop' } { $x265BaseArray.Add('--no-open-gop') > $null } - { $_ -notcontains '--keyint' } { $x265BaseArray.AddRange(@('--keyint', 192)) } - { $_ -notcontains '--min-keyint' } { $x265BaseArray.AddRange(@('--min-keyint', 24)) } - } - ($PresetParams.BIntra -eq 1) ? ($x265BaseArray.Add('--b-intra') > $null) : diff --git a/modules/FFTools/Private/Set-FFMpegArgs.ps1 b/modules/FFTools/Private/Set-FFMpegArgs.ps1 index 388bb49..97e371b 100644 --- a/modules/FFTools/Private/Set-FFMpegArgs.ps1 +++ b/modules/FFTools/Private/Set-FFMpegArgs.ps1 @@ -99,7 +99,7 @@ function Set-FFMpegArgs { [int[]]$VBV, [Parameter(Mandatory = $false)] - [array]$FFMpegExtra, + [Generic.List[object]]$FFMpegExtra, # Extra encoder parameters passed by user [Parameter(Mandatory = $false)] @@ -138,7 +138,7 @@ function Set-FFMpegArgs { ## Unpack extra parameters ## if ($PSBoundParameters['FFMpegExtra']) { - $ffmpegExtraArray = [ArrayList]@() + $ffmpegExtraArray = [Generic.List[object]]@() foreach ($arg in $FFMpegExtra) { if ($arg -is [hashtable]) { foreach ($entry in $arg.GetEnumerator()) { @@ -152,20 +152,13 @@ function Set-FFMpegArgs { } } - $skip = @{} - @('OpenGop', 'Keyint', 'MinKeyInt', 'Sao').ForEach({ $skip.$_ = $false }) if ($PSBoundParameters['EncoderExtra']) { $encoderExtraArray = [ArrayList]@() foreach ($arg in $EncoderExtra.GetEnumerator()) { - elseif ($arg.Name -eq 'open-gop') { $skip.OpenGOP = $true } - elseif ($arg.Name -eq 'keyint') { $skip.Keyint = $true } - elseif ($arg.Name -eq 'min-keyint') { $skip.MinKeyint = $true } - else { - $encoderExtraArray.Add("$($arg.Name)=$($arg.Value)") > $null - } + $encoderExtraArray.Add("$($arg.Name)=$($arg.Value)") > $null } } - + ## Base Array Declarations ## #Primary array list initialized with global values @@ -301,13 +294,6 @@ function Set-FFMpegArgs { $ffmpegExtraArray.RemoveRange($i, 2) } - # Set hard coded defaults unless overridden - switch ($skip) { - { $skip.OpenGOP -eq $false } { $encoderBaseArray.Add('open-gop=0') > $null } - { $skip.KeyInt -eq $false } { $encoderBaseArray.Add('keyint=192') > $null } - { $skip.MinKeyInt -eq $false } { $encoderBaseArray.Add('min-keyint=24') > $null } - } - # Set video specific filter arguments unless VS is used if ([string]::IsNullOrEmpty($Paths.VPY)) { $vfHash = @{ diff --git a/modules/FFTools/Public/Invoke-FFMpeg.ps1 b/modules/FFTools/Public/Invoke-FFMpeg.ps1 index 18b4ebc..3a6887c 100644 --- a/modules/FFTools/Public/Invoke-FFMpeg.ps1 +++ b/modules/FFTools/Public/Invoke-FFMpeg.ps1 @@ -1,4 +1,5 @@ using namespace System.IO +using namespace System.Collections <# .SYNOPSIS @@ -165,7 +166,7 @@ function Invoke-FFMpeg { # Additional ffmpeg options [Parameter(Mandatory = $false)] [Alias('FE', 'FFExtra')] - [array]$FFMpegExtra, + [Generic.List[object]]$FFMpegExtra, # Additional encoder-specific options [Parameter(Mandatory = $false)] @@ -402,10 +403,58 @@ function Invoke-FFMpeg { <# BUILD FINAL ARGUMENT ARRAYS + Pull configuration file contents Set the base arguments Pass arguments to Set-FFMpegArgs or Set-DvArgs functions to prepare for encoding #> + + # Try to parse the config files + try { + # Read config file for additional options + $config = Read-Config -Encoder $Encoder + + # Add multi-valued ffmpeg config options to existing hash + if ($config['FFMpegHash']) { + if ($FFMpegExtra) { + $index = $ffmpegExtra.FindIndex( { + $args[0] -is [hashtable] + } ) + + if ($index -ne -1) { + # Catch error if duplicate keys are present + $FFMpegExtra[$index] += $config['FFMpegHash'] + } + else { $FFMpegExtra.Add($config['FFMpegHash']) } + } + else { + $FFMpegExtra = @() + $FFMpegExtra.Add($config['FFMpegHash']) + } + } + + # Add single valued ffmpeg config options + if ($config['FFMpegArray']) { + if ($FFMpegExtra) { $ffmpegExtra.AddRange($config['FFMpegArray']) } + else { + $FFMpegExtra = @() + $FFMpegExtra.AddRange($config['FFMpegArray']) + } + } + + # Add encoder settings from config file + if ($config['Encoder']) { + if ($EncoderExtra) { + $EncoderExtra += $config['Encoder'] + } + else { $EncoderExtra = $config['Encoder'] } + } + } + catch { + $e = $_.Exception.Message + Write-Error "Failed to parse the configuration file(s): $e" + } + $baseArgs = @{ Encoder = $Encoder Audio = $audio diff --git a/modules/FFTools/Utils/Read-Config.ps1 b/modules/FFTools/Utils/Read-Config.ps1 new file mode 100644 index 0000000..98595d3 --- /dev/null +++ b/modules/FFTools/Utils/Read-Config.ps1 @@ -0,0 +1,94 @@ +using namespace System.IO + +<# + .SYNOPSIS + Parses FFEncoder .ini file + .DESCRIPTION + Users may add default values to the encoder.ini file if they do not + wish to set them manually each run with the -EncoderExtra option. + The values parsed from this file will be added to -EncoderExtra + automatically. + .PARAMETER Encoder + User selected encoder + .PARAMETER EncoderConfigFile + Location of encoder configuration file. Default is ../../../config/encoder.ini + .PARAMETER FFMpegConfigFile + Location of ffmpeg configuration file. Default is ../../../config/ffmpeg.ini +#> +function Read-Config { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Encoder, + + [Parameter(Mandatory = $false)] + [string]$EncoderConfigFile = + [Path]::Join((Get-Item $PSScriptRoot).Parent.Parent.Parent, 'config', 'encoder.ini'), + + [Parameter(Mandatory = $false)] + [string]$FFMpegConfigFile = + [Path]::Join((Get-Item $PSScriptRoot).Parent.Parent.Parent, 'config', 'ffmpeg.ini') + ) + + $encoderHash = @{} + $ffmpegHash = @{} + $ffmpegArray = [System.Collections.Generic.List[string]]@() + $skipEncoder, $skipFFMpeg = $false, $false + + if (![File]::Exists($EncoderConfigFile)) { + $skipEncoder = $true + } + if (![File]::Exists($FFMpegConfigFile)) { + Write-Host "Skipping ffmpeg config" + $skipFFMpeg = $true + } + + if ($skipEncoder -and $skipFFMpeg) { + Write-Warning "Could not locate configuration file paths. Configurations will not be copied" + return + } + + if (!$skipEncoder) { + $encoderINI = [File]::ReadAllLines($EncoderConfigFile) + $start = $encoderINI.IndexOf("[$encoder]") + foreach ($line in $encoderINI[($start + 1)..($encoderINI.Length - 1)]) { + if ($line.StartsWith('[')) { + break + } + elseif ($line -notlike '*=*') { + continue + } + elseif ($line -and $line -notlike ';*') { + $name, $value = $line.Split('=').Trim() + $encoderHash[$name] = $value + } + } + } + if (!$skipFFMpeg) { + $ffmpegINI = [File]::ReadAllLines($FFMpegConfigFile) + $noArgs = $ffmpegINI.IndexOf('[NoArguments]') + # Parse Arguments + foreach ($line in $ffmpegINI[1..($noArgs - 1)]) { + if ($line -and $line -notlike ';*' -and $line -like '*=*') { + $name, $value = ($line -split '=', 2).Trim() + $ffmpegHash[$name] = $value + } + } + # Parse NoArguments + foreach ($line in $ffmpegINI[($noArgs + 1)..($ffmpegINI.Length - 1)]) { + if ($line -and $line -notlike ';*' -and $line -notlike '*=*') { + $ffmpegArray.Add($line) + } + } + } + + $returnHash = @{ + Encoder = ($encoderHash.Count -gt 0) ? $encoderHash : $null + FFMpegHash = ($ffmpegHash.Count -gt 0) ? $ffmpegHash : $null + FFMpegArray = ($ffmpegArray.Count -gt 0) ? $ffmpegArray : $null + } + + Write-Verbose "Parsed config file contents:`n $($returnHash | Out-String)" + + return $returnHash +}