Add -Frames parameter to Clone-Video.ps1 for flexible frame range specification supporting start-end ranges, positive numbers for 0-to-N, negative numbers for last N frames, % placeholder for last frame index, and 'all' keyword, with mutual exclusivity enforcement against -StartFrame, -EndFrame, and -Sequence parameters in both video and PNG sequence modes

This commit is contained in:
2026-07-17 20:30:47 -04:00
parent db71ce09a0
commit 56ddfc4371
+55 -1
View File
@@ -3,6 +3,7 @@
Clones (copies) a video file using ffmpeg. 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. 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. 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 Frames is provided, it defines a frame range using a flexible format and is mutually exclusive with StartFrame, EndFrame and Sequence.
When Sequence is provided and EndFrame is -1: 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). - 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). - After, the video is cloned (copied) from StartFrame to the second-to-last frame (excluding the last frame).
@@ -13,7 +14,7 @@
from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified). from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified).
- -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, -Reverse and -LTX are all honoured. - -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse and -LTX are all honoured.
.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.
@@ -24,6 +25,13 @@
0-based frame index to start cloning from. 0-based frame index to start cloning from.
.PARAMETER EndFrame .PARAMETER EndFrame
0-based frame index to end cloning at. Use -1 to clone to the end (default). 0-based frame index to end cloning at. Use -1 to clone to the end (default).
.PARAMETER Frames
0-based frames index to extract. Supports the following formats:
- start-end: Extract frames from start to end (e.g., "100-200")
- positive_number: Extract frames from 0 to that number (e.g., "50")
- -negative_number: Extract last N frames (e.g., "-10")
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%", "-%")
- all: Extract all frames (same as "0-%")
.PARAMETER Sequence .PARAMETER Sequence
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
@@ -79,6 +87,9 @@ param(
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[int]$EndFrame = -1, [int]$EndFrame = -1,
[Parameter(Mandatory = $false)]
[string]$Frames = $null,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[string]$Sequence = $null, [string]$Sequence = $null,
@@ -191,6 +202,13 @@ if ($NoAudio -and -not [string]::IsNullOrWhiteSpace($Audio)) {
exit 1 exit 1
} }
if (-not [string]::IsNullOrWhiteSpace($Frames)) {
if ($PSBoundParameters.ContainsKey('StartFrame') -or $PSBoundParameters.ContainsKey('EndFrame') -or -not [string]::IsNullOrWhiteSpace($Sequence)) {
Write-Error '-Frames cannot be used together with -StartFrame, -EndFrame, or -Sequence'
exit 1
}
}
$AudioPath = $null $AudioPath = $null
$externalAudioCodec = '' $externalAudioCodec = ''
$externalAudioCodecMismatch = $false $externalAudioCodecMismatch = $false
@@ -266,6 +284,24 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
$maxPngIndex = $pngFiles.Count - 1 $maxPngIndex = $pngFiles.Count - 1
if (-not [string]::IsNullOrWhiteSpace($Frames)) {
$framesPattern = $Frames -replace '%', $maxPngIndex
if ($framesPattern -ieq 'all') { $framesPattern = "0-$maxPngIndex" }
if ($framesPattern -match '^(\d+)-(\d+)$') {
$StartFrame = [int]$Matches[1]
$EndFrame = [int]$Matches[2]
} elseif ($framesPattern -match '^-(\d+)$') {
$StartFrame = [Math]::Max(0, $maxPngIndex - [int]$Matches[1])
$EndFrame = $maxPngIndex
} elseif ($framesPattern -match '^\d+$') {
$StartFrame = 0
$EndFrame = [Math]::Min([int]$framesPattern, $maxPngIndex)
} else {
Write-Error ('Invalid Frames format: {0}. Expected: start-end, positive number, -N for last N frames, % for last frame, or "all"' -f $Frames)
exit 1
}
}
if ($StartFrame -lt 0 -or $StartFrame -gt $maxPngIndex) { if ($StartFrame -lt 0 -or $StartFrame -gt $maxPngIndex) {
Write-Error ('StartFrame {0} is out of range. Valid range: 0 to {1}' -f $StartFrame, $maxPngIndex) Write-Error ('StartFrame {0} is out of range. Valid range: 0 to {1}' -f $StartFrame, $maxPngIndex)
exit 1 exit 1
@@ -482,6 +518,24 @@ if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
$totalFramesInt = [int]$totalFrames $totalFramesInt = [int]$totalFrames
$maxFrameIndex = $totalFramesInt - 1 $maxFrameIndex = $totalFramesInt - 1
if (-not [string]::IsNullOrWhiteSpace($Frames)) {
$framesPattern = $Frames -replace '%', $maxFrameIndex
if ($framesPattern -ieq 'all') { $framesPattern = "0-$maxFrameIndex" }
if ($framesPattern -match '^(\d+)-(\d+)$') {
$StartFrame = [int]$Matches[1]
$EndFrame = [int]$Matches[2]
} elseif ($framesPattern -match '^-(\d+)$') {
$StartFrame = [Math]::Max(0, $maxFrameIndex - [int]$Matches[1])
$EndFrame = $maxFrameIndex
} elseif ($framesPattern -match '^\d+$') {
$StartFrame = 0
$EndFrame = [Math]::Min([int]$framesPattern, $maxFrameIndex)
} else {
Write-Error ('Invalid Frames format: {0}. Expected: start-end, positive number, -N for last N frames, % for last frame, or "all"' -f $Frames)
exit 1
}
}
# Probe source video dimensions (used by -LTX to pick the closest aspect-ratio bucket). # Probe source video dimensions (used by -LTX to pick the closest aspect-ratio bucket).
$ffprobeDimOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x -- $SourcePath 2>&1 $ffprobeDimOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x -- $SourcePath 2>&1
$sourceWidth = 0 $sourceWidth = 0