Compare commits

...

2 Commits

6 changed files with 501 additions and 26 deletions
+94 -6
View File
@@ -1,6 +1,8 @@
<#
.SYNOPSIS
Clones (copies) a video file using ffmpeg.
Supports MP4 (H.264), WebM and Matroska (MKV) sources, including YouTube/Google DASH downloads encoded as VP9 + Opus/AAC.
Non-H.264 video and non-MP4-compatible audio are automatically transcoded to H.264 / AAC so the target is always a standard MP4.
When Sequence is provided and EndFrame is -1:
- First, the last frame of the video is saved as a PNG image (using the Sequence value as filename).
- After, the video is cloned (copied) from StartFrame to the second-to-last frame (excluding the last frame).
@@ -8,7 +10,8 @@
- First, the frame at EndFrame index is saved as a PNG image (using the Sequence value as filename).
- After, the video is cloned (copied) from StartFrame to EndFrame-1 (excluding the extracted frame).
.PARAMETER Source
Path to the input video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
Path to the input video file. Can be absolute or relative to current directory.
If the path has no extension, .mp4, .webm and .mkv are tried in turn.
.PARAMETER Target
Path to the output video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
.PARAMETER StartFrame
@@ -76,12 +79,26 @@ $ErrorActionPreference = 'Stop'
$SourcePath = $Source
if (-not $SourcePath.EndsWith('.mp4', [StringComparison]::OrdinalIgnoreCase)) {
$SourcePath += '.mp4'
# Resolve source path: accept .mp4, .webm or .mkv. If the path doesn't exist as-is,
# try appending each supported extension in turn (preserving the original .mp4-default
# behaviour while adding WebM/MKV support for YouTube/Google DASH downloads).
$supportedSourceExts = @('.mp4', '.webm', '.mkv')
if (-not (Test-Path -LiteralPath $SourcePath)) {
$resolved = $null
foreach ($ext in $supportedSourceExts) {
$candidate = "$SourcePath$ext"
if (Test-Path -LiteralPath $candidate) {
$resolved = $candidate
break
}
}
if ($resolved) {
$SourcePath = $resolved
}
}
if (-not (Test-Path -LiteralPath $SourcePath)) {
Write-Error ('Source video file not found: {0}' -f $SourcePath)
Write-Error ('Source video file not found: {0}' -f $Source)
exit 1
}
@@ -103,12 +120,22 @@ if ($NoAudio -and -not [string]::IsNullOrWhiteSpace($Audio)) {
}
$AudioPath = $null
$externalAudioCodec = ''
$externalAudioCodecMismatch = $false
if (-not [string]::IsNullOrWhiteSpace($Audio)) {
$AudioPath = $Audio
if (-not (Test-Path -LiteralPath $AudioPath)) {
Write-Error ('Audio file not found: {0}' -f $AudioPath)
exit 1
}
# Probe the external audio file's codec so a non-MP4-compatible codec
# (e.g. Opus from a YouTube/DASH download) is transcoded to AAC instead
# of being stream-copied into the MP4 container.
$ffprobeExtAudioOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $AudioPath 2>&1
if ($LASTEXITCODE -eq 0) {
$externalAudioCodec = ($ffprobeExtAudioOutput | Select-Object -First 1).Trim().ToLowerInvariant()
}
}
$sizeMap = @{
@@ -190,6 +217,30 @@ if ($ffprobeExit -eq 0) {
}
}
# Probe source video and audio codecs. The MP4 container only supports a limited set
# of codecs: when the source is VP9/VP8/AV1 video or Opus/Vorbis/etc. audio (typical
# YouTube/Google DASH downloads), we must transcode rather than stream-copy.
$ffprobeVideoCodecOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
$sourceVideoCodec = ''
if ($LASTEXITCODE -eq 0) {
$sourceVideoCodec = ($ffprobeVideoCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant()
}
$ffprobeAudioCodecOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
$sourceAudioCodec = ''
if ($LASTEXITCODE -eq 0) {
$sourceAudioCodec = ($ffprobeAudioCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant()
}
# A non-H.264 source forces a video re-encode regardless of trimming/scaling/FPS changes.
$videoCodecMismatch = ($sourceVideoCodec -and $sourceVideoCodec -ne 'h264')
# MP4-container-compatible audio codecs. Anything else (opus, vorbis, flac, ...) must
# be transcoded to AAC when stream-copying audio.
$mp4AudioCodecs = @('aac', 'mp3', 'ac3', 'eac3', 'alac')
$audioCodecMismatch = ($sourceAudioCodec -and ($mp4AudioCodecs -notcontains $sourceAudioCodec))
$externalAudioCodecMismatch = ($externalAudioCodec -and ($mp4AudioCodecs -notcontains $externalAudioCodec))
if ($StartFrame -lt 0 -or $StartFrame -gt $maxFrameIndex) {
Write-Error ('StartFrame {0} is out of range. Valid range: 0 to {1}' -f $StartFrame, $maxFrameIndex)
exit 1
@@ -258,7 +309,17 @@ if (-not [string]::IsNullOrWhiteSpace($Sequence)) {
$frameCount = $actualEndFrame - $StartFrame + 1
Write-Host ('Cloning video: {0}' -f $Source)
Write-Host ('Cloning video: {0}' -f $SourcePath)
if ($sourceVideoCodec) {
Write-Host (' Source codec: video={0}{1} audio={2}{3}' -f `
$sourceVideoCodec, $(if ($videoCodecMismatch) { ' (will transcode to h264)' } else { '' }), `
$(if ($sourceAudioCodec) { $sourceAudioCodec } else { 'none' }), `
$(if ($audioCodecMismatch) { ' (will transcode to aac)' } else { '' }))
}
if ($AudioPath -and $externalAudioCodec) {
Write-Host (' External audio codec: {0}{1}' -f `
$externalAudioCodec, $(if ($externalAudioCodecMismatch) { ' (will transcode to aac)' } else { '' }))
}
Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount)
if ($scaleWidth -gt 0) {
Write-Host (' Size: {0}x{1}' -f $scaleWidth, $scaleHeight)
@@ -277,7 +338,7 @@ if ($AudioPath) {
$ffmpegArgs += @('-i', $AudioPath)
}
$needsReencode = $scaleWidth -gt 0 -or $FPS -gt 0
$needsReencode = $scaleWidth -gt 0 -or $FPS -gt 0 -or $videoCodecMismatch
if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
$filterParts = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame)
@@ -292,6 +353,9 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
$ffmpegArgs += @(
'-vf', $videoFilter
'-vsync', 'vfr'
'-c:v', 'libx264'
'-pix_fmt', 'yuv420p'
'-preset', 'slow'
)
if ($CRF -ge 0) {
$ffmpegArgs += @('-crf', $CRF.ToString())
@@ -308,8 +372,15 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
if ($FPS -gt 0) {
$filterParts += ('fps={0}' -f $FPS)
}
if ($filterParts.Count -gt 0) {
$videoFilter = $filterParts -join ','
$ffmpegArgs += @('-vf', $videoFilter)
}
$ffmpegArgs += @(
'-c:v', 'libx264'
'-pix_fmt', 'yuv420p'
'-preset', 'slow'
)
if ($CRF -ge 0) {
$ffmpegArgs += @('-crf', $CRF.ToString())
} elseif ($videoBitrate) {
@@ -335,6 +406,15 @@ if ($NoAudio) {
'-t', $durationStr
'-c:a', 'aac'
)
} elseif ($externalAudioCodecMismatch) {
# External audio (e.g. Opus from a YouTube/DASH WebM) cannot be stream-copied into MP4.
$ffmpegArgs += @(
'-map', '0:v'
'-map', '1:a'
'-c:a', 'aac'
'-b:a', '192k'
'-shortest'
)
} else {
$ffmpegArgs += @(
'-map', '0:v'
@@ -351,6 +431,14 @@ if ($NoAudio) {
$endTimeStr = $endTime.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)
$audioFilter = "atrim=start=${startTimeStr}:end=${endTimeStr},asetpts=PTS-STARTPTS"
$ffmpegArgs += @('-af', $audioFilter)
if ($audioCodecMismatch) {
# ffmpeg will already re-encode here (no -c:a copy), but force AAC explicitly
# so the output is MP4-compatible regardless of source codec.
$ffmpegArgs += @('-c:a', 'aac', '-b:a', '192k')
}
} elseif ($audioCodecMismatch) {
# Source audio (e.g. Opus from a YouTube/DASH WebM) cannot be stream-copied into MP4.
$ffmpegArgs += @('-c:a', 'aac', '-b:a', '192k')
} else {
$ffmpegArgs += @('-c:a', 'copy')
}
+2
View File
@@ -0,0 +1,2 @@
@echo off
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Create-PillarBox.ps1" %*
+376
View File
@@ -0,0 +1,376 @@
<#
.SYNOPSIS
Creates a pillar-boxed video file from a video file.
.PARAMETER Source
Path to the input video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
.PARAMETER Target
Path to the output video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
.PARAMETER Effect
The effect to apply to the side bars. Possible values are:
black: The side bars will be black.
white: The side bars will be white.
standard: Creates softly blurred side bars generated from the video itself, with a subtle zoom to fill the 16:9 frame.
dark: Produces blurred side bars with reduced brightness, creating a darker, more contrasted background that enhances focus on the main video.
gaussian: Generates smooth, highquality Gaussianblurred side bars for a clean, professional broadcast-style background.
.PARAMETER Size
Size of the output video. Uses the input video's closest size that will fit the 16:9 aspect ratio if not specified.
Possible values: 480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height".
.PARAMETER FPS
Frames per second for the output video. Uses the input video's FPS if not specified.
.PARAMETER NoAudio
When present, excludes audio from the source video.
.PARAMETER Audio
Path to an alternate audio file to use for the target video. Can be absolute or relative to current directory. When specified, this audio replaces the source video's audio.
.PARAMETER CRF
Constant Rate Factor for video encoding (0-51, lower is better quality). When specified, overrides both the video bitrate and the default -crf 8 value. Only used when re-encoding is required.
.DESCRIPTION
Creates a pillar-boxed video file from a video file. Will add black bars to the sides of the video to match 16:9 aspect ratio.
The black bars will be verticals on each side of the output video if the source video is more portrait-like (e.g. 9:16).
Otherwise, the black bars will be horizontal on the top and bottom of the output video.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Target,
[Parameter(Mandatory = $true)]
[string]$Effect,
[Parameter(Mandatory = $false)]
[string]$Size = $null,
[Parameter(Mandatory = $false)]
[double]$FPS = 0,
[Parameter(Mandatory = $false)]
[switch]$NoAudio,
[Parameter(Mandatory = $false)]
[string]$Audio = $null,
[Parameter(Mandatory = $false)]
[int]$CRF = -1
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-VideoInfo {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$json = & ffprobe -v error -print_format json -show_streams -show_format -- "$Path"
if ($LASTEXITCODE -ne 0 -or -not $json) {
throw "ffprobe failed for: $Path"
}
$info = $json | ConvertFrom-Json
$videoStream = $info.streams | Where-Object { $_.codec_type -eq 'video' } | Select-Object -First 1
if (-not $videoStream) {
throw "No video stream found in: $Path"
}
$audioStream = $info.streams | Where-Object { $_.codec_type -eq 'audio' } | Select-Object -First 1
# Safe property accessor: returns $null when the property is absent (strict-mode friendly).
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
$rFrameRate = & $get $videoStream 'r_frame_rate'
$fps = $null
if ($rFrameRate -and $rFrameRate -match '^(\d+)/(\d+)$') {
$num = [double]$Matches[1]
$den = [double]$Matches[2]
if ($den -ne 0) {
$fps = $num / $den
}
}
$videoBitRate = & $get $videoStream 'bit_rate'
$formatBitRate = & $get $info.format 'bit_rate'
$bitRate = if ($videoBitRate) { $videoBitRate } else { $formatBitRate }
[pscustomobject]@{
Path = $Path
FileName = [System.IO.Path]::GetFileName($Path)
Width = [int]$videoStream.width
Height = [int]$videoStream.height
Fps = $fps
BitRate = $bitRate
HasAudio = [bool]$audioStream
VideoBitRate = $videoBitRate
}
}
function Format-Quality {
param(
[Parameter(Mandatory = $false)]
$BitRate
)
if (-not $BitRate -or $BitRate -notmatch '^\d+$') {
return 'unknown'
}
$kbps = [math]::Round(([double]$BitRate) / 1000.0)
return "${kbps}kbps"
}
function Resolve-Size {
param(
[Parameter(Mandatory = $false)]
[string]$Value,
[Parameter(Mandatory = $true)]
[int]$SourceWidth,
[Parameter(Mandatory = $true)]
[int]$SourceHeight
)
# Standard 16:9 sizes (largest first only matters for closest-fit lookup)
$standards = @(
@{ Name = '480p'; Width = 854; Height = 480 },
@{ Name = '720p'; Width = 1280; Height = 720 },
@{ Name = '1080p'; Width = 1920; Height = 1080 },
@{ Name = 'HD'; Width = 1920; Height = 1080 },
@{ Name = '1440p'; Width = 2560; Height = 1440 },
@{ Name = '2K'; Width = 2560; Height = 1440 },
@{ Name = '2160p'; Width = 3840; Height = 2160 },
@{ Name = '4K'; Width = 3840; Height = 2160 }
)
if ($Value) {
$named = $standards | Where-Object { $_.Name -ieq $Value } | Select-Object -First 1
if ($named) {
return [pscustomobject]@{ Width = $named.Width; Height = $named.Height }
}
if ($Value -match '^(\d+):(\d+)$') {
return [pscustomobject]@{ Width = [int]$Matches[1]; Height = [int]$Matches[2] }
}
throw "Invalid -Size value: $Value. Use a named preset (480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K) or 'width:height'."
}
# Pick the smallest standard 16:9 preset whose longer side (width) is at least
# the source's longer side. The foreground is scaled down with
# 'force_original_aspect_ratio=decrease', so the source is allowed to shrink
# to fit; we only need the preset to be "big enough" relative to the source's
# largest dimension. This produces the documented behavior:
# 1080x1920 portrait -> 1920x1080 (vertical bars on either side)
# 720x1280 portrait -> 1280x720
# 2560x1080 landscape -> 2560x1440 (horizontal bars top/bottom)
$longSide = [math]::Max($SourceWidth, $SourceHeight)
$unique = $standards | Sort-Object { $_.Width } -Unique
foreach ($s in $unique) {
if ($s.Width -ge $longSide) {
return [pscustomobject]@{ Width = $s.Width; Height = $s.Height }
}
}
# Source larger than any standard preset: build a 16:9 frame whose width
# matches the source's longer side (rounded to even).
$w = $longSide + ($longSide % 2)
$h = [int][math]::Ceiling($w * 9.0 / 16.0)
if ($h % 2) { $h++ }
return [pscustomobject]@{ Width = $w; Height = $h }
}
# Append .mp4 extension if missing
if (-not [System.IO.Path]::GetExtension($Source)) {
$Source = "$Source.mp4"
}
if (-not [System.IO.Path]::GetExtension($Target)) {
$Target = "$Target.mp4"
}
if (-not (Test-Path -LiteralPath $Source)) {
throw "Source video not found: $Source"
}
$sourceFull = (Resolve-Path -LiteralPath $Source).Path
# Resolve Target to an absolute path (may not exist yet)
if ([System.IO.Path]::IsPathRooted($Target)) {
$targetFull = $Target
}
else {
$targetFull = Join-Path (Get-Location).Path $Target
}
if ($Audio) {
if (-not (Test-Path -LiteralPath $Audio)) {
throw "Audio file not found: $Audio"
}
$audioFull = (Resolve-Path -LiteralPath $Audio).Path
}
else {
$audioFull = $null
}
if ($CRF -ne -1 -and ($CRF -lt 0 -or $CRF -gt 51)) {
throw "Invalid -CRF value: $CRF. Must be in range 0-51."
}
$validEffects = @('black', 'white', 'standard', 'dark', 'gaussian')
$effectLower = $Effect.ToLower()
if ($effectLower -notin $validEffects) {
throw "Invalid -Effect value: $Effect. Must be one of: $($validEffects -join ', ')"
}
$inInfo = Get-VideoInfo -Path $sourceFull
$inRes = "{0}x{1}" -f $inInfo.Width, $inInfo.Height
$inFpsText = if ($inInfo.Fps) { "{0:0.###}" -f $inInfo.Fps } else { 'unknown' }
$inQuality = Format-Quality -BitRate $inInfo.BitRate
$inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' }
Write-Host "Input : Filename=$($inInfo.FileName) Resolution=$inRes FPS=$inFpsText Quality=$inQuality Audio=$inAudioText"
$outSize = Resolve-Size -Value $Size -SourceWidth $inInfo.Width -SourceHeight $inInfo.Height
$outWidth = $outSize.Width
$outHeight = $outSize.Height
# Force even dimensions for yuv420p compatibility
if ($outWidth % 2) { $outWidth++ }
if ($outHeight % 2) { $outHeight++ }
# Scale the source so it fits entirely inside the output frame, preserving aspect ratio.
$scaleFg = "scale=w=${outWidth}:h=${outHeight}:force_original_aspect_ratio=decrease"
# Build the video filter graph depending on the requested -Effect.
# black/white : simple pad with the solid color (uses -vf)
# standard : softly blurred background with a subtle zoom (uses -filter_complex)
# dark : blurred background with reduced brightness/saturation (uses -filter_complex)
# gaussian : high-quality Gaussian-blurred background (uses -filter_complex)
$useFilterComplex = $effectLower -in @('standard', 'dark', 'gaussian')
if (-not $useFilterComplex) {
$padExpr = "pad=${outWidth}:${outHeight}:(ow-iw)/2:(oh-ih)/2:${effectLower}"
$vf = "${scaleFg},${padExpr},setsar=1"
}
else {
# Background scaling: fill frame entirely, then crop. 'standard' adds a subtle ~10% zoom.
if ($effectLower -eq 'standard') {
$zoomW = [int][math]::Ceiling($outWidth * 1.1)
$zoomH = [int][math]::Ceiling($outHeight * 1.1)
if ($zoomW % 2) { $zoomW++ }
if ($zoomH % 2) { $zoomH++ }
$bgScale = "scale=w=${zoomW}:h=${zoomH}:force_original_aspect_ratio=increase,crop=${outWidth}:${outHeight}"
}
else {
$bgScale = "scale=w=${outWidth}:h=${outHeight}:force_original_aspect_ratio=increase,crop=${outWidth}:${outHeight}"
}
# Per-effect blur and color treatment for the background.
switch ($effectLower) {
'standard' { $bgEffect = 'boxblur=20:1' }
'dark' { $bgEffect = 'boxblur=20:1,eq=brightness=-0.25:saturation=0.8' }
'gaussian' { $bgEffect = 'gblur=sigma=30' }
}
$filterComplex = "[0:v]split=2[bg][fg];" `
+ "[bg]${bgScale},${bgEffect},setsar=1[blurred];" `
+ "[fg]${scaleFg},setsar=1[scaled];" `
+ "[blurred][scaled]overlay=(W-w)/2:(H-h)/2[outv]"
}
# Effective output FPS for log display
$effFps = if ($FPS -gt 0) { $FPS } elseif ($inInfo.Fps) { $inInfo.Fps } else { $null }
$effFpsText = if ($effFps) { "{0:0.###}" -f $effFps } else { 'unknown' }
# Build ffmpeg arguments
$ffArgs = @('-y', '-i', $sourceFull)
if ($audioFull) {
$ffArgs += @('-i', $audioFull)
}
if ($useFilterComplex) {
$ffArgs += @('-filter_complex', $filterComplex)
$videoMap = '[outv]'
}
else {
$ffArgs += @('-vf', $vf)
$videoMap = '0:v:0'
}
if ($FPS -gt 0) {
$ffArgs += @('-r', ("{0}" -f $FPS))
}
# Video encoding: prefer CRF if specified, otherwise preserve source bitrate, else fallback to -crf 8.
if ($CRF -ge 0) {
$ffArgs += @('-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', "$CRF")
}
elseif ($inInfo.VideoBitRate -and $inInfo.VideoBitRate -match '^\d+$') {
$ffArgs += @('-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-b:v', $inInfo.VideoBitRate)
}
else {
$ffArgs += @('-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'slow', '-crf', '8')
}
# Audio handling. Always emit -map for video so filter_complex output is wired correctly;
# audio source depends on -NoAudio / -Audio / source-has-audio.
if ($NoAudio) {
$ffArgs += @('-map', $videoMap, '-an')
}
elseif ($audioFull) {
# Map video from filter (or input 0) and audio from input 1, stop at shortest stream
$ffArgs += @('-map', $videoMap, '-map', '1:a:0', '-c:a', 'aac', '-b:a', '192k', '-shortest')
}
elseif ($inInfo.HasAudio) {
$ffArgs += @('-map', $videoMap, '-map', '0:a:0?', '-c:a', 'copy')
}
else {
$ffArgs += @('-map', $videoMap, '-an')
}
$ffArgs += @($targetFull)
Write-Host "Output: Filename=$([System.IO.Path]::GetFileName($targetFull)) Resolution=${outWidth}x${outHeight} FPS=$effFpsText Effect=$effectLower"
# Ensure target directory exists
$targetDir = [System.IO.Path]::GetDirectoryName($targetFull)
if ($targetDir -and -not (Test-Path -LiteralPath $targetDir)) {
if ($PSCmdlet.ShouldProcess($targetDir, 'Create output directory')) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
}
if ($PSCmdlet.ShouldProcess($targetFull, 'Create pillar-boxed video with ffmpeg')) {
$ffmpegOutput = & ffmpeg @ffArgs 2>&1
$ffmpegExit = $LASTEXITCODE
if ($ffmpegExit -ne 0) {
if (Test-Path -LiteralPath $targetFull) {
Remove-Item -LiteralPath $targetFull -Force -ErrorAction SilentlyContinue
}
# Use Write-Host (not Write-Error) so the full ffmpeg output is shown;
# under $ErrorActionPreference = 'Stop' a piped Write-Error would terminate
# on the first line, swallowing the rest of the diagnostic output and the
# final 'throw' below.
$ffmpegOutput | ForEach-Object { Write-Host $_ }
Write-Host "ffmpeg $($ffArgs -join ' ')"
throw "ffmpeg failed while creating pillar-boxed video: $($inInfo.FileName)"
}
$outInfo = Get-VideoInfo -Path $targetFull
$outRes = "{0}x{1}" -f $outInfo.Width, $outInfo.Height
$outFpsText = if ($outInfo.Fps) { "{0:0.###}" -f $outInfo.Fps } else { 'unknown' }
$outQuality = Format-Quality -BitRate $outInfo.BitRate
$outAudioText = if ($outInfo.HasAudio) { 'yes' } else { 'no' }
Write-Host "Done : Filename=$($outInfo.FileName) Resolution=$outRes FPS=$outFpsText Quality=$outQuality Audio=$outAudioText"
}
else {
$formattedArgs = $ffArgs | ForEach-Object { if ($_ -match '\s') { "`"$_`"" } else { $_ } }
Write-Host "WhatIf: Would create pillar-boxed video with ffmpeg $($formattedArgs -join ' ')"
}
+9 -6
View File
@@ -40,8 +40,12 @@ function Get-VideoInfo {
$audioStream = $info.streams | Where-Object { $_.codec_type -eq 'audio' } | Select-Object -First 1
# Safe property accessor: returns $null when the property is absent (strict-mode friendly).
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
$rFrameRate = & $get $videoStream 'r_frame_rate'
$fps = $null
if ($videoStream.r_frame_rate -and $videoStream.r_frame_rate -match '^(\d+)/(\d+)$') {
if ($rFrameRate -and $rFrameRate -match '^(\d+)/(\d+)$') {
$num = [double]$Matches[1]
$den = [double]$Matches[2]
if ($den -ne 0) {
@@ -49,10 +53,9 @@ function Get-VideoInfo {
}
}
$bitRate = $videoStream.bit_rate
if (-not $bitRate) {
$bitRate = $info.format.bit_rate
}
$videoBitRate = & $get $videoStream 'bit_rate'
$formatBitRate = & $get $info.format 'bit_rate'
$bitRate = if ($videoBitRate) { $videoBitRate } else { $formatBitRate }
[pscustomobject]@{
Path = $Path
@@ -62,7 +65,7 @@ function Get-VideoInfo {
Fps = $fps
BitRate = $bitRate
HasAudio = [bool]$audioStream
VideoBitRate = $videoStream.bit_rate
VideoBitRate = $videoBitRate
}
}
+9 -6
View File
@@ -40,8 +40,12 @@ function Get-VideoInfo {
$audioStream = $info.streams | Where-Object { $_.codec_type -eq 'audio' } | Select-Object -First 1
# Safe property accessor: returns $null when the property is absent (strict-mode friendly).
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
$rFrameRate = & $get $videoStream 'r_frame_rate'
$fps = $null
if ($videoStream.r_frame_rate -and $videoStream.r_frame_rate -match '^(\d+)/(\d+)$') {
if ($rFrameRate -and $rFrameRate -match '^(\d+)/(\d+)$') {
$num = [double]$Matches[1]
$den = [double]$Matches[2]
if ($den -ne 0) {
@@ -49,10 +53,9 @@ function Get-VideoInfo {
}
}
$bitRate = $videoStream.bit_rate
if (-not $bitRate) {
$bitRate = $info.format.bit_rate
}
$videoBitRate = & $get $videoStream 'bit_rate'
$formatBitRate = & $get $info.format 'bit_rate'
$bitRate = if ($videoBitRate) { $videoBitRate } else { $formatBitRate }
[pscustomobject]@{
Path = $Path
@@ -62,7 +65,7 @@ function Get-VideoInfo {
Fps = $fps
BitRate = $bitRate
HasAudio = [bool]$audioStream
VideoBitRate = $videoStream.bit_rate
VideoBitRate = $videoBitRate
}
}
+9 -6
View File
@@ -40,8 +40,12 @@ function Get-VideoInfo {
$audioStream = $info.streams | Where-Object { $_.codec_type -eq 'audio' } | Select-Object -First 1
# Safe property accessor: returns $null when the property is absent (strict-mode friendly).
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
$rFrameRate = & $get $videoStream 'r_frame_rate'
$fps = $null
if ($videoStream.r_frame_rate -and $videoStream.r_frame_rate -match '^(\d+)/(\d+)$') {
if ($rFrameRate -and $rFrameRate -match '^(\d+)/(\d+)$') {
$num = [double]$Matches[1]
$den = [double]$Matches[2]
if ($den -ne 0) {
@@ -49,10 +53,9 @@ function Get-VideoInfo {
}
}
$bitRate = $videoStream.bit_rate
if (-not $bitRate) {
$bitRate = $info.format.bit_rate
}
$videoBitRate = & $get $videoStream 'bit_rate'
$formatBitRate = & $get $info.format 'bit_rate'
$bitRate = if ($videoBitRate) { $videoBitRate } else { $formatBitRate }
[pscustomobject]@{
Path = $Path
@@ -62,7 +65,7 @@ function Get-VideoInfo {
Fps = $fps
BitRate = $bitRate
HasAudio = [bool]$audioStream
VideoBitRate = $videoStream.bit_rate
VideoBitRate = $videoBitRate
}
}