Last active
March 6, 2026 04:30
-
-
Save Venipa/b6a160fddac7fe8b07b72a7bb97ed9d7 to your computer and use it in GitHub Desktop.
shell-x script for combining video files and converting them into HEVC format with GPU Encoder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Combine (hevc, same bitrate unless picked by user).c.ps1 | |
| # This script combines multiple videos into a single hevc video with the same bitrate and quality. | |
| # Author: Venipa <admin@venipa.net> | |
| # License: MIT | |
| # Version: 1.1.0 | |
| # - Updated missing audio fallback with silence, fixed duration of the silence triggering X buffers queued in out_X_1 | |
| # - ffprobe per file for audio and duration check | |
| # - size down subsequent video aspect ratios to the first sequence | |
| # Version: 1.0.0 | |
| # Date: 2026-03-06 | |
| # Usage: Combine (hevc, same bitrate).c.ps1 <input files> | |
| function ParseFileArguments { | |
| param ( | |
| [string]$inputString | |
| ) | |
| # Pattern matches A:\, C:\, etc., and captures until next <drive>:\ or end of string. | |
| $regex = '([a-zA-Z]:\\.*?)(?=\s+[a-zA-Z]:\\|$)' | |
| $regexMatches = [regex]::Matches($inputString, $regex) | |
| $fileList = @() | |
| foreach ($match in $regexMatches) { | |
| $escapedValue = $match.Groups[1].Value.Trim() -replace '\[', '``[' -replace '\]', '``]' | |
| $fileList += $escapedValue | |
| } | |
| return $fileList | |
| } | |
| Write-Debug "Arguments:`n$args`n`n" | |
| function getInputResolution { | |
| param ( | |
| [string]$inputPath | |
| ) | |
| $ffprobePath = "$env:ChocolateyInstall\bin\ffprobe.exe" | |
| if (-not (Test-Path $ffprobePath)) { | |
| $ffprobePath = "ffprobe" | |
| } | |
| $ffprobeCmd = @( | |
| "-v", "error" | |
| "-select_streams", "v:0" | |
| "-show_entries", "stream=width,height" | |
| "-of", "csv=s=x:p=0" | |
| "`"$inputPath`"" | |
| ) | |
| $ffprobeOutput = & $ffprobePath @ffprobeCmd | |
| if ($LASTEXITCODE -eq 0 -and $ffprobeOutput -match "^(\d+)x(\d+)$") { | |
| $width = $matches[1] | |
| $height = $matches[2] | |
| return $width, $height | |
| } | |
| return $null, $null | |
| } | |
| $args = ParseFileArguments($args -join ' ') | sort-object | |
| if ($args.Count -eq 0 -or $args.Count -eq 1) { | |
| Write-Host "Please provide at least 2 input files!"; | |
| Pause | |
| exit 1 | |
| } | |
| $defaultQuality = 30 | |
| $maxQuality = 51 | |
| $minQuality = 10 | |
| $defaultWidth = 1920 | |
| $defaultHeight = 1080 | |
| # Prompt for quality | |
| $quality = Read-Host "Quality ($minQuality-$maxQuality, default $defaultQuality)" | |
| if ([string]::IsNullOrWhiteSpace($quality)) { | |
| $quality = $defaultQuality | |
| } | |
| # Make sure quality is an integer between 10 and 51 | |
| try { | |
| $quality = [int]$quality | |
| } | |
| catch { | |
| $quality = $defaultQuality | |
| } | |
| if ($quality -lt $minQuality) { $quality = $minQuality } | |
| if ($quality -gt $maxQuality) { $quality = $maxQuality } | |
| $inputs = @() | |
| foreach ($filepath in $args) { | |
| if (Test-Path $filepath) { | |
| $file = Get-Item $filepath | |
| $inputs += $file | |
| } | |
| } | |
| if ($inputs.Count -eq 0) { | |
| Write-Host "No input files provided!"; Pause; exit 1 | |
| } | |
| Write-Host "Converting to HEVC with quality CRF $quality and resolution $width x $height" | |
| $cInputs = @() | |
| $cFilters = @() | |
| $cInputCount = 0 | |
| $firstInput = $inputs[0] | |
| $width, $height = getInputResolution($firstInput.FullName) | |
| if ($null -eq $width -or $null -eq $height) { | |
| Write-Host "Could not determine input resolution, using defaults." | |
| $width = $defaultWidth | |
| $height = $defaultHeight | |
| } | |
| else { | |
| $width = [int]$width | |
| $height = [int]$height | |
| } | |
| foreach ($inputPath in $inputs) { | |
| $escapedInputPath = [System.IO.Path]::GetFullPath($inputPath.FullName) | |
| $probe = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_type:format=duration -of default=noprint_wrappers=1 "`"$escapedInputPath`"" | |
| $hasAudio = $probe -match "audio" | |
| $duration = [float]($probe | Select-String "duration=(\d+\.?\d*)").Matches.Groups[1].Value | |
| $cInputs += "-hwaccel", "cuda", "-i", "`"$escapedInputPath`"" | |
| $cFilters += "[$($cInputCount):v]format=yuv420p,scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1[v$($cInputCount)];" | |
| if ($hasAudio -match "audio") { | |
| $cFilters += "[$($cInputCount):a]atrim=duration=${duration},asetpts=PTS-STARTPTS[a$($cInputCount)];" | |
| } | |
| else { | |
| $cFilters += "anullsrc=channel_layout=stereo:sample_rate=48000,atrim=duration=${duration},asetpts=PTS-STARTPTS[a$($cInputCount)];" | |
| } | |
| $cInputCount++ | |
| } | |
| $cFormatCount = 0 | |
| foreach ($inputPath in $inputs) { | |
| $cFormats += "`[v$($cFormatCount)`]`[a$($cFormatCount)`]" | |
| $cFormatCount++ | |
| } | |
| $cFormatsJoined = ([string]::Join("", $cFormats)) | |
| # Output filename (based on first input) | |
| $firstInput = $inputs[0] | |
| $firstInputBase = [System.IO.Path]::Combine( | |
| [System.IO.Path]::GetDirectoryName($firstInput), | |
| ([System.IO.Path]::GetFileNameWithoutExtension($firstInput) + "-combined-hevc.mp4") | |
| ) | |
| $oFilename = $firstInputBase | |
| # Compose filter_complex | |
| $cFiltersJoined = ([string]::Join("", $cFilters)) + $cFormatsJoined + "concat=n=$($cInputCount):v=1:a=1[v][a]" | |
| Write-Host "`n`nConverting $cInputCount inputs to HEVC with quality CRF $quality" | |
| Write-Host "Inputs: $([string]::Join(" ", $cInputs))" | |
| Write-Host "Filters: $([string]::Join(" ", $cFiltersJoined))" | |
| Write-Host "Output: $oFilename`n`n" | |
| # Find ffmpeg | |
| $ffmpegPath = "$env:ChocolateyInstall\bin\ffmpeg.exe" | |
| if (-not (Test-Path $ffmpegPath)) { | |
| $ffmpegPath = "ffmpeg" | |
| } | |
| # Construct argument list | |
| $ffmpegArgs = @( | |
| "-hide_banner", | |
| "-y" | |
| ) | |
| foreach ($ffmpegInput in $cInputs) { | |
| $ffmpegArgs += $ffmpegInput | |
| } | |
| $ffmpegArgs += @( | |
| "-filter_complex", "`"$($cFiltersJoined)`"" | |
| "-map", "`"[v]`"" | |
| "-map", "`"[a]`"" | |
| "-noautoscale", | |
| "-c:v", "hevc_nvenc" | |
| "-preset:v", "p7" | |
| "-tune:v", "hq" | |
| "-rc:v", "vbr" | |
| "-cq:v", "$quality" | |
| "-b:v", "0" | |
| "-maxrate:v", "10M" | |
| "-b:a", "128k" | |
| "-ac", "2" | |
| "`"$oFilename`"" | |
| ) | |
| try { | |
| # write-Host "FFmpeg Command:`n $ffmpegPath $([string]::Join(" ", $ffmpegArgs))" | |
| # Run ffmpeg | |
| Invoke-Expression "$ffmpegPath $([string]::Join(" ", $ffmpegArgs))" | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "Error occurred during ffmpeg execution." | |
| pause | |
| exit 1 | |
| } | |
| } | |
| catch { | |
| Write-Error "Error occurred during ffmpeg execution: `n`n$_`n`n" | |
| pause | |
| exit 1 | |
| } | |
| Write-Host "Conversion completed successfully." | |
| Pause | |
| exit 0 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Combined 27 video files.
Example:
https://up.venipa.net/view/ragged-babyish-bat.mp4