Join-Media.ps1
function Join-Media { <# .Synopsis Joins media files .Description Joins multiple media files together into a single file. .Link Get-Media .Link Convert-Media .Notes Join-Media has a variety of uses: * Creating a Time Lapse or Stop Motion from a series of images * Mixing audio into an existing video file * Joining multiple videos or audio files into a single long file #> [CmdletBinding(DefaultParameterSetName='Auto')] [OutputType([IO.FileInfo])] param( # The input path [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName,ValueFromPipeline)] [Alias('Fullname')] [string[]] $InputPath, # The output path [Parameter(Mandatory,Position=1)] [string] $OutputPath, # If set, will transcode input files before concatinating them together. [switch] $Transcode, # If inputs are mixed together, instead of concatenated, then the shortest input file will be preferred [Switch] $Shortest, # The frame rate. If joining images, this determines how long each image takes. [Alias('FPS','FramesPerSecond')] [uint32] $FrameRate = 30, # If set, will generate a time lapse. # This will assume all inputs are images (skipping individual analysis) [Parameter(Mandatory=$true,ParameterSetName='TimeLapse')] [Alias('StopMotion','IsStopMotion','IsTimeLapse')] [Switch] $TimeLapse, # The pixel format for video and image output. This maps to the -pix_fmt parameter in ffmpeg. By default, yuv420p. [Parameter(ValueFromPipelineByPropertyName=$true)] [Alias('pix_fmt')] [string] $PixelFormat = 'yuv420p' ) begin { $inputPaths = @() $inputList = @() $inputMedia = @{} $ffMpegConvertStart = { $progSplat= @{Activity='Encoding'}} $ffmpegConvertProcess = { if ($_ -like "*time=*" -and $_ -like "*bitrate=*") { Write-Verbose "$_" $lineChunks = $_.Tostring() -split "[ =]" -ne '' | Where-Object { $_.Trim() } $lineData = New-Object PSObject for ($i =0; $i -lt $lineChunks.Count; $i+=2) { $lineData |Add-Member NoteProperty $lineChunks[$i].TrimEnd("=") $lineChunks[$i + 1] -Force } $time = $lineData.Time -as [Timespan] if ($theDuration) { $progSplat.PercentComplete = $time.TotalMilliseconds * 100 / $theDuration.TotalMilliseconds } else { $progSplat.Remove('PercentComplete') } Write-Progress @progSplat "$lineData".TrimStart("@{").TrimEnd("}") -Id $id } else { Write-Verbose "$_" } } $ffMpegConvertEnd = { Write-Progress @progsplat -Status ' ' -Completed -Id $id } } process { #region Find FFMpeg $ffMpeg = Get-FFMpeg -ffMpegPath $ffmpegPath #endregion Find FFMpeg $inputPaths+=$InputPath } end { $id = Get-Random $t = $inputPaths.Count $c = 0 foreach ($i in $InputPaths) { $c++ $p = $c * 100 / $t $skipAnalysis = $TimeLapse $fileStr = "$($ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($i))" $inputList += "$fileStr" if (-not $skipAnalysis ) { Write-Progress "Analyzing Files" "$i" -PercentComplete $p -Id $id $mediaInfo = Get-Media -InputPath $i $inputMedia["$fileStr"] = $mediaInfo } } $videoFiles = @($inputMedia.Values| Where-Object { $_.Resolution -and $_.Duration -and $_.Duration -gt '00:00:00.5'}) $audioFiles = @($inputMedia.Values| Where-Object { $_.CodecTypes -and @($_.CodecTypes)[0] -eq 'Audio' }) $imageFiles = @($inputMedia.Values| Where-Object { $_.Resolution -and (-not $_.Duration -or ($_.Duration -lt '00:00:00.5')) }) $isAllVideo = $videoFiles.Count -eq $inputMedia.Count -and $inputMedia.Count -gt 0 $isAllAudio = $audioFiles.Count -eq $inputMedia.Count -and $inputMedia.Count -gt 0 $isAllImages = $imageFiles.Count -eq $inputMedia.Count -and $inputMedia.Count -gt 0 if ($isAllVideo -or $isAllAudio) { if (@($inputMedia.Values | Select-Object -ExpandProperty codecs -Unique).Count -gt 1) { $Transcode = $true } $tempFiles = foreach ($in in $inputList) { if ($Transcode) { if ($isAllVideo) { $tempFile = [IO.Path]::GetTempPath() + "$(Get-Random).mp4" & $ffmpeg -i $in "-qscale:v" 1 $tempFile -y 2>&1 | ForEach-Object -Begin $ffMpegConvertStart $ffmpegConvertProcess -End $ffmpegConvertEnd } else { $tempFile = [IO.Path]::GetTempPath() + "$(Get-Random).mp3" & $ffmpeg -i $in $tempFile -y 2>&1 | ForEach-Object -Begin $ffMpegConvertStart $ffmpegConvertProcess -End $ffmpegConvertEnd } $tempFile } else { "$in" } } $ffmpegParams = @( if (-not $Transcode) { $tempFilesroot = $tempfiles | Split-Path | Select-Object -Unique [IO.Directory]::SetCurrentDirectory($tempFilesRoot) $tmpFile = Join-Path $tempfilesRoot "fileList.txt" $tmpfileContent = @($tempFiles | Foreach-Object { "file '$_'" }) $tmpfileContent | Set-Content -Path $tmpFile "-f", "concat", "-safe", "0", "-i", $tmpFile, "-c", "copy" } else { "-i", "concat:$($tempFiles -join '|')" } ) $uro = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($outputPath) & $ffmpeg @ffMpegParams $uro -y 2>&1 | ForEach-Object -Begin $ffMpegConvertStart $ffmpegConvertProcess -End $ffmpegConvertEnd Get-Item -Path $uro -ErrorAction SilentlyContinue if ($Transcode) { foreach ($tmp in $tempFiles) { Remove-Item -LiteralPath $tmp -Force } } } elseif ($inputList.Count -eq 2 -and $videoFiles -and $audioFiles) { # In this case, we're mixing an audio and a video input $ffMpegParams = @() $c = 0 $longestDuration = 0 $shortestDuration = 0 foreach ($i in $inputList) { $ffMpegParams += '-i' $ffMpegParams += "$i" if ($inputMedia[$i].Duration -and $inputMedia[$i].Resolution) { $videoStreamNumber = $c } else { $audioStreamNumber = $c } if ($inputMedia[$i].Duration.TotalMilliseconds -gt $longestDuration) { $longestDuration = $inputMedia[$i].Duration.TotalMilliseconds } if ($inputMedia[$i].Duration.TotalMilliseconds -lt $shortestDuration -or $shortestDuration -eq 0) { $shortestDuration= $inputMedia[$i].Duration.TotalMilliseconds } $C++ } $ffMpegParams += '-map' $ffMpegParams += "${VideoStreamNumber}:v" $ffMpegParams += '-map' $ffMpegParams += "${AudioStreamNumber}:a" if ($AudioCodec -or $VideoCodec) { if ($AudioCodec) { $ffMpegParams += '-c:a' $ffMpegParams += $AudioCodec } else { $ffMpegParams += '-c:a' $ffMpegParams += 'copy' } if ($VideoCodec) { $ffMpegParams += '-c:v' $ffMpegParams += $VideoCodec } else { $ffMpegParams += '-c:v' $ffMpegParams += 'copy' } } else { $ffMpegParams += '-codec' $ffMpegParams += 'copy' } $theDuration = if ($Shortest) { $ffMpegParams += '-shortest' [Timespan]::FromMilliseconds($shortestDuration) } else { [Timespan]::FromMilliseconds($longestDuration) } $uro = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($outputPath) $ffMpegParams += $uro $ffMpegParams += '-y' & $ffmpeg @ffmpegParams 2>&1 | ForEach-Object -Begin $ffMpegConvertStart $ffmpegConvertProcess -End $ffmpegConvertEnd Get-Item -Path $uro -ErrorAction SilentlyContinue } elseif ($isAllImages -or $TimeLapse) { $uro = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($outputPath) $tmpDir = [io.pATH]::GetTempPath() $tempRoot = New-Item -ItemType Directory -Path (Join-Path $tmpDir $(Get-Random)) $script:c = 0 $extension = $inputList | Select-Object -First 1 | Get-Item | Select-Object -ExpandProperty Extension $inputList| Copy-Item -Destination { $script:C++ Join-Path $tempRoot ("image-{0}.$extension" -f $script:c) } -PassThru | ForEach-Object { Write-Progress "Copying Files" "$script:c of $($inputList.Count)" -PercentComplete ($script:c * 100 / $inputList.Length) -Id $id } $theDuration = [TimeSpan]::FromSeconds($inputList.Count / $frameRate) # $tBlend = @('-filter:v', 'tblend') & $FFMpeg -framerate $FrameRate -i "$temproot$([IO.Path]::DirectorySeparatorChar)image-%d.$extension" -pix_fmt $PixelFormat -y $uro 2>&1 | ForEach-Object -Begin $ffMpegConvertStart $ffmpegConvertProcess -End $ffmpegConvertEnd Get-Item -Path $uro -ErrorAction SilentlyContinue Remove-Item -Path $tempRoot -Force -ErrorAction SilentlyContinue -Recurse } } } |