diff --git a/Clone-Video.ps1 b/Clone-Video.ps1 index e18937b..e3031dd 100644 --- a/Clone-Video.ps1 +++ b/Clone-Video.ps1 @@ -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