Dmi3.PowerShell.FFTools.psm1
<#
.SYNOPSIS A module for various FFmpeg tasks including subtitle syncing, extractopm, copy and transcode streams for compatibilty, counting streams, analysing audio and codecs and merging. .DESCRIPTION This module provides a set of functions to perform various FFmpeg tasks. .EXAMPLE Invoke-FFTools subsync 'movie.mkv' -Offset 2.5 Synchronizes the subtitles in 'movie.mkv' by adding an offset of 2.5 seconds. Generates the following ffmpeg command: # TODO .EXAMPLE Invoke-FFTools extract 'movie.mkv' -Video -Audio -Sub Extracts the video, audio, and subtitle streams from 'movie.mkv'. Generates the following ffmpeg command: # TODO .EXAMPLE Invoke-FFTools merge 'movie1.mkv' -ExtraInputFiles 'audio.mka', 'movie2.mkv' Merges 'movie1.mkv', 'movie2.mkv', and 'audio.mkv' into a single file. Generates the following ffmpeg command: # TODO .PARAMETER Command The command to execute. This parameter has the index 0 and is mandatory, so `-Command` can be omitted. .PARAMETER InputFile The input file for the command. This parameter has the index 1 and is mandatory, so `-InputFile` can be omitted. .PARAMETER Preview Show the FFmpeg command that will be executed and asks for confirmation before executing. .PARAMETER Video Processes the video stream for certain commands. Alias: -v .PARAMETER Audio Processes the audio stream for certain commands. Alias: -a .PARAMETER Sub Processes the subtitle stream for certain commands. Alias: -s .PARAMETER AudioMap Specifies the audio stream map for certain commands in ffmpeg syntax. Default is '0:a:0'. .PARAMETER SubMap Specifies the subtitle stream map for certain commands in ffmpeg syntax. Default is '0:s:0'. .PARAMETER NoPathFix Disable automatic path fixing for certain use-cases. .PARAMETER Offset The offset for the subsync command. Can be a positive or negative double. .PARAMETER SubFormat The subtitle format for the extract command. .PARAMETER ExtraInputFiles Additional input files for the merge command. .Notes Author: Dmitrij Drandarov Source: https://github.com/drandarov-io/Dmi3.PowerShell.FFTools #> function Invoke-FFTools { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Mandatory=$true, Position=0)] [ValidateSet('subsync', 'extract', 'copytranscode', 'countstreams', 'codecs', 'audiomap', 'map', 'merge')] [string]$Command, [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)] [string]$InputFile, ############### # Common [Alias('p')] [switch]$Preview, [Alias('v')] [switch]$Video, [Alias('a')] [switch]$Audio, [Alias('s')] [switch]$Sub, [string]$AudioMap = '0:a:0', [string]$SubMap = '0:s:0', [switch]$NoPathFix = $false, ############### # Subsync [Parameter(Mandatory=$true, Position=2, ParameterSetName='subsync')] [double]$Offset, ############### # Subextract [Parameter(Mandatory=$false, Position=3, ParameterSetName='extract')] [ValidateSet('srt', 'ass')] [string]$SubFormat = 'srt', ############### # Merge [Parameter(Mandatory=$true, Position=2, ParameterSetName='merge')] [string[]]$ExtraInputFiles ) process { filter ext { [IO.Path]::ChangeExtension($_ ? $_ : $args[0], $_ ? $args[0] : $args[1]) } filter suffix { ($_ ? $_ : $args[0]) -replace '\.[^.]+$', "$($_ ? $args[0] : $args[1])$&" } function mergeobj { $obj1, $prop1, $obj2, $prop2 = $args; $obj1 | ForEach-Object { $p = $_; $obj2 | Where-Object { $p.$prop1 -eq $_.$prop2 } } } # ffmpeg requires literal paths, also this script uses single quotes so they need to be escaped as '' if (-not $NoPathFix) { if (-not (Test-Path -LiteralPath $InputFile)) { $InputFile = Resolve-Path -Relative $InputFile -ErrorAction SilentlyContinue # Convert to literal path if (-not $InputFile) { Write-Error "File is not accessible or does not exist."; return } Write-Host "Converted Path: $InputFile" } $InputFile = $InputFile.Replace("'", "''") } $vas = (!$Video -and !$Audio -and !$Sub) # TODO: Try to replace Command strings with actual commands that get translated into strings # TODO Proper object output switch ($Command) { 'subsync' { if ($Offset -eq 0 ) { Write-Error "subsync -Offset [double] is mandatory and must not be 0"; return } $newSubtitleFile = suffix $InputFile ('.' + (($Offset -gt 0) ? "+$Offset" : "$Offset")) $ffmpegCommand = "ffmpeg -y -itsoffset $Offset -i '$InputFile' '$newSubtitleFile'" } 'extract' { $ffmpegCommand += ($Video -or $vas) ? "ffmpeg -y -i '$InputFile' -map 0:v:0 -c copy '$(suffix $InputFile .ext)';" : "" $ffmpegCommand += ($Audio -or $vas) ? "ffmpeg -y -i '$InputFile' -map $AudioMap -c copy '$(ext $InputFile mka)';" : "" $ffmpegCommand += ($Sub -or $vas) ? "ffmpeg -y -i '$InputFile' -map $SubMap -c copy '$(ext $InputFile $SubFormat)'" : "" } 'copytranscode' { $audioIndex = ff countstreams -a $InputFile -NoPathFix if ($null -eq $audioIndex) { exit } $ffmpegCommand = "ffmpeg -i '$InputFile' -map 0 -c copy -map $AudioMap -c:a:$audioIndex libvorbis -metadata:s:a:$audioIndex title='Vorbis (Compatibility)' '$(suffix $InputFile _VORBIS)'" } 'countstreams' { $vCount = ($Video -or $vas) ? (Invoke-Expression "ffprobe -v error -select_streams v -show_entries stream=index -of json=c=1 '$InputFile' | ConvertFrom-Json | Select-Object -Expand streams | Measure-Object | %{`$_.Count}") : 0 $aCount = ($Audio -or $vas) ? (Invoke-Expression "ffprobe -v error -select_streams a -show_entries stream=index -of json=c=1 '$InputFile' | ConvertFrom-Json | Select-Object -Expand streams | Measure-Object | %{`$_.Count}") : 0 $sCount = ($Sub -or $vas) ? (Invoke-Expression "ffprobe -v error -select_streams s -show_entries stream=index -of json=c=1 '$InputFile' | ConvertFrom-Json | Select-Object -Expand streams | Measure-Object | %{`$_.Count}") : 0 $ffmpegCommand = "$vCount + $aCount + $sCount" } 'codecs' { $vCodecs = ($Video -or $vas) ? "Video:`t" + (Invoke-Expression "ffprobe -v error -select_streams v -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 '$InputFile'") + "`n" : "" $aCodecs = ($Audio -or $vas) ? "Audio:`t" + (Invoke-Expression "ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 '$InputFile'") + "`n" : "" $sCodecs = ($Sub -or $vas) ? "Subs:`t" + (Invoke-Expression "ffprobe -v error -select_streams s -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 '$InputFile'") + "`n" : "" $ffmpegCommand = "Write-Host -NoNewline '$vCodecs'; Write-Host -NoNewline '$aCodecs'; Write-Host -NoNewline '$sCodecs'" } 'audiomap' { $i = 0 $ffmpegCommand = "ffprobe -v error -select_streams a -show_entries stream=index,codec_name,bit_rate,channels:stream_tags=title,language -of json=c=1 '$InputFile' | ConvertFrom-Json | Select-Object -Expand streams | Add-Member -Pass 'file_name' '$InputFile' | ForEach-Object {Add-Member -Pass -InputObject `$_ 'audio_index' ('0:a:' + `$i++)}" # TODO (lfr -Filter '*.mkv' | ff audiomap)[0] should return all audio streams and not only one } 'map' { $vMap = ($Video -or $vas) ? "Video:`t" + (Invoke-Expression "ffprobe -v error -select_streams v -show_entries stream=index -of csv=p=0 '$InputFile'") + "`n" : "" $aMap = ($Audio -or $vas) ? "Audio:`t" + (Invoke-Expression "ffprobe -v error -select_streams a -show_entries stream=index -of csv=p=0 '$InputFile'") + "`n" : "" $sMap = ($Sub -or $vas) ? "Subs:`t" + (Invoke-Expression "ffprobe -v error -select_streams s -show_entries stream=index -of csv=p=0 '$InputFile'") + "`n" : "" $ffmpegCommand = "Write-Host -NoNewline '$vMap'; Write-Host -NoNewline '$aMap'; Write-Host -NoNewline '$sMap'" } 'merge' { $i = 0 (,$InputFile + $ExtraInputFiles) | ForEach-Object { $ffmpegCommand += " -i '$_'" } (,$InputFile + $ExtraInputFiles) | ForEach-Object { $ffmpegCommand += (" -map " + $i++) } $ffmpegCommand = "ffmpeg -y$ffmpegCommand -c copy '$(suffix $InputFile _merged)'" } default { Write-Host "Unknown command: $Command"; return } } if ($Preview) { Write-Host $ffmpegCommand } if (!$Preview -or (Read-Host "Execute? (Y/n)") -match '^[Yy]?$') { Invoke-Expression $ffmpegCommand } } } Set-Alias -Name ff -Value Invoke-FFTools |