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

731 lines
28 KiB
PowerShell

<#
.SYNOPSIS
Creates a short clip video file from a 16:9 video file using the specified plan.
.PARAMETER Video
Path to the input video file. Can be absolute or relative to current directory.
.PARAMETER Images
Path to the output PNG images file pattern from which the short clip video will be created. Can be absolute or relative to current directory.
If not provided, the output files will be in subdirectory 'Frames' and named as the video file with a frame number suffix padded with zeros to 4 digits.
.PARAMETER Plan
Path to the plan file in INI format.
The file contains sections and key=value entries that define how the short clip video is created.
.PARAMETER Keep
If specified, keeps the temporary directory containing extracted frames with padding applied.
.PARAMETER Render
If specified, skips frame extraction and cropping, and only creates the output video from existing frames in the Images path.
.PARAMETER Color
Color name or RGB value to use for padding. Overrides the Color value from the INI plan file if specified. Default: black.
.PARAMETER Overlay
Path to an optional PNG file with transparency to overlay on each cropped frame. Overrides the Overlay value from the INI plan file if specified. Can be absolute or relative to current directory.
.PARAMETER Smoothing
Mode for adjusting cropping transition length to control speed. Overrides the Smoothing value from the INI plan file if specified.
Valid values: None, Up, Center, Down.
.PARAMETER Speed
Maximum cropping transition speed in pixels per frame (minimum: 1). Overrides the Speed value from the INI plan file if specified.
.DESCRIPTION
Creates a short clip video file from a 16:9 video file using the specified plan.
See Create-ShortClip.ini for details about the INI format and supported entries for the plan file.
Supports -WhatIf and -Confirm for safe execution.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $true)]
[string]$Video,
[Parameter(Mandatory = $false)]
[string]$Images = '',
[Parameter(Mandatory = $true)]
[string]$Plan = '',
[Parameter(Mandatory = $false)]
[switch]$Keep,
[Parameter(Mandatory = $false)]
[switch]$Render,
[Parameter(Mandatory = $false)]
[string]$Color = '',
[Parameter(Mandatory = $false)]
[string]$Overlay = '',
[Parameter(Mandatory = $false)]
[string]$Smoothing = '',
[Parameter(Mandatory = $false)]
[int]$Speed = $null
)
$ErrorActionPreference = 'Stop'
function Read-IniFile {
param([string]$Path)
if (-not (Test-Path $Path)) {
throw "Plan file not found: $Path"
}
$ini = @{}
$currentSection = $null
foreach ($line in Get-Content $Path) {
$line = $line.Trim()
if ($line -match '^\s*#' -or $line -eq '') {
continue
}
if ($line -match '^\[(.+)\]$') {
$currentSection = $matches[1]
$ini[$currentSection] = @{}
}
elseif ($line -match '^([^=]+)=(.*)$' -and $currentSection) {
$key = $matches[1].Trim()
$value = $matches[2].Trim()
$ini[$currentSection][$key] = $value
}
}
return $ini
}
function Get-VideoInfo {
param([string]$VideoPath)
$widthOutput = ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 $VideoPath 2>&1
$heightOutput = ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 $VideoPath 2>&1
$frameCountOutput = ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 $VideoPath 2>&1
$fpsOutput = ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 $VideoPath 2>&1
$bitrateOutput = ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of csv=p=0 $VideoPath 2>&1
$codecOutput = ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 $VideoPath 2>&1
$pixFmtOutput = ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 $VideoPath 2>&1
$audioCodecOutput = ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 $VideoPath 2>&1
$audioBitrateOutput = ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of csv=p=0 $VideoPath 2>&1
if ($widthOutput -match '^\d+$') {
$width = [int]$widthOutput
}
else {
throw "Failed to get video width"
}
if ($heightOutput -match '^\d+$') {
$height = [int]$heightOutput
}
else {
throw "Failed to get video height"
}
if ($frameCountOutput -match '^\d+$') {
$frameCount = [int]$frameCountOutput
}
else {
throw "Failed to get frame count"
}
if ($fpsOutput -match '^(\d+)/(\d+)$') {
$fps = [double]$matches[1] / [double]$matches[2]
}
elseif ($fpsOutput -match '^\d+(\.\d+)?$') {
$fps = [double]$fpsOutput
}
else {
throw "Failed to get frame rate"
}
$bitrate = if ($bitrateOutput -match '^\d+$') { $bitrateOutput } else { $null }
$codec = if ($codecOutput -and $codecOutput -ne 'N/A') { $codecOutput.Trim() } else { 'h264' }
$pixFmt = if ($pixFmtOutput -and $pixFmtOutput -ne 'N/A') { $pixFmtOutput.Trim() } else { 'yuv420p' }
$audioCodec = if ($audioCodecOutput -and $audioCodecOutput -ne 'N/A') { $audioCodecOutput.Trim() } else { $null }
$audioBitrate = if ($audioBitrateOutput -match '^\d+$') { [int]$audioBitrateOutput } else { $null }
return @{
Width = $width
Height = $height
FrameCount = $frameCount
FPS = $fps
Bitrate = $bitrate
Codec = $codec
PixelFormat = $pixFmt
AudioCodec = $audioCodec
AudioBitrate = $audioBitrate
}
}
function ConvertTo-CroppingOffset {
param(
[string]$Value,
[int]$InputWidth,
[int]$OutputWidth,
[string]$Position = 'Center'
)
switch ($Value) {
'Left' { return 0 }
'Center' {
$offset = [int](($InputWidth - $OutputWidth) / 2)
return [Math]::Max(0, $offset)
}
'Right' {
return [Math]::Max(0, $InputWidth - $OutputWidth)
}
default {
if ($Value -match '^\d+$') {
$numValue = [int]$Value
switch ($Position) {
'Left' {
$adjustedValue = $numValue
}
'Center' {
$adjustedValue = $numValue - [int]($OutputWidth / 2)
}
'Right' {
$adjustedValue = $numValue - $OutputWidth
}
}
$maxRight = [Math]::Max(0, $InputWidth - $OutputWidth)
return [Math]::Max(0, [Math]::Min($adjustedValue, $maxRight))
}
throw "Invalid cropping value: $Value"
}
}
}
function Get-CroppingOffsets {
param(
[hashtable]$CroppingSection,
[int]$TotalFrames,
[int]$InputWidth,
[int]$OutputWidth,
[string]$Position = 'Center',
[string]$Smoothing = 'None',
[int]$Speed = 10
)
$offsets = @{}
if ($null -eq $CroppingSection -or $CroppingSection.Count -eq 0) {
$defaultOffset = ConvertTo-CroppingOffset -Value 'Center' -InputWidth $InputWidth -OutputWidth $OutputWidth -Position $Position
for ($i = 0; $i -lt $TotalFrames; $i++) {
$offsets[$i] = $defaultOffset
}
return $offsets
}
$keyFrames = @()
foreach ($key in $CroppingSection.Keys) {
if ($key -match '^\d+$') {
$frameNum = [int]$key
$value = $CroppingSection[$key]
$offset = ConvertTo-CroppingOffset -Value $value -InputWidth $InputWidth -OutputWidth $OutputWidth -Position $Position
$keyFrames += [PSCustomObject]@{ Frame = $frameNum; Offset = $offset }
}
}
$keyFrames = @($keyFrames | Sort-Object -Property Frame)
for ($i = 1; $i -lt $keyFrames.Count; $i++) {
if ($keyFrames[$i].Frame -le $keyFrames[$i - 1].Frame) {
throw "Frame numbers in Cropping section must be in ascending order"
}
}
if ($keyFrames.Count -eq 0) {
$defaultOffset = ConvertTo-CroppingOffset -Value 'Center' -InputWidth $InputWidth -OutputWidth $OutputWidth -Position $Position
for ($i = 0; $i -lt $TotalFrames; $i++) {
$offsets[$i] = $defaultOffset
}
return $offsets
}
for ($i = 0; $i -lt $keyFrames.Count; $i++) {
$currentFrame = $keyFrames[$i].Frame
$currentOffset = $keyFrames[$i].Offset
$offsets[$currentFrame] = $currentOffset
if ($i -lt $keyFrames.Count - 1) {
$nextFrame = $keyFrames[$i + 1].Frame
$nextOffset = $keyFrames[$i + 1].Offset
$frameDiff = $nextFrame - $currentFrame
if ($frameDiff -gt 1) {
$centerFrame = [Math]::Round(($currentFrame + $nextFrame) / 2)
$offsetDiff = $nextOffset - $currentOffset
$step = [double]$offsetDiff / [double]$frameDiff
if ($Smoothing -ne 'None' -and $offsetDiff -ne 0) {
$computedSpeed = [Math]::Abs($step)
$startFrame = $currentFrame
$endFrame = $nextFrame
if ($computedSpeed -gt $Speed) {
Write-Host "Smoothing required between frames $startFrame and $endFrame at speed $computedSpeed (Max: $Speed)"
while ($computedSpeed -gt $Speed) {
$mode = $Smoothing
if ($mode -eq 'Center') {
if (($endFrame - $centerFrame) -le ($centerFrame - $startFrame)) {
$mode = 'Up'
}
else {
$mode = 'Down'
}
}
if ($mode -eq 'Up') {
if (($endFrame + 1) -lt $TotalFrames) {
$endFrame++
}
elseif (($startFrame - 1) -ge 0) {
$startFrame--
}
else {
break
}
}
elseif ($mode -eq 'Down') {
if (($startFrame - 1) -ge 0) {
$startFrame--
}
elseif (($endFrame + 1) -lt $TotalFrames) {
$endFrame++
}
else {
break
}
}
else {
throw "Internal error for mode value: $mode. Must be 'Up', or 'Down'"
}
$frameDiff = $endFrame - $startFrame
$step = [double]$offsetDiff / [double]$frameDiff
$computedSpeed = [Math]::Abs($step)
if ($computedSpeed -gt $Speed) {
$currentFrame = $startFrame
$nextFrame = $endFrame
}
}
Write-Host "Smoothing completed with frames now between $currentFrame and $nextFrame"
}
}
for ($f = $currentFrame + 1; $f -le $nextFrame; $f++) {
$interpolatedOffset = $currentOffset + ($step * ($f - $currentFrame))
$offsets[$f] = [Math]::Round($interpolatedOffset)
}
if (($i + 1) -lt $keyFrames.Count) {
$keyFrames[$i + 1].Frame = $nextFrame + 1
}
}
}
}
$lastKeyFrame = $keyFrames[-1]
for ($f = $lastKeyFrame.Frame + 1; $f -lt $TotalFrames; $f++) {
$offsets[$f] = $lastKeyFrame.Offset
}
for ($f = 0; $f -lt $keyFrames[0].Frame; $f++) {
if (-not $offsets.ContainsKey($f)) {
$offsets[$f] = $keyFrames[0].Offset
}
}
return $offsets
}
function Test-IniContent {
param([hashtable]$Ini)
if (-not $Ini.ContainsKey('Clip')) {
throw "INI file must contain a [Clip] section"
}
$clip = $Ini['Clip']
if ($clip.ContainsKey('Format')) {
if ($clip['Format'] -notin @('Portrait', 'Square')) {
throw "Invalid Format value: $($clip['Format']). Must be 'Portrait' or 'Square'"
}
}
if ($clip.ContainsKey('Height')) {
if ($clip['Height'] -notmatch '^\d+$') {
throw "Invalid Height value: $($clip['Height']). Must be a positive integer"
}
}
if ($clip.ContainsKey('Padding')) {
if ($clip['Padding'] -notin @('Top', 'Bottom', 'Center', 'None')) {
throw "Invalid Padding value: $($clip['Padding']). Must be 'Top', 'Bottom', 'Center', or 'None'"
}
}
if ($clip.ContainsKey('Audio')) {
if ($clip['Audio'] -notin @('Yes', 'No')) {
throw "Invalid Audio value: $($clip['Audio']). Must be 'Yes' or 'No'"
}
}
if ($clip.ContainsKey('Position')) {
if ($clip['Position'] -notin @('Left', 'Center', 'Right')) {
throw "Invalid Position value: $($clip['Position']). Must be 'Left', 'Center', or 'Right'"
}
}
if ($clip.ContainsKey('Smoothing')) {
if ($clip['Smoothing'] -notin @('None', 'Center', 'Up', 'Down')) {
throw "Invalid Smoothing value: $($clip['Smoothing']). Must be 'None', 'Center', 'Up', or 'Down'"
}
}
if ($clip.ContainsKey('Speed')) {
if ($clip['Speed'] -notmatch '^\d+$') {
throw "Invalid Speed value: $($clip['Speed']). Must be a positive integer"
}
elseif ([int]$clip['Speed'] -lt 1) {
throw "Invalid Speed value: $($clip['Speed']). Must be greater than or equal to 1"
}
}
}
try {
if (-not (Test-Path $Video)) {
throw "Video file not found: $Video"
}
# Overlay validation will be done after reading INI file
Write-Host "Reading plan file: $Plan"
$ini = Read-IniFile -Path $Plan
Write-Host "Validating plan file content"
Test-IniContent -Ini $ini
$clip = $ini['Clip']
$format = if ($clip.ContainsKey('Format')) { $clip['Format'] } else { 'Portrait' }
$outputHeight = if ($clip.ContainsKey('Height')) { [int]$clip['Height'] } else { 1920 }
$padding = if ($clip.ContainsKey('Padding')) { $clip['Padding'] } else { 'Center' }
$audio = if ($clip.ContainsKey('Audio')) { $clip['Audio'] } else { 'Yes' }
$position = if ($clip.ContainsKey('Position')) { $clip['Position'] } else { 'Center' }
if ($Smoothing -ne '') {
$smoothing = $Smoothing
}
elseif ($clip.ContainsKey('Smoothing')) {
$smoothing = $clip['Smoothing']
}
else {
$smoothing = 'None'
}
if ($Speed -gt 0) {
$speed = $Speed
}
elseif ($clip.ContainsKey('Speed')) {
$speed = [int]$clip['Speed']
}
else {
$speed = 10
}
# Color: Parameter overrides INI, defaults to 'black'
if ($Color -ne '') {
$color = $Color
}
elseif ($clip.ContainsKey('Color')) {
$color = $clip['Color']
}
else {
$color = 'black'
}
# Overlay: Parameter overrides INI, defaults to empty
if ($Overlay -ne '') {
$overlay = $Overlay
}
elseif ($clip.ContainsKey('Overlay')) {
$overlay = $clip['Overlay']
}
else {
$overlay = ''
}
# Convert overlay to absolute path if specified
if ($overlay -ne '') {
if (-not [System.IO.Path]::IsPathRooted($overlay)) {
$overlay = Join-Path (Get-Location).Path $overlay
}
}
# Validate overlay file exists if specified
if ($overlay -ne '' -and -not (Test-Path $overlay)) {
throw "Overlay file not found: $overlay"
}
$outputWidth = if ($format -eq 'Portrait') { [int]($outputHeight * 9 / 16) } else { $outputHeight }
Write-Host "Getting video information"
$videoInfo = Get-VideoInfo -VideoPath $Video
Write-Host "Input video: $($videoInfo.Width)x$($videoInfo.Height), $($videoInfo.FrameCount) frames, $($videoInfo.FPS) fps"
Write-Host "Output clip: ${outputWidth}x${outputHeight}, Format: $format"
# Determine if we need to work at input resolution then scale down
$needsDownscale = $videoInfo.Height -gt $outputHeight
if ($needsDownscale) {
# Calculate intermediate dimensions at input resolution scale
$intermediateHeight = $videoInfo.Height
$intermediateWidth = if ($format -eq 'Portrait') { [int]($intermediateHeight * 9 / 16) } else { $intermediateHeight }
Write-Host "Working at input resolution: ${intermediateWidth}x${intermediateHeight}, then scaling to ${outputWidth}x${outputHeight}"
} else {
$intermediateHeight = $outputHeight
$intermediateWidth = $outputWidth
}
if ($Images -eq '') {
$videoBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Video)
$videoDir = [System.IO.Path]::GetDirectoryName($Video)
if ($videoDir -eq '') { $videoDir = '.' }
$framesDir = Join-Path $videoDir 'Frames'
if (Test-Path $framesDir) {
if ($PSCmdlet.ShouldProcess($framesDir, 'Clear existing frames directory')) {
Get-ChildItem -Path $framesDir -File | Remove-Item -Force
}
}
else {
if ($PSCmdlet.ShouldProcess($framesDir, 'Create frames directory')) {
New-Item -ItemType Directory -Path $framesDir | Out-Null
}
}
$Images = Join-Path $framesDir "${videoBaseName}-%04d.png"
}
$imagesDir = [System.IO.Path]::GetDirectoryName($Images)
if ($imagesDir -and -not (Test-Path $imagesDir)) {
if ($PSCmdlet.ShouldProcess($imagesDir, 'Create output images directory')) {
New-Item -ItemType Directory -Path $imagesDir | Out-Null
}
}
$paddingColor = $color
if ($color -match '^#([0-9A-Fa-f]{6})$') {
$paddingColor = "0x$($matches[1])"
}
if ($Render) {
Write-Host "Render mode: Skipping frame extraction and cropping"
$imagesPattern = $Images -replace '%\d+d', '*'
$existingFrames = Get-ChildItem -Path $imagesPattern -ErrorAction SilentlyContinue
if (-not $existingFrames -or $existingFrames.Count -eq 0) {
throw "No existing frames found at: $Images. Cannot render without frames."
}
Write-Host "Found $($existingFrames.Count) existing frames to render"
}
else {
Write-Host "Calculating cropping offsets"
$croppingSection = if ($ini.ContainsKey('Cropping')) { $ini['Cropping'] } else { $null }
# Use intermediate dimensions for cropping calculation
$croppingOffsets = Get-CroppingOffsets -CroppingSection $croppingSection -TotalFrames $videoInfo.FrameCount -InputWidth $videoInfo.Width -OutputWidth $intermediateWidth -Position $position -Smoothing $smoothing -Speed $speed
Write-Host "`nCropping Offsets:"
Write-Host ("Frame".PadRight(10) + "Left Offset")
Write-Host ("-" * 21)
$previousOffset = $null
foreach ($frame in ($croppingOffsets.Keys | Sort-Object)) {
$currentOffset = $croppingOffsets[$frame]
if ($currentOffset -ne $previousOffset) {
Write-Host ("{0,-10}{1}" -f $frame, $currentOffset)
$previousOffset = $currentOffset
}
}
Write-Host ""
Write-Host "Extracting and processing frames"
$paddingFilter = ''
if ($padding -eq 'None') {
# Scale to fill intermediate height only; horizontal cropping will be applied per-frame
$paddingFilter = "scale=-2:${intermediateHeight}"
}
elseif ($videoInfo.Height -lt $intermediateHeight) {
$padHeight = $intermediateHeight - $videoInfo.Height
switch ($padding) {
'Top' { $paddingFilter = "pad=$($videoInfo.Width):${intermediateHeight}:0:${padHeight}:${paddingColor}" }
'Bottom' { $paddingFilter = "pad=$($videoInfo.Width):${intermediateHeight}:0:0:${paddingColor}" }
'Center' {
$padTop = [int]($padHeight / 2)
$paddingFilter = "pad=$($videoInfo.Width):${intermediateHeight}:0:${padTop}:${paddingColor}"
}
}
}
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "ShortClip_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir | Out-Null
try {
$extractPattern = Join-Path $tempDir "frame-%04d.png"
if ($PSCmdlet.ShouldProcess($tempDir, 'Extract frames with padding to temporary directory')) {
Write-Host "Extracting frames with padding to temporary directory"
if ($paddingFilter -ne '') {
ffmpeg -i $Video -vf $paddingFilter $extractPattern -y -loglevel error
}
else {
ffmpeg -i $Video $extractPattern -y -loglevel error
}
if ($LASTEXITCODE -ne 0) {
throw "ffmpeg failed to extract frames with exit code $LASTEXITCODE"
}
$extractedFrames = Get-ChildItem -Path $tempDir -Filter "frame-*.png" | Sort-Object Name
if ($extractedFrames.Count -eq 0) {
throw "No frames were extracted"
}
Write-Host "Applying cropping to $($extractedFrames.Count) frames"
$imagesPattern = $Images -replace '%\d+d', '*'
$existingOutputFrames = Get-ChildItem -Path $imagesPattern -ErrorAction SilentlyContinue
if (($existingOutputFrames) -and ($PSCmdlet.ShouldProcess($imagesPattern, 'Remove existing output frames'))) {
Write-Host "Removing existing output frames"
$existingOutputFrames | Remove-Item -Force
}
for ($i = 0; $i -lt $extractedFrames.Count; $i++) {
$frameFile = $extractedFrames[$i]
$frameNumber = $i
$cropOffset = $croppingOffsets[$frameNumber]
$outputFramePath = $Images -replace '%04d', $frameNumber.ToString('0000')
if ($PSCmdlet.ShouldProcess($outputFramePath, 'Crop and save frame')) {
# Crop at intermediate resolution, then scale down if needed
if ($needsDownscale) {
if ($overlay -ne '') {
# Crop, scale, then overlay
$cropFilter = "[0:v]crop=${intermediateWidth}:${intermediateHeight}:${cropOffset}:0,scale=${outputWidth}:${outputHeight}[scaled];[scaled][1:v]overlay=0:0"
ffmpeg -i $frameFile.FullName -i $overlay -filter_complex $cropFilter $outputFramePath -y -loglevel error
}
else {
$cropFilter = "crop=${intermediateWidth}:${intermediateHeight}:${cropOffset}:0,scale=${outputWidth}:${outputHeight}"
ffmpeg -i $frameFile.FullName -vf $cropFilter $outputFramePath -y -loglevel error
}
}
else {
if ($overlay -ne '') {
# Use overlay as second input to avoid path escaping issues
$cropFilter = "[0:v]crop=${outputWidth}:${outputHeight}:${cropOffset}:0[cropped];[cropped][1:v]overlay=0:0"
ffmpeg -i $frameFile.FullName -i $overlay -filter_complex $cropFilter $outputFramePath -y -loglevel error
}
else {
$cropFilter = "crop=${outputWidth}:${outputHeight}:${cropOffset}:0"
ffmpeg -i $frameFile.FullName -vf $cropFilter $outputFramePath -y -loglevel error
}
}
if ($LASTEXITCODE -ne 0) {
throw "ffmpeg failed to crop frame $frameNumber with exit code $LASTEXITCODE"
}
}
if (($i + 1) % 10 -eq 0 -or ($i + 1) -eq $extractedFrames.Count) {
Write-Host "Processed $($i + 1) / $($extractedFrames.Count) frames"
}
}
}
}
finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force
Write-Host "Temporary directory removed: $tempDir"
}
}
}
Write-Host "Creating output video file"
$videoExt = [System.IO.Path]::GetExtension($Video)
$videoBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Video)
$videoDir = [System.IO.Path]::GetDirectoryName($Video)
if ($videoDir -eq '') { $videoDir = '.' }
$outputVideo = Join-Path $videoDir "${videoBaseName}-ShortClip${videoExt}"
$inputPattern = $Images
$videoCodec = if ($videoInfo.Codec -eq 'h264') { 'libx264' } elseif ($videoInfo.Codec -eq 'hevc') { 'libx265' } else { 'libx264' }
$pixFmt = $videoInfo.PixelFormat
$encodingArgs = @('-c:v', $videoCodec, '-pix_fmt', $pixFmt, '-preset', 'slow')
if ($videoInfo.Bitrate) {
$encodingArgs += @('-b:v', $videoInfo.Bitrate)
}
else {
$encodingArgs += @('-crf', '8')
}
if ($PSCmdlet.ShouldProcess($outputVideo, 'Create output video file')) {
if ($audio -eq 'Yes') {
Write-Host "Creating video with audio"
$mp4CompatibleAudioCodecs = @('aac', 'ac3', 'eac3', 'mp3')
if ($videoInfo.AudioCodec -and $mp4CompatibleAudioCodecs -contains $videoInfo.AudioCodec) {
$audioArgs = @('-c:a', 'copy')
}
elseif ($videoInfo.AudioBitrate) {
$audioBitrateK = [Math]::Max(320, [int]($videoInfo.AudioBitrate / 1000))
$audioArgs = @('-c:a', 'aac', '-b:a', "${audioBitrateK}k")
}
else {
$audioArgs = @('-c:a', 'aac', '-b:a', '320k')
}
Write-Host "Audio args: $($audioArgs -join ' ')"
ffmpeg -framerate $videoInfo.FPS -i $inputPattern -i $Video -map 0:v:0 -map 1:a:0? @encodingArgs @audioArgs $outputVideo -y
if ($LASTEXITCODE -ne 0) {
throw "ffmpeg failed to create output video with exit code $LASTEXITCODE"
}
}
else {
Write-Host "Creating video without audio"
ffmpeg -framerate $videoInfo.FPS -i $inputPattern @encodingArgs $outputVideo -y
if ($LASTEXITCODE -ne 0) {
throw "ffmpeg failed to create output video with exit code $LASTEXITCODE"
}
}
if (-not (Test-Path $outputVideo)) {
throw "Output video file was not created: $outputVideo"
}
Write-Host "Output video created: $outputVideo" -ForegroundColor Green
}
# Clean up frames directory after video is successfully created
if ((-not $Keep) -and (Test-Path $framesDir) -and ($PSCmdlet.ShouldProcess($framesDir, 'Remove frames directory'))) {
Remove-Item -Path $framesDir -Recurse -Force
Write-Host "Frames directory removed: $framesDir"
}
elseif ($Keep -and (Test-Path $framesDir)) {
Write-Host "Frames directory preserved: $framesDir"
}
}
catch {
Write-Error "Error: $_"
exit 1
}