Add image-to-video mode to Clone-Video.ps1 for building videos from static images with audio duration matching, supporting .png, .jpg, .jpeg, .webp, .bmp, .tiff, .tif, and .gif formats with -Size, -FPS, -CRF, and -LTX parameters, using ffmpeg's stillimage tune for libx264 and -shortest flag for audio synchronization, while prohibiting -NoAudio and ignoring -StartFrame, -EndFrame, -Frames, -Sequence, -Metadata, and -Reverse in this mode
This commit is contained in:
+149
@@ -15,10 +15,16 @@
|
|||||||
- -Sequence and -Metadata are not allowed in this mode.
|
- -Sequence and -Metadata are not allowed in this mode.
|
||||||
- Defaults: 24 FPS, CRF 8, no audio (unless -Audio is provided).
|
- Defaults: 24 FPS, CRF 8, no audio (unless -Audio is provided).
|
||||||
- -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse and -LTX are all honoured.
|
- -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse and -LTX are all honoured.
|
||||||
|
When Source is a supported image file (.png, .jpg, .jpeg, .webp, .bmp, .tiff, .tif, .gif) and Audio is provided, the script builds a video of the static image for the duration of the audio file.
|
||||||
|
- -NoAudio is not allowed in this mode.
|
||||||
|
- Defaults: 24 FPS, CRF 8, source image dimensions.
|
||||||
|
- -Size, -FPS, -CRF and -LTX are honoured.
|
||||||
|
- -StartFrame, -EndFrame, -Frames, -Sequence, -Metadata and -Reverse are ignored.
|
||||||
.PARAMETER Source
|
.PARAMETER Source
|
||||||
Path to the input video file, or a directory containing PNG files with sequence numbers (e.g. frame_0000.png).
|
Path to the input video file, or a directory containing PNG files with sequence numbers (e.g. frame_0000.png).
|
||||||
For video files: can be absolute or relative to current directory. If the path has no extension, .mp4, .webm and .mkv are tried in turn.
|
For video files: can be absolute or relative to current directory. If the path has no extension, .mp4, .webm and .mkv are tried in turn.
|
||||||
For PNG sequence directories: the script builds a video from the PNG files sorted alphabetically; -Sequence and -Metadata are not allowed in this mode.
|
For PNG sequence directories: the script builds a video from the PNG files sorted alphabetically; -Sequence and -Metadata are not allowed in this mode.
|
||||||
|
For supported image files (.png, .jpg, .jpeg, .webp, .bmp, .tiff, .tif, .gif): when -Audio is provided, builds a video of the static image for the audio duration; -NoAudio is not allowed.
|
||||||
.PARAMETER Target
|
.PARAMETER Target
|
||||||
Path to the output video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
|
Path to the output video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
|
||||||
.PARAMETER StartFrame
|
.PARAMETER StartFrame
|
||||||
@@ -50,6 +56,7 @@
|
|||||||
Forces a video re-encode, since removing the audio stream requires re-muxing the output file.
|
Forces a video re-encode, since removing the audio stream requires re-muxing the output file.
|
||||||
.PARAMETER Audio
|
.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.
|
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.
|
||||||
|
When Source is a supported image file, Audio determines the output video duration.
|
||||||
.PARAMETER CRF
|
.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.
|
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.
|
||||||
.PARAMETER Metadata
|
.PARAMETER Metadata
|
||||||
@@ -71,6 +78,7 @@
|
|||||||
This script uses ffmpeg to clone (copy) a video file with optional frame range and audio exclusion.
|
This script uses ffmpeg to clone (copy) a video file with optional frame range and audio exclusion.
|
||||||
When Sequence is provided, it extracts a specific frame to PNG and then clones the video accordingly.
|
When Sequence is provided, it extracts a specific frame to PNG and then clones the video accordingly.
|
||||||
When Source is a directory, the script builds a video from the PNG image sequence found inside.
|
When Source is a directory, the script builds a video from the PNG image sequence found inside.
|
||||||
|
When Source is a supported image file and Audio is provided, builds a video of the static image for the audio duration.
|
||||||
Supports -WhatIf and -Confirm for safe execution.
|
Supports -WhatIf and -Confirm for safe execution.
|
||||||
#>
|
#>
|
||||||
[CmdletBinding(SupportsShouldProcess)]
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
@@ -265,6 +273,147 @@ if (-not [string]::IsNullOrWhiteSpace($Size)) {
|
|||||||
|
|
||||||
$PSNativeCommandUseErrorActionPreference = $false
|
$PSNativeCommandUseErrorActionPreference = $false
|
||||||
|
|
||||||
|
# Image-to-video mode: when $Source is a supported image file and $Audio is provided.
|
||||||
|
# Builds a video of the static image for the duration of the audio file.
|
||||||
|
$supportedImageExts = @('.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif', '.gif')
|
||||||
|
$sourceExt = [System.IO.Path]::GetExtension($SourcePath).ToLowerInvariant()
|
||||||
|
if (($supportedImageExts -contains $sourceExt) -and $AudioPath) {
|
||||||
|
if ($NoAudio) {
|
||||||
|
Write-Error '-NoAudio cannot be used in image-to-video mode'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgSrcWidth = 0
|
||||||
|
$imgSrcHeight = 0
|
||||||
|
$imgDimOut = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x -- $SourcePath 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
$imgDimLine = $imgDimOut | Select-Object -First 1
|
||||||
|
if ($imgDimLine -and ([string]$imgDimLine).Trim() -match '^(\d+)x(\d+)$') {
|
||||||
|
$imgSrcWidth = [int]$Matches[1]
|
||||||
|
$imgSrcHeight = [int]$Matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgFPS = if ($FPS -gt 0) { $FPS } else { 24 }
|
||||||
|
$imgCRF = if ($CRF -ge 0) { $CRF } else { 8 }
|
||||||
|
|
||||||
|
$imgOutWidth = $scaleWidth
|
||||||
|
$imgOutHeight = $scaleHeight
|
||||||
|
|
||||||
|
if ($standardSizeHeight -gt 0 -and -not $LTX -and $imgSrcWidth -gt 0 -and $imgSrcHeight -gt 0) {
|
||||||
|
if ($imgSrcWidth -ge $imgSrcHeight) {
|
||||||
|
$imgOutHeight = $standardSizeHeight
|
||||||
|
$imgOutWidth = [int][Math]::Round([double]$imgSrcWidth * $standardSizeHeight / $imgSrcHeight)
|
||||||
|
if ($imgOutWidth % 2) { $imgOutWidth++ }
|
||||||
|
} else {
|
||||||
|
$imgOutWidth = $standardSizeHeight
|
||||||
|
$imgOutHeight = [int][Math]::Round([double]$imgSrcHeight * $standardSizeHeight / $imgSrcWidth)
|
||||||
|
if ($imgOutHeight % 2) { $imgOutHeight++ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($LTX) {
|
||||||
|
$imgLtxKey = $Size
|
||||||
|
if ($imgLtxKey -eq 'HD') { $imgLtxKey = '1080p' }
|
||||||
|
$imgLtxMap = @{
|
||||||
|
'1080p' = @(
|
||||||
|
@{ W = 1920; H = 1088 }, @{ W = 1088; H = 1920 },
|
||||||
|
@{ W = 1664; H = 960 }, @{ W = 960; H = 1664 }
|
||||||
|
)
|
||||||
|
'720p' = @(
|
||||||
|
@{ W = 1280; H = 704 }, @{ W = 704; H = 1280 },
|
||||||
|
@{ W = 1088; H = 576 }, @{ W = 576; H = 1088 }
|
||||||
|
)
|
||||||
|
'480p' = @(
|
||||||
|
@{ W = 832; H = 448 }, @{ W = 448; H = 832 },
|
||||||
|
@{ W = 896; H = 512 }, @{ W = 512; H = 896 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (-not $imgLtxMap.ContainsKey($imgLtxKey)) {
|
||||||
|
Write-Error '-LTX requires -Size to be one of: 480p, 720p, 1080p, HD'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($imgSrcWidth -le 0 -or $imgSrcHeight -le 0) {
|
||||||
|
Write-Error 'Could not determine image dimensions; -LTX cannot pick an aspect-ratio bucket.'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$imgAR = [double]$imgSrcWidth / [double]$imgSrcHeight
|
||||||
|
$imgBest = $null
|
||||||
|
$imgBestDiff = [double]::MaxValue
|
||||||
|
foreach ($imgEntry in $imgLtxMap[$imgLtxKey]) {
|
||||||
|
$imgR = [double]$imgEntry.W / [double]$imgEntry.H
|
||||||
|
$imgD = [Math]::Abs($imgR - $imgAR)
|
||||||
|
if ($imgD -lt $imgBestDiff) { $imgBestDiff = $imgD; $imgBest = $imgEntry }
|
||||||
|
}
|
||||||
|
$imgOutWidth = $imgBest.W
|
||||||
|
$imgOutHeight = $imgBest.H
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($imgOutWidth -eq 0 -and $imgSrcWidth -gt 0) {
|
||||||
|
$imgOutWidth = $imgSrcWidth
|
||||||
|
$imgOutHeight = $imgSrcHeight
|
||||||
|
}
|
||||||
|
if ($imgOutWidth % 2) { $imgOutWidth++ }
|
||||||
|
if ($imgOutHeight % 2) { $imgOutHeight++ }
|
||||||
|
|
||||||
|
$imgFfmpegArgs = @(
|
||||||
|
'-y'
|
||||||
|
'-loop', '1'
|
||||||
|
'-i', $SourcePath
|
||||||
|
'-i', $AudioPath
|
||||||
|
'-r', $imgFPS.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($imgOutWidth -gt 0) {
|
||||||
|
$imgFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $imgOutWidth, $imgOutHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgFfmpegArgs += $targetVideoEncodeArgs
|
||||||
|
if ($targetVideoCodec -eq 'libx264') {
|
||||||
|
$imgFfmpegArgs += @('-tune', 'stillimage')
|
||||||
|
}
|
||||||
|
$imgFfmpegArgs += @('-crf', $imgCRF.ToString())
|
||||||
|
if ($targetVideoCodec -eq 'libvpx-vp9') { $imgFfmpegArgs += @('-b:v', '0') }
|
||||||
|
|
||||||
|
$imgExtAudioMismatch = ($externalAudioCodec -and ($audioCopyCodecs -notcontains $externalAudioCodec))
|
||||||
|
if ($imgExtAudioMismatch) {
|
||||||
|
$imgFfmpegArgs += @('-map', '0:v', '-map', '1:a', '-c:a', $targetAudioCodec, '-b:a', $targetAudioBitrate)
|
||||||
|
} else {
|
||||||
|
$imgFfmpegArgs += @('-map', '0:v', '-map', '1:a', '-c:a', 'copy')
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgFfmpegArgs += @('-shortest')
|
||||||
|
$imgFfmpegArgs += @('-map_metadata', '-1')
|
||||||
|
$imgFfmpegArgs += @('--', $TargetPath)
|
||||||
|
|
||||||
|
Write-Host ('Building video from image: {0}' -f $SourcePath)
|
||||||
|
Write-Host (' Target container: {0} (video={1})' -f $targetContainer, $targetVideoCodec)
|
||||||
|
Write-Host (' FPS: {0}' -f $imgFPS)
|
||||||
|
if ($imgOutWidth -gt 0) {
|
||||||
|
if ($LTX) {
|
||||||
|
Write-Host (' Size: {0}x{1} (LTX-2)' -f $imgOutWidth, $imgOutHeight)
|
||||||
|
} else {
|
||||||
|
Write-Host (' Size: {0}x{1}' -f $imgOutWidth, $imgOutHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host (' CRF: {0}' -f $imgCRF)
|
||||||
|
Write-Host (' Audio: {0}' -f $AudioPath)
|
||||||
|
Write-Host (' Target: {0}' -f $TargetPath)
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($TargetPath, 'Build video from image')) {
|
||||||
|
$imgOutput = & ffmpeg @imgFfmpegArgs 2>&1
|
||||||
|
$imgExit = $LASTEXITCODE
|
||||||
|
if ($imgExit -ne 0) {
|
||||||
|
Write-Host 'ffmpeg error output:' -ForegroundColor Red
|
||||||
|
$imgOutput | ForEach-Object { Write-Host $_ }
|
||||||
|
Write-Error ('ffmpeg failed to build video from image with exit code: {0}' -f $imgExit)
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host ('Video built successfully to: {0}' -f $TargetPath)
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# PNG image sequence mode: when $Source is a directory containing PNG files
|
# PNG image sequence mode: when $Source is a directory containing PNG files
|
||||||
if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
||||||
if (-not [string]::IsNullOrWhiteSpace($Sequence)) {
|
if (-not [string]::IsNullOrWhiteSpace($Sequence)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user