Files
Video-Processing-Scripts/Compare-Videos.ps1
T

612 lines
29 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; or 1216x1080 with -Clip)
- 3 videos: Side-by-side (608x1080 each, centered with padding to 1920x1080; or 1824x1080 with -Clip)
- 4 videos: Side-by-side (480x854 each, centered with padding to 1920x1080; or 1920x854 with -Clip)
Portrait mode (height > width, e.g., 1080x1920) - 4K UHD:
- 2 videos: Side-by-side (1216x2160 each, centered with padding to 3840x2160; or 2432x2160 with -Clip)
- 3 videos: Side-by-side (1216x2160 each, centered with padding to 3840x2160; or 3648x2160 with -Clip)
- 4 videos: Side-by-side (960x1708 each, centered with padding to 3840x2160; or 3840x1708 with -Clip)
With -Sideways, all videos are arranged in a single horizontal row. Adding -Clip removes
black padding: for landscape, cells are clipped to content height; for portrait, the
outer centering pad is omitted.
Sideways mode - Landscape (width > height) - 1K HD:
- 2 videos: Cell 960x1080, content 960x540; total 1920x1080 (or 1920x540 with -Clip)
- 3 videos: Cell 640x1080, content 640x360; total 1920x1080 (or 1920x360 with -Clip)
- 4 videos: Cell 480x1080, content 480x270; total 1920x1080 (or 1920x270 with -Clip)
Sideways mode - Landscape (width > height) - 4K UHD:
- 2 videos: Cell 1920x2160, content 1920x1080; total 3840x2160 (or 3840x1080 with -Clip)
- 3 videos: Cell 1280x2160, content 1280x720; total 3840x2160 (or 3840x720 with -Clip)
- 4 videos: Cell 960x2160, content 960x540; total 3840x2160 (or 3840x540 with -Clip)
Sideways mode - Portrait (height > width) - 1K HD:
- 2 videos: Cell 608x1080; output 1920x1080 (or 1216x1080 with -Clip)
- 3 videos: Cell 608x1080; output 1920x1080 (or 1824x1080 with -Clip)
- 4 videos: Cell 480x854; output 1920x1080 (or 1920x854 with -Clip)
Sideways mode - Portrait (height > width) - 4K UHD:
- 2 videos: Cell 1216x2160; output 3840x2160 (or 2432x2160 with -Clip)
- 3 videos: Cell 1216x2160; output 3840x2160 (or 3648x2160 with -Clip)
- 4 videos: Cell 960x1706; output 3840x2160 (or 3840x1706 with -Clip)
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).
.PARAMETER Clip
Reduces the output height to match the actual scaled height of the sources, removing
the top/bottom black letterbox padding. For landscape, only applies when exactly 2
source videos are provided; ignored otherwise. For portrait, applies when 2-4 source
videos are provided (all sources must be portrait); for 2-3 videos removes the outer
side padding, for 4 videos removes the outer top/bottom padding.
Behavior depends on the orientations of the two sources:
- Both same orientation (both landscape or both portrait): the output height is set
to the larger of the two sources' scaled heights inside their half-width cells.
Example: two 1280x720 sources -> 1920x540 (1K) or 3840x1080 (4K).
- Mixed orientations (one landscape, one portrait), and the landscape source is
smaller than the target resolution: the landscape video occupies its native pixel
width at 1K HD (or 2x in 4K), and the portrait video is scaled to match the
landscape's height and centered in the remaining width with black pillar boxes.
Example: 1280x720 landscape + 720x1280 portrait at 1K HD -> 1920x720 (landscape
occupies 1280x720 on its S1/S2 side, portrait fits in the remaining 640x720).
At 4K the same pair becomes 3840x1440 (landscape 2560x1440, portrait cell 1280x1440).
.PARAMETER Sideways
Arranges all videos in a single horizontal row. For landscape mode, replaces the default
2x2 grid layout (3 and 4 videos) with a single row. For portrait mode, the layout is
the same as the default (already side-by-side). Can be combined with -Clip to remove
black padding: for landscape, the output height equals the natural content height; for
portrait, the outer centering pad is omitted.
.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.
.EXAMPLE
.\Compare-Videos.ps1 -S1 a_720p.mp4 -S2 b_720p.mp4 -Output side-by-side.mp4 -Clip
Compare two 720p (1280x720) videos with no top/bottom letterboxing: output is 1920x540.
.EXAMPLE
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -Output sideways.mp4 -Sideways
Compare three landscape videos in a single row (three 640x1080 cells, total 1920x1080).
.EXAMPLE
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -S4 d.mp4 -Output sideways.mp4 -Sideways
Compare four landscape videos in a single row (four 480x1080 cells, total 1920x1080).
.EXAMPLE
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -Output sideways_clip.mp4 -Sideways -Clip
Three landscape videos in a single row, clipped to content height (three 640x360 cells, total 1920x360).
.EXAMPLE
.\Compare-Videos.ps1 -S1 portrait1.mp4 -S2 portrait2.mp4 -Output portrait_clip.mp4 -Clip
Two portrait videos without outer side padding: output is 1216x1080 (1K) or 2432x2160 (4K with -In4K).
.EXAMPLE
.\Compare-Videos.ps1 -S1 p1.mp4 -S2 p2.mp4 -S3 p3.mp4 -Output portrait3_clip.mp4 -Clip
Three portrait videos without outer side padding: output is 1824x1080 (1K). All sources must be portrait; if any is not, falls back to padded 1920x1080 layout.
.EXAMPLE
.\Compare-Videos.ps1 -S1 p1.mp4 -S2 p2.mp4 -S3 p3.mp4 -S4 p4.mp4 -Output portrait4_clip.mp4 -Clip
Four portrait videos without outer top/bottom padding: output is 1920x854 (1K). All sources must be portrait; if any is not, falls back to padded 1920x1080 layout.
#>
[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,
[switch]$Clip,
[switch]$Sideways
)
$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
}
if ($Clip -and -not $Sideways) {
$clipAllowed = ($isPortrait -and $videoCount -le 4) -or (-not $isPortrait -and $videoCount -eq 2)
if (-not $clipAllowed) {
Write-Warning "-Clip without -Sideways is only supported with 2 landscape videos, or 2-4 portrait videos; ignoring."
$Clip = $false
}
}
# --- Clip mode: shrink output height to the actual scaled height of the sources ---
$clipMixedLandscape = $false
$clipMixedFilterComplex = $null
if ($Clip -and -not $Sideways) {
Write-Host "Probing S2 dimensions for -Clip..." -ForegroundColor Cyan
$probeOutput2 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S2 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to probe video dimensions for S2: $probeOutput2"
exit 1
}
$dim2 = $probeOutput2 -split ','
$w2 = [int]$dim2[0]
$h2 = [int]$dim2[1]
$s1IsLandscape = $width -gt $height
$s2IsLandscape = $w2 -gt $h2
$isMixedOrientation = ($s1IsLandscape -xor $s2IsLandscape)
if ($isMixedOrientation) {
# One source is landscape, the other is portrait. Try the mixed-orientation
# layout: the landscape video keeps its native pixel size at 1K HD (or 2x in 4K),
# and the portrait video is scaled to match the landscape's height and centered
# in the remaining width with black pillar-box bars.
$scaleFactor = $outputWidth / 1920.0 # 1.0 at 1K HD, 2.0 at 4K UHD
if ($s1IsLandscape) {
$landscapeW = $width; $landscapeH = $height
} else {
$landscapeW = $w2; $landscapeH = $h2
}
$landscapeOutW = [int][Math]::Floor($landscapeW * $scaleFactor)
$landscapeOutH = [int][Math]::Floor($landscapeH * $scaleFactor)
if ($landscapeOutW % 2) { $landscapeOutW++ }
if ($landscapeOutH % 2) { $landscapeOutH++ }
if ($landscapeOutW -lt $outputWidth -and $landscapeOutH -le $outputHeight) {
$portraitCellW = $outputWidth - $landscapeOutW
$outputHeight = $landscapeOutH
Write-Host ("Clip mode (mixed orientation): landscape {0}x{1} + portrait {2}x{1} -> {3}x{1}" -f $landscapeOutW, $landscapeOutH, $portraitCellW, $outputWidth) -ForegroundColor Yellow
# Preserve S1=left, S2=right ordering. Each input is scaled into its cell
# with aspect-ratio preserved, then padded with black to exactly fill the cell.
if ($s1IsLandscape) {
$leftW = $landscapeOutW; $rightW = $portraitCellW
} else {
$leftW = $portraitCellW; $rightW = $landscapeOutW
}
$clipMixedFilterComplex = "[0:v]scale=${leftW}:${landscapeOutH}:force_original_aspect_ratio=decrease,pad=${leftW}:${landscapeOutH}:(ow-iw)/2:(oh-ih)/2:black[v0];" +
"[1:v]scale=${rightW}:${landscapeOutH}:force_original_aspect_ratio=decrease,pad=${rightW}:${landscapeOutH}:(ow-iw)/2:(oh-ih)/2:black[v1];" +
"[v0][v1]hstack=inputs=2[outv]"
$clipMixedLandscape = $true
} else {
Write-Host "Clip mode: mixed orientations detected, but the landscape source already fills the target width; using standard clip layout." -ForegroundColor Yellow
}
}
if (-not $clipMixedLandscape) {
# Standard -Clip behaviour: both sources share an orientation (or mixed fell back).
# Compute the largest natural scaled height across both sources and reduce the
# output height to that value.
# Per-source cell width matches the 2-video branches below.
if ($isPortrait) {
$clipCellWidth = [int]($outputWidth * 0.6333 / 2)
} else {
$clipCellWidth = [int]($outputWidth / 2)
}
# Height each source would naturally scale to inside its cell when fitted by width
# (force_original_aspect_ratio=decrease). Take the larger of the two so neither
# source loses content; cap at the original output height.
$h1Scaled = [int][Math]::Floor($clipCellWidth * $height / $width)
$h2Scaled = [int][Math]::Floor($clipCellWidth * $h2 / $w2)
$clippedHeight = [Math]::Max($h1Scaled, $h2Scaled)
if ($clippedHeight % 2) { $clippedHeight++ } # libx264/yuv420p requires even dimensions
# 320p and 384p sources have non-standard widths that cause off-by-one rounding
# below a true 16:9 height. Force the exact 16:9 cell height for these sources.
if (-not $isPortrait -and ($height -eq 320 -or $height -eq 384 -or $h2 -eq 320 -or $h2 -eq 384)) {
$forced16x9Height = [int]($clipCellWidth * 9 / 16)
if ($forced16x9Height % 2) { $forced16x9Height++ }
$clippedHeight = $forced16x9Height
}
if ($clippedHeight -gt $outputHeight) { $clippedHeight = $outputHeight }
if ($clippedHeight -lt $outputHeight) {
Write-Host ("Clip mode: output height reduced from {0} to {1} ({2}x{1})" -f $outputHeight, $clippedHeight, $outputWidth) -ForegroundColor Yellow
$outputHeight = $clippedHeight
} else {
Write-Host "Clip mode: sources already fill the full output height; no clipping applied." -ForegroundColor Yellow
}
}
}
$filterComplex = ""
$inputArgs = @("-i", $S1, "-i", $S2)
if ($Sideways) {
Write-Host "Sideways mode: arranging $videoCount videos in a single row." -ForegroundColor Cyan
if ($isPortrait) {
if ($videoCount -le 3) {
$swCellW = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K
$swCellH = $outputHeight
} else {
$swCellW = [int]($outputWidth / 4) # 480 for 1K, 960 for 4K
$swCellH = [int]($outputHeight * 0.79) # ~854 for 1K, ~1706 for 4K
if ($swCellH % 2) { $swCellH++ }
}
} else {
$swCellW = [int]($outputWidth / $videoCount) # 960/640/480 for 1K, 1920/1280/960 for 4K
$swCellH = $outputHeight
}
if ($Clip -and -not $isPortrait) {
$swCellH = [int]($swCellW * $height / $width)
if ($swCellH % 2) { $swCellH++ }
# 320p and 384p sources have non-standard widths that cause off-by-one rounding
# below a true 16:9 height. Force the exact 16:9 cell height for these sources.
if ($height -eq 320 -or $height -eq 384 -or $h2 -eq 320 -or $h2 -eq 384) {
$forced16x9Height = [int]($swCellW * 9 / 16)
if ($forced16x9Height % 2) { $forced16x9Height++ }
$swCellH = $forced16x9Height
}
if ($swCellH -gt $outputHeight) { $swCellH = $outputHeight }
Write-Host ("Sideways clip: cell height reduced to {0} ({1}x{0} per cell)" -f $swCellH, $swCellW) -ForegroundColor Yellow
}
$swFilter = "scale=${swCellW}:${swCellH}:force_original_aspect_ratio=decrease,pad=${swCellW}:${swCellH}:(ow-iw)/2:(oh-ih)/2:black"
if ($Clip) {
$swHstackEnd = "[outv]"
} else {
$swHstackEnd = "[stacked];[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]"
}
switch ($videoCount) {
2 {
$filterComplex = "[0:v]${swFilter}[v0];" +
"[1:v]${swFilter}[v1];" +
"[v0][v1]hstack=inputs=2${swHstackEnd}"
}
3 {
$inputArgs += @("-i", $S3)
$filterComplex = "[0:v]${swFilter}[v0];" +
"[1:v]${swFilter}[v1];" +
"[2:v]${swFilter}[v2];" +
"[v0][v1][v2]hstack=inputs=3${swHstackEnd}"
}
4 {
$inputArgs += @("-i", $S3, "-i", $S4)
$filterComplex = "[0:v]${swFilter}[v0];" +
"[1:v]${swFilter}[v1];" +
"[2:v]${swFilter}[v2];" +
"[3:v]${swFilter}[v3];" +
"[v0][v1][v2][v3]hstack=inputs=4${swHstackEnd}"
}
}
} elseif ($clipMixedLandscape) {
# Mixed-orientation -Clip layout was prepared above; use it as-is.
$filterComplex = $clipMixedFilterComplex
} elseif ($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][Math]::Ceiling($cellWidth4 * 16.0 / 9.0) # 9:16 ratio: 854 for 1K, 1708 for 4K
if ($cellHeight4 % 2) { $cellHeight4++ }
switch ($videoCount) {
2 {
# 2 videos side-by-side, centered (or unpadded with -Clip)
if ($Clip) {
$clippedWidth = $cellWidth2 * 2
Write-Host ("Clip mode (portrait): output width reduced from {0} to {1} ({1}x{2})" -f $outputWidth, $clippedWidth, $outputHeight) -ForegroundColor Yellow
$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]"
} else {
$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 (or unpadded with -Clip if all sources are portrait)
$inputArgs += @("-i", $S3)
$applyPortraitClip = $false
if ($Clip) {
$probeOutput3 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S3 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to probe video dimensions for S3: $probeOutput3"
exit 1
}
$dim3 = $probeOutput3 -split ','
$w3 = [int]$dim3[0]; $h3 = [int]$dim3[1]
if (($h2 -gt $w2) -and ($h3 -gt $w3)) {
$applyPortraitClip = $true
} else {
Write-Warning "-Clip (portrait 3-video): not all sources are portrait; falling back to padded layout."
}
}
if ($applyPortraitClip) {
$clippedWidth = $cellWidth3 * 3
Write-Host ("Clip mode (portrait): output width reduced from {0} to {1} ({1}x{2})" -f $outputWidth, $clippedWidth, $outputHeight) -ForegroundColor Yellow
$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[outv]"
} else {
$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 (or unpadded with -Clip if all sources are portrait)
$inputArgs += @("-i", $S3, "-i", $S4)
$applyPortraitClip4 = $false
if ($Clip) {
$probeOutput3 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S3 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to probe video dimensions for S3: $probeOutput3"
exit 1
}
$dim3 = $probeOutput3 -split ','
$w3 = [int]$dim3[0]; $h3 = [int]$dim3[1]
$probeOutput4 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S4 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to probe video dimensions for S4: $probeOutput4"
exit 1
}
$dim4 = $probeOutput4 -split ','
$w4 = [int]$dim4[0]; $h4 = [int]$dim4[1]
if (($h2 -gt $w2) -and ($h3 -gt $w3) -and ($h4 -gt $w4)) {
$applyPortraitClip4 = $true
} else {
Write-Warning "-Clip (portrait 4-video): not all sources are portrait; falling back to padded layout."
}
}
if ($applyPortraitClip4) {
Write-Host ("Clip mode (portrait): output height reduced from {0} to {1} ({2}x{1})" -f $outputHeight, $cellHeight4, $outputWidth) -ForegroundColor Yellow
$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[outv]"
} else {
$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", "slow",
"-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
}
}