Skip to content

Instantly share code, notes, and snippets.

@Venipa
Last active March 6, 2026 04:30
Show Gist options
  • Select an option

  • Save Venipa/b6a160fddac7fe8b07b72a7bb97ed9d7 to your computer and use it in GitHub Desktop.

Select an option

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
# 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
@Venipa
Copy link
Author

Venipa commented Mar 6, 2026

Combined 27 video files.
Example:
https://up.venipa.net/view/ragged-babyish-bat.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment