Add 320p, 384p, and 900p size presets to Clone-Video.ps1 with aspect-ratio-preserving scaling logic, add -Sideways parameter to Compare-Videos.ps1 for single-row layouts with optional -Clip support, extend -Clip to portrait 2-4 video layouts, and add corresponding crop filters to Crop-Clips.ps1

This commit is contained in:
2026-06-15 20:25:57 -04:00
parent 8addeb0db1
commit 84811de486
3 changed files with 278 additions and 37 deletions
+23 -2
View File
@@ -22,7 +22,11 @@
Path to PNG file for extracted frame. Can be absolute or relative to current directory. .png extension is added if not provided. If EndFrame is -1, extracts the last frame and excludes it from the cloned video. If EndFrame is not -1, extracts the frame at EndFrame index and excludes it from the cloned video. Path to PNG file for extracted frame. Can be absolute or relative to current directory. .png extension is added if not provided. If EndFrame is -1, extracts the last frame and excludes it from the cloned video. If EndFrame is not -1, extracts the frame at EndFrame index and excludes it from the cloned video.
.PARAMETER Size .PARAMETER Size
Size of the output video. Uses the input video's size if not specified. Size of the output video. Uses the input video's size if not specified.
Possible values: 480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height". Possible values: 320p, 384p, 480p, 720p, 900p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height".
For standard named sizes the output preserves the source's aspect ratio: the reference
height is used as the short-side target and the other dimension is computed from the
source AR (landscape/square: target height = reference, width computed; portrait: target
width = reference, height computed). Custom sizes (width:height) are applied exactly.
Forces a video re-encode, since scaling requires re-encoding. Forces a video re-encode, since scaling requires re-encoding.
.PARAMETER FPS .PARAMETER FPS
Frames per second for the output video. Uses the input video's FPS if not specified. Frames per second for the output video. Uses the input video's FPS if not specified.
@@ -203,8 +207,11 @@ if (-not [string]::IsNullOrWhiteSpace($Audio)) {
} }
$sizeMap = @{ $sizeMap = @{
'320p' = @{ Width = 570; Height = 320 }
'384p' = @{ Width = 684; Height = 384 }
'480p' = @{ Width = 854; Height = 480 } '480p' = @{ Width = 854; Height = 480 }
'720p' = @{ Width = 1280; Height = 720 } '720p' = @{ Width = 1280; Height = 720 }
'900p' = @{ Width = 1600; Height = 900 }
'1080p' = @{ Width = 1920; Height = 1080 } '1080p' = @{ Width = 1920; Height = 1080 }
'HD' = @{ Width = 1920; Height = 1080 } 'HD' = @{ Width = 1920; Height = 1080 }
'1440p' = @{ Width = 2560; Height = 1440 } '1440p' = @{ Width = 2560; Height = 1440 }
@@ -215,16 +222,18 @@ $sizeMap = @{
$scaleWidth = 0 $scaleWidth = 0
$scaleHeight = 0 $scaleHeight = 0
$standardSizeHeight = 0
if (-not [string]::IsNullOrWhiteSpace($Size)) { if (-not [string]::IsNullOrWhiteSpace($Size)) {
if ($sizeMap.ContainsKey($Size)) { if ($sizeMap.ContainsKey($Size)) {
$scaleWidth = $sizeMap[$Size].Width $scaleWidth = $sizeMap[$Size].Width
$scaleHeight = $sizeMap[$Size].Height $scaleHeight = $sizeMap[$Size].Height
$standardSizeHeight = $sizeMap[$Size].Height
} elseif ($Size -match '^(\d+):(\d+)$') { } elseif ($Size -match '^(\d+):(\d+)$') {
$scaleWidth = [int]$Matches[1] $scaleWidth = [int]$Matches[1]
$scaleHeight = [int]$Matches[2] $scaleHeight = [int]$Matches[2]
} else { } else {
Write-Error ('Invalid Size value: {0}. Possible values: 480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height".' -f $Size) Write-Error ('Invalid Size value: {0}. Possible values: 320p, 384p, 480p, 720p, 900p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height".' -f $Size)
exit 1 exit 1
} }
} }
@@ -287,6 +296,18 @@ if ($LASTEXITCODE -eq 0) {
} }
} }
if ($standardSizeHeight -gt 0 -and -not $LTX -and $sourceWidth -gt 0 -and $sourceHeight -gt 0) {
if ($sourceWidth -ge $sourceHeight) {
$scaleHeight = $standardSizeHeight
$scaleWidth = [int][Math]::Round([double]$sourceWidth * $standardSizeHeight / $sourceHeight)
if ($scaleWidth % 2) { $scaleWidth++ }
} else {
$scaleWidth = $standardSizeHeight
$scaleHeight = [int][Math]::Round([double]$sourceHeight * $standardSizeHeight / $sourceWidth)
if ($scaleHeight % 2) { $scaleHeight++ }
}
}
if ($LTX) { if ($LTX) {
$ltxKey = $Size $ltxKey = $Size
if ($ltxKey -eq 'HD') { $ltxKey = '1080p' } if ($ltxKey -eq 'HD') { $ltxKey = '1080p' }
+237 -35
View File
@@ -21,14 +21,38 @@
- 4 videos: 2x2 grid (1920x1080 each) - 4 videos: 2x2 grid (1920x1080 each)
Portrait mode (height > width, e.g., 1080x1920) - 1K HD: Portrait mode (height > width, e.g., 1080x1920) - 1K HD:
- 2 videos: Side-by-side (608x1080 each, centered with padding to 1920x1080) - 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) - 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) - 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: Portrait mode (height > width, e.g., 1080x1920) - 4K UHD:
- 2 videos: Side-by-side (1216x2160 each, centered with padding to 3840x2160) - 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) - 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) - 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. Requires ffmpeg and ffprobe in system PATH. Supports -WhatIf for dry-run testing.
@@ -55,8 +79,10 @@
.PARAMETER Clip .PARAMETER Clip
Reduces the output height to match the actual scaled height of the sources, removing Reduces the output height to match the actual scaled height of the sources, removing
the top/bottom black letterbox padding. Only applies when exactly 2 source videos are the top/bottom black letterbox padding. For landscape, only applies when exactly 2
provided; ignored otherwise. 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: Behavior depends on the orientations of the two sources:
@@ -72,6 +98,13 @@
occupies 1280x720 on its S1/S2 side, portrait fits in the remaining 640x720). 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). 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 .EXAMPLE
.\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison.mp4 .\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison.mp4
@@ -101,6 +134,36 @@
.\Compare-Videos.ps1 -S1 a_720p.mp4 -S2 b_720p.mp4 -Output side-by-side.mp4 -Clip .\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. 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)] [CmdletBinding(SupportsShouldProcess)]
param( param(
@@ -123,7 +186,9 @@ param(
[switch]$In4K, [switch]$In4K,
[switch]$Clip [switch]$Clip,
[switch]$Sideways
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -158,11 +223,6 @@ $videoCount = 2
if ($S4) { $videoCount = 4 } if ($S4) { $videoCount = 4 }
elseif ($S3) { $videoCount = 3 } elseif ($S3) { $videoCount = 3 }
if ($Clip -and $videoCount -ne 2) {
Write-Warning "-Clip is only supported with exactly 2 source videos; ignoring."
$Clip = $false
}
# --- Detect orientation of S1 --- # --- Detect orientation of S1 ---
Write-Host "Detecting orientation of S1..." -ForegroundColor Cyan Write-Host "Detecting orientation of S1..." -ForegroundColor Cyan
@@ -196,10 +256,18 @@ if ($isPortrait) {
Write-Host "Landscape mode detected (${width}x${height}) - Output: $resolutionLabel" -ForegroundColor Green 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 --- # --- Clip mode: shrink output height to the actual scaled height of the sources ---
$clipMixedLandscape = $false $clipMixedLandscape = $false
$clipMixedFilterComplex = $null $clipMixedFilterComplex = $null
if ($Clip) { if ($Clip -and -not $Sideways) {
Write-Host "Probing S2 dimensions for -Clip..." -ForegroundColor Cyan 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 $probeOutput2 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S2 2>&1
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
@@ -273,6 +341,15 @@ if ($Clip) {
$h2Scaled = [int][Math]::Floor($clipCellWidth * $h2 / $w2) $h2Scaled = [int][Math]::Floor($clipCellWidth * $h2 / $w2)
$clippedHeight = [Math]::Max($h1Scaled, $h2Scaled) $clippedHeight = [Math]::Max($h1Scaled, $h2Scaled)
if ($clippedHeight % 2) { $clippedHeight++ } # libx264/yuv420p requires even dimensions 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 -gt $outputHeight) { $clippedHeight = $outputHeight }
if ($clippedHeight -lt $outputHeight) { if ($clippedHeight -lt $outputHeight) {
@@ -287,7 +364,68 @@ if ($Clip) {
$filterComplex = "" $filterComplex = ""
$inputArgs = @("-i", $S1, "-i", $S2) $inputArgs = @("-i", $S1, "-i", $S2)
if ($clipMixedLandscape) { 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. # Mixed-orientation -Clip layout was prepared above; use it as-is.
$filterComplex = $clipMixedFilterComplex $filterComplex = $clipMixedFilterComplex
} elseif ($isPortrait) { } elseif ($isPortrait) {
@@ -295,34 +433,98 @@ if ($clipMixedLandscape) {
$cellWidth2 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K $cellWidth2 = [int]($outputWidth * 0.6333 / 2) # ~608 for 1K, ~1216 for 4K
$cellWidth3 = [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 $cellWidth4 = [int]($outputWidth / 4) # 480 for 1K, 960 for 4K
$cellHeight4 = [int]($outputHeight * 0.79) # ~854 for 1K, ~1708 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) { switch ($videoCount) {
2 { 2 {
# 2 videos side-by-side, centered # 2 videos side-by-side, centered (or unpadded with -Clip)
$filterComplex = "[0:v]scale=${cellWidth2}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth2}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v0];" + if ($Clip) {
"[1:v]scale=${cellWidth2}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth2}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v1];" + $clippedWidth = $cellWidth2 * 2
"[v0][v1]hstack=inputs=2[stacked];" + Write-Host ("Clip mode (portrait): output width reduced from {0} to {1} ({1}x{2})" -f $outputWidth, $clippedWidth, $outputHeight) -ForegroundColor Yellow
"[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]" $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 {
# 3 videos side-by-side, centered # 3 videos side-by-side, centered (or unpadded with -Clip if all sources are portrait)
$inputArgs += @("-i", $S3) $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];" + $applyPortraitClip = $false
"[1:v]scale=${cellWidth3}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth3}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v1];" + if ($Clip) {
"[2:v]scale=${cellWidth3}:${outputHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth3}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[v2];" + $probeOutput3 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S3 2>&1
"[v0][v1][v2]hstack=inputs=3[stacked];" + if ($LASTEXITCODE -ne 0) {
"[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]" 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 {
# 4 videos side-by-side, centered # 4 videos side-by-side, centered (or unpadded with -Clip if all sources are portrait)
$inputArgs += @("-i", $S3, "-i", $S4) $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];" + $applyPortraitClip4 = $false
"[1:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v1];" + if ($Clip) {
"[2:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v2];" + $probeOutput3 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S3 2>&1
"[3:v]scale=${cellWidth4}:${cellHeight4}:force_original_aspect_ratio=decrease,pad=${cellWidth4}:${cellHeight4}:(ow-iw)/2:(oh-ih)/2:black[v3];" + if ($LASTEXITCODE -ne 0) {
"[v0][v1][v2][v3]hstack=inputs=4[stacked];" + Write-Error "Failed to probe video dimensions for S3: $probeOutput3"
"[stacked]pad=${outputWidth}:${outputHeight}:(ow-iw)/2:(oh-ih)/2:black[outv]" 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 { } else {
+18
View File
@@ -131,6 +131,24 @@ foreach ($video in $videos) {
elseif ($inInfo.Width -eq 448 -and $inInfo.Height -eq 832) { elseif ($inInfo.Width -eq 448 -and $inInfo.Height -eq 832) {
$vf = 'scale=480:-2,crop=480:854:(in_w-480)/2:(in_h-854)/2' $vf = 'scale=480:-2,crop=480:854:(in_w-480)/2:(in_h-854)/2'
} }
elseif ($inInfo.Width -eq 1600 -and $inInfo.Height -eq 896) {
$vf = 'scale=-2:900,crop=1600:900:(in_w-1600)/2:(in_h-900)/2'
}
elseif ($inInfo.Width -eq 640 -and $inInfo.Height -eq 384) {
$vf = 'scale=684:-2,crop=684:384:(in_w-684)/2:(in_h-384)/2'
}
elseif ($inInfo.Width -eq 576 -and $inInfo.Height -eq 320) {
$vf = 'crop=570:320:3:0'
}
elseif ($inInfo.Width -eq 896 -and $inInfo.Height -eq 1600) {
$vf = 'scale=900:-2,crop=900:1600:(in_w-900)/2:(in_h-1600)/2'
}
elseif ($inInfo.Width -eq 384 -and $inInfo.Height -eq 640) {
$vf = 'scale=-2:684,crop=384:684:(in_w-384)/2:(in_h-684)/2'
}
elseif ($inInfo.Width -eq 320 -and $inInfo.Height -eq 576) {
$vf = 'crop=320:570:0:3'
}
if (-not $vf) { if (-not $vf) {
Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)" Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)"