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.
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 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:
- 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).
@@ -13,7 +14,7 @@
from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified).
- -Sequence and -Metadata are not allowed in this mode.
- 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
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.
@@ -24,6 +25,13 @@
0-based frame index to start cloning from.
.PARAMETER EndFrame
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
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
@@ -79,6 +87,9 @@ param(
[Parameter(Mandatory = $false)]
[int]$EndFrame = -1,
[Parameter(Mandatory = $false)]
[string]$Frames = $null,
[Parameter(Mandatory = $false)]
[string]$Sequence = $null,
@@ -191,6 +202,13 @@ if ($NoAudio -and -not [string]::IsNullOrWhiteSpace($Audio)) {
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
$externalAudioCodec = ''
$externalAudioCodecMismatch = $false
@@ -266,6 +284,24 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
$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) {
Write-Error ('StartFrame {0} is out of range. Valid range: 0 to {1}' -f $StartFrame, $maxPngIndex)
exit 1
@@ -482,6 +518,24 @@ if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
$totalFramesInt = [int]$totalFrames
$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).
$ffprobeDimOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x -- $SourcePath 2>&1
$sourceWidth = 0