diff --git a/Clone-Video.ps1 b/Clone-Video.ps1 index 5e7aebc..73a322d 100644 --- a/Clone-Video.ps1 +++ b/Clone-Video.ps1 @@ -33,6 +33,11 @@ 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 When present, copies available JSON metadata from the source video file to the target video file. +.PARAMETER Reverse + When present, re-encodes the video so it plays backward (from the last selected frame to the first). + Honours all other parameters (StartFrame/EndFrame range, Size, FPS, CRF, container choice, etc.). + Audio is NOT reversed; it is processed exactly as it would be without -Reverse (trimmed/replaced/copied as applicable) and plays forward alongside the reversed video. + Forces a video re-encode, since the reverse filter cannot be applied to a stream-copied video. .DESCRIPTION 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. @@ -71,7 +76,10 @@ param( [int]$CRF = -1, [Parameter(Mandatory = $false)] - [switch]$Metadata + [switch]$Metadata, + + [Parameter(Mandatory = $false)] + [switch]$Reverse ) Set-StrictMode -Version Latest @@ -173,7 +181,10 @@ if (-not [string]::IsNullOrWhiteSpace($Audio)) { # rather than stream-copied. $ffprobeExtAudioOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $AudioPath 2>&1 if ($LASTEXITCODE -eq 0) { - $externalAudioCodec = ($ffprobeExtAudioOutput | Select-Object -First 1).Trim().ToLowerInvariant() + $extAudioLine = $ffprobeExtAudioOutput | Select-Object -First 1 + if ($extAudioLine) { + $externalAudioCodec = ([string]$extAudioLine).Trim().ToLowerInvariant() + } } } @@ -215,7 +226,8 @@ if ($ffprobeExit -ne 0) { exit 1 } -$fpsRatio = ($ffprobeFpsOutput | Select-Object -First 1).Trim() +$fpsRatioLine = $ffprobeFpsOutput | Select-Object -First 1 +$fpsRatio = if ($fpsRatioLine) { ([string]$fpsRatioLine).Trim() } else { '' } if (-not $fpsRatio -or $fpsRatio -notmatch '^\d+/\d+$') { Write-Error ('Could not determine frame rate for: {0}' -f $SourcePath) @@ -235,7 +247,8 @@ if ($ffprobeExit -ne 0) { exit 1 } -$totalFrames = ($ffprobeFramesOutput | Select-Object -First 1).Trim() +$totalFramesLine = $ffprobeFramesOutput | Select-Object -First 1 +$totalFrames = if ($totalFramesLine) { ([string]$totalFramesLine).Trim() } else { '' } if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') { Write-Error ('Could not determine frame count for: {0}' -f $SourcePath) @@ -250,9 +263,12 @@ $ffprobeExit = $LASTEXITCODE $videoBitrate = $null if ($ffprobeExit -eq 0) { - $bitrateValue = ($ffprobeBitrateOutput | Select-Object -First 1).Trim() - if ($bitrateValue -and $bitrateValue -match '^\d+$') { - $videoBitrate = $bitrateValue + $bitrateLine = $ffprobeBitrateOutput | Select-Object -First 1 + if ($bitrateLine) { + $bitrateValue = ([string]$bitrateLine).Trim() + if ($bitrateValue -and $bitrateValue -match '^\d+$') { + $videoBitrate = $bitrateValue + } } } @@ -262,13 +278,19 @@ if ($ffprobeExit -eq 0) { $ffprobeVideoCodecOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1 $sourceVideoCodec = '' if ($LASTEXITCODE -eq 0) { - $sourceVideoCodec = ($ffprobeVideoCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant() + $vCodecLine = $ffprobeVideoCodecOutput | Select-Object -First 1 + if ($vCodecLine) { + $sourceVideoCodec = ([string]$vCodecLine).Trim().ToLowerInvariant() + } } $ffprobeAudioCodecOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1 $sourceAudioCodec = '' if ($LASTEXITCODE -eq 0) { - $sourceAudioCodec = ($ffprobeAudioCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant() + $aCodecLine = $ffprobeAudioCodecOutput | Select-Object -First 1 + if ($aCodecLine) { + $sourceAudioCodec = ([string]$aCodecLine).Trim().ToLowerInvariant() + } } # A source codec not on the target container's stream-copy list forces a re-encode @@ -364,6 +386,9 @@ if ($scaleWidth -gt 0) { if ($FPS -gt 0) { Write-Host (' FPS: {0}' -f $FPS) } +if ($Reverse) { + Write-Host ' Reverse: video reversed, audio unchanged' +} Write-Host (' Target: {0}' -f $TargetPath) $ffmpegArgs = @( @@ -375,7 +400,7 @@ if ($AudioPath) { $ffmpegArgs += @('-i', $AudioPath) } -$needsReencode = $scaleWidth -gt 0 -or $FPS -gt 0 -or $videoCodecMismatch +$needsReencode = $scaleWidth -gt 0 -or $FPS -gt 0 -or $videoCodecMismatch -or $Reverse if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) { $filterParts = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame) @@ -386,6 +411,9 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) { if ($FPS -gt 0) { $filterParts += ('fps={0}' -f $FPS) } + if ($Reverse) { + $filterParts += 'reverse' + } $videoFilter = $filterParts -join ',' $ffmpegArgs += @( '-vf', $videoFilter @@ -410,6 +438,9 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) { if ($FPS -gt 0) { $filterParts += ('fps={0}' -f $FPS) } + if ($Reverse) { + $filterParts += 'reverse' + } if ($filterParts.Count -gt 0) { $videoFilter = $filterParts -join ',' $ffmpegArgs += @('-vf', $videoFilter) diff --git a/Crop-ClipsWan.ps1 b/Crop-ClipsWan.ps1 index 6db0962..3cfab94 100644 --- a/Crop-ClipsWan.ps1 +++ b/Crop-ClipsWan.ps1 @@ -6,7 +6,7 @@ .PARAMETER Target Target directory for cropped video files. .DESCRIPTION - Crops video clips from 1928x1076 or 1926x1076 to 1920x1080 using ffmpeg. + Crops video clips from 1928x1076 or 1926x1076 to 1920x1080, or from 1284x716 to 1280x720 using ffmpeg. Supports -WhatIf and -Confirm for safe execution. #> [CmdletBinding(SupportsShouldProcess)] @@ -113,7 +113,8 @@ foreach ($video in $videos) { $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } $isValidResolution = ($inInfo.Width -eq 1928 -and $inInfo.Height -eq 1076) -or - ($inInfo.Width -eq 1926 -and $inInfo.Height -eq 1076) + ($inInfo.Width -eq 1926 -and $inInfo.Height -eq 1076) -or + ($inInfo.Width -eq 1284 -and $inInfo.Height -eq 716) if (-not $isValidResolution) { Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)" @@ -132,10 +133,20 @@ foreach ($video in $videos) { $outCropPath = Join-Path $targetFull ($video.BaseName + '.crop.mp4') $outFinalPath = Join-Path $targetFull $video.Name + # Determine crop parameters based on input resolution + if ($inInfo.Width -eq 1284 -and $inInfo.Height -eq 716) { + # 1284x716: expand to 720 height, then crop to 1280x720 + $videoFilter = 'scale=-2:720,crop=1280:720:(in_w-1280)/2:(in_h-720)/2' + } + else { + # 1928x1076 or 1926x1076: expand to 1080 height, then crop to 1920x1080 + $videoFilter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' + } + $ffArgs = @( '-y', '-i', $inputPath, - '-vf', 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' + '-vf', $videoFilter ) if ($inInfo.HasAudio) {