<# .SYNOPSIS Creates side-by-side or grid comparison videos with automatic scaling and padding to preserve aspect ratios. .DESCRIPTION Combines 2 to 4 video files into a single comparison video using ffmpeg. By default, creates 1920x1080 (1K HD) output. Use -In4K for 3840x2160 (4K UHD) output. Each source video is scaled to fit its cell without stretching, with black letterbox/pillarbox padding applied as needed to preserve aspect ratios. The script detects the orientation of S1 (landscape or portrait) and applies appropriate scaling: Landscape mode (width > height) - 1K HD: - 2 videos: Side-by-side (960x1080 each) - 3 videos: 2x2 grid with S3 centered on bottom row (960x540 each) - 4 videos: 2x2 grid (960x540 each) Landscape mode (width > height) - 4K UHD: - 2 videos: Side-by-side (1920x2160 each) - 3 videos: 2x2 grid with S3 centered on bottom row (1920x1080 each) - 4 videos: 2x2 grid (1920x1080 each) Portrait mode (height > width, e.g., 1080x1920) - 1K HD: - 2 videos: Side-by-side (608x1080 each, centered with padding to 1920x1080) - 3 videos: Side-by-side (608x1080 each, centered with padding to 1920x1080) - 4 videos: Side-by-side (480x854 each, centered with padding to 1920x1080) Portrait mode (height > width, e.g., 1080x1920) - 4K UHD: - 2 videos: Side-by-side (1216x2160 each, centered with padding to 3840x2160) - 3 videos: Side-by-side (1216x2160 each, centered with padding to 3840x2160) - 4 videos: Side-by-side (960x1708 each, centered with padding to 3840x2160) Requires ffmpeg and ffprobe in system PATH. Supports -WhatIf for dry-run testing. .PARAMETER S1 Path to source video 1 (required). .PARAMETER S2 Path to source video 2 (required). .PARAMETER S3 Path to source video 3 (optional). .PARAMETER S4 Path to source video 4 (optional). .PARAMETER Output Path to the output video file (required). .PARAMETER NoAudio Exclude audio from output. By default, audio is copied from S1. .PARAMETER In4K Generate comparison video in 4K UHD (3840x2160) resolution instead of 1K HD (1920x1080). .EXAMPLE .\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison.mp4 Compare two videos side-by-side. .EXAMPLE .\Compare-Videos.ps1 -S1 version1.mp4 -S2 version2.mp4 -S3 version3.mp4 -Output comparison.mp4 Compare three videos. .EXAMPLE .\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -S4 d.mp4 -Output grid.mp4 Compare four videos in a 2x2 grid. .EXAMPLE .\Compare-Videos.ps1 -S1 video1.mp4 -S2 video2.mp4 -Output comparison.mp4 -NoAudio Compare videos without including audio in the output. .EXAMPLE .\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison_4k.mp4 -In4K Compare two videos in 4K UHD (3840x2160) resolution. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [string]$S1, [Parameter(Mandatory = $true)] [string]$S2, [Parameter(Mandatory = $false)] [string]$S3, [Parameter(Mandatory = $false)] [string]$S4, [Parameter(Mandatory = $true)] [string]$Output, [switch]$NoAudio, [switch]$In4K ) $ErrorActionPreference = "Stop" function Test-VideoExists { param([string]$Path, [string]$Name) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { Write-Error "Source video $Name does not exist: $Path" exit 1 } } Test-VideoExists -Path $S1 -Name "S1" Test-VideoExists -Path $S2 -Name "S2" if ($S3) { Test-VideoExists -Path $S3 -Name "S3" } if ($S4) { Test-VideoExists -Path $S4 -Name "S4" } $outputDir = Split-Path -Path $Output -Parent if ($outputDir -and -not (Test-Path -LiteralPath $outputDir -PathType Container)) { Write-Error "Output directory does not exist: $outputDir" exit 1 } $videoCount = 2 if ($S4) { $videoCount = 4 } elseif ($S3) { $videoCount = 3 } # --- Detect orientation of S1 --- Write-Host "Detecting orientation of S1..." -ForegroundColor Cyan $probeOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S1 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error "Failed to probe video dimensions for S1: $probeOutput" exit 1 } $dimensions = $probeOutput -split ',' $width = [int]$dimensions[0] $height = [int]$dimensions[1] $isPortrait = $height -gt $width # Set resolution based on -In4K switch if ($In4K) { $outputWidth = 3840 $outputHeight = 2160 $resolutionLabel = "4K UHD" } else { $outputWidth = 1920 $outputHeight = 1080 $resolutionLabel = "1K HD" } if ($isPortrait) { Write-Host "Portrait mode detected (${width}x${height}) - Output: $resolutionLabel" -ForegroundColor Green } else { Write-Host "Landscape mode detected (${width}x${height}) - Output: $resolutionLabel" -ForegroundColor Green } $filterComplex = "" $inputArgs = @("-i", $S1, "-i", $S2) if ($isPortrait) { # Portrait mode scaling - calculate cell dimensions based on output resolution $cellWidth2 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K $cellWidth3 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K $cellWidth4 = [int]($outputWidth / 4) # 480 for 1K, 960 for 4K $cellHeight4 = [int]($outputHeight * 0.79) # ~854 for 1K, ~1708 for 4K switch ($videoCount) { 2 { # 2 videos side-by-side, centered $filterComplex = "[0:v]scale=${cellWidth2}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth2}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v0];" + "[1:v]scale=${cellWidth2}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth2}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v1];" + "[v0][v1]hstack=inputs=2[stacked];" + "[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]" } 3 { # 3 videos side-by-side, centered $inputArgs += @("-i", $S3) $filterComplex = "[0:v]scale=${cellWidth3}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth3}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v0];" + "[1:v]scale=${cellWidth3}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth3}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v1];" + "[2:v]scale=${cellWidth3}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth3}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v2];" + "[v0][v1][v2]hstack=inputs=3[stacked];" + "[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]" } 4 { # 4 videos side-by-side, centered $inputArgs += @("-i", $S3, "-i", $S4) $filterComplex = "[0:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v0];" + "[1:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v1];" + "[2:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v2];" + "[3:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v3];" + "[v0][v1][v2][v3]hstack=inputs=4[stacked];" + "[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]" } } } else { # Landscape mode scaling - calculate cell dimensions based on output resolution $cellWidth2 = [int]($outputWidth / 2) # 960 for 1K, 1920 for 4K $cellWidth4 = [int]($outputWidth / 2) # 960 for 1K, 1920 for 4K $cellHeight4 = [int]($outputHeight / 2) # 540 for 1K, 1080 for 4K $padX = [int]($outputWidth / 4) # 480 for 1K, 960 for 4K switch ($videoCount) { 2 { # 2 videos side-by-side $filterComplex = "[0:v]scale=${cellWidth2}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth2}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v0];" + "[1:v]scale=${cellWidth2}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth2}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v1];" + "[v0][v1]hstack=inputs=2[outv]" } 3 { # 2x2 grid with S3 centered on bottom row $inputArgs += @("-i", $S3) $filterComplex = "[0:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v0];" + "[1:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v1];" + "[2:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v2];" + "[v0][v1]hstack=inputs=2[top];" + "[v2]pad=${outputWidth}:${cellHeight4}:${padX}:0:black[bottom];" + "[top][bottom]vstack=inputs=2[outv]" } 4 { # 2x2 grid $inputArgs += @("-i", $S3, "-i", $S4) $filterComplex = "[0:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v0];" + "[1:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v1];" + "[2:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v2];" + "[3:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v3];" + "[v0][v1]hstack=inputs=2[top];" + "[v2][v3]hstack=inputs=2[bottom];" + "[top][bottom]vstack=inputs=2[outv]" } } } $ffmpegArgs = $inputArgs + @( "-filter_complex", $filterComplex, "-map", "[outv]" ) if (-not $NoAudio) { $ffmpegArgs += @("-map", "0:a?") } $ffmpegArgs += @( "-c:v", "libx264", "-preset", "fast", "-crf", "8", "-pix_fmt", "yuv420p" ) if (-not $NoAudio) { $ffmpegArgs += @("-c:a", "copy") } $ffmpegArgs += @("-y", $Output) $commandLine = "ffmpeg " + ($ffmpegArgs | ForEach-Object { if ($_ -match '\s') { "`"$_`"" } else { $_ } }) -join " " Write-Host "Executing ffmpeg command:" -ForegroundColor Cyan Write-Host $commandLine -ForegroundColor Yellow Write-Host "" if ($PSCmdlet.ShouldProcess($Output, "Create comparison video")) { & ffmpeg @ffmpegArgs if ($LASTEXITCODE -eq 0) { Write-Host "`nComparison video created successfully: $Output" -ForegroundColor Green } else { Write-Error "ffmpeg failed with exit code $LASTEXITCODE" exit $LASTEXITCODE } }