Inital push
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user