Files
Video-Processing-Scripts/Compare-Videos.ps1
T
2026-05-19 14:55:09 -04:00

288 lines
11 KiB
PowerShell

<#
.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
}
}