Skip to content

Instantly share code, notes, and snippets.

@omarsaad98
Last active December 26, 2024 03:29
Show Gist options
  • Save omarsaad98/9a542b6102c5e07302af96153388f08d to your computer and use it in GitHub Desktop.
Save omarsaad98/9a542b6102c5e07302af96153388f08d to your computer and use it in GitHub Desktop.
# This PowerShell script performs a 2-pass encode on a video using ffmpeg.
# The video is trimmed to the given start and end times, and the output file size is targeted to 8 MiB.
param(
[string]$InputFile,
[string]$StartTime,
[string]$EndTime,
[string]$OutputFile
)
# Constants
$TargetSizeBytes = 8 * 1024 * 1024 # 8 MiB
$AudioBitrate = 96000 # 96 kbps
$SizeMargin = 0.2 * 1024 * 1024 # 0.2 MiB
# Ensure ffmpeg is available
if (-not (Get-Command "ffmpeg" -ErrorAction SilentlyContinue)) {
Write-Error "ffmpeg is not installed or not in the PATH."; exit 1
}
# Ensure ffprobe is available
if (-not (Get-Command "ffprobe" -ErrorAction SilentlyContinue)) {
Write-Error "ffprobe is not installed or not in the PATH."; exit 1
}
# Run ffprobe to get video and audio durations (used for bitrate calculation)
Write-Host "Calculating video duration..."
ffprobe -i $InputFile -show_entries format=duration -v quiet -of csv="p=0" 2>&1 | Tee-Object -Variable VideoInfo
if ($VideoInfo -match "^(\d+\.?\d*)$") {
$Duration = [decimal]$matches[1]
} else {
Write-Error "Unable to determine video duration."; exit 1
}
# Get source video FPS
Write-Host "Detecting source video FPS..."
ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 $InputFile 2>&1 | Tee-Object -Variable FPSInfo
if ($FPSInfo -match "(\d+)/(\d+)") {
$SourceFPS = [math]::Round([decimal]$matches[1] / [decimal]$matches[2], 3)
Write-Host "Source video FPS: $SourceFPS"
} else {
Write-Error "Unable to determine source video FPS."; exit 1
}
# Construct FPS filter based on source FPS
$FPSFilter = if ($SourceFPS -gt 60) { ",fps=60" } else { "" }
$StartSeconds = [timespan]::Parse($StartTime).TotalSeconds
$EndSeconds = [timespan]::Parse($EndTime).TotalSeconds
if ($EndSeconds -lt 0 -or $EndSeconds -gt $Duration -or $EndSeconds -le $StartSeconds) {
Write-Error "Invalid end time. Ensure StartTime and EndTime are correct and within the video duration."; exit 1
}
$TrimDuration = $EndSeconds - $StartSeconds
# Calculate video bitrate for first pass
$AudioSize = $TrimDuration * ($AudioBitrate / 8)
$VideoSize = $TargetSizeBytes - $AudioSize - $SizeMargin
if ($VideoSize -lt 0) {
Write-Error "Target size is too small for the given audio bitrate and duration."; exit 1
}
$VideoBitrate = [math]::Floor($VideoSize * 8 / $TrimDuration)
Write-Host "Target video bitrate: $VideoBitrate"
Write-Host "Target size in MiB: $([math]::Round(($VideoSize + $AudioSize) / 1024 / 1024, 2))"
# Determine maximum resolution
if ($TrimDuration -gt 10) {
$MaxWidth = 1280
$MaxHeight = 720
} else {
$MaxWidth = 1920
$MaxHeight = 1080
}
# First Pass
Write-Host "Running first pass..."
ffmpeg -hide_banner -y -i $InputFile -ss $StartTime -to $EndTime -vf "scale=w=min(iw\,$MaxWidth):h=min(ih\,$MaxHeight):force_original_aspect_ratio=decrease$FPSFilter" -an -c:v libx264 -b:v ${VideoBitrate} -preset veryslow -f null NUL 2>&1 | Tee-Object -Variable FirstPassOutput
if ($LastExitCode -ne 0) { Write-Error "First pass failed."; exit 1 }
# Parse final ratefactor from ffmpeg output
$AverageCRF = $FirstPassOutput | Select-String -Pattern "final ratefactor: (\d+\.?\d*)" | ForEach-Object { [double]$_.Matches.Groups[1].Value }
if (-not $AverageCRF) { Write-Error "Unable to parse average CRF from pass log."; exit 1 }
Write-Host "Average CRF: $AverageCRF"
$AudioTracks = (ffprobe -v error -select_streams a -show_entries stream=index -of csv=p=0 "$InputFile" | Measure-Object -Line).Lines
if ($AudioTracks -lt 1) { Write-Error "No audio tracks found in input file"; exit 1 }
# Second Pass
Write-Host "Running second pass..."
ffmpeg -hide_banner -y -i $InputFile -ss $StartTime -to $EndTime -filter_complex "[0:a]amix=inputs=$AudioTracks[aout]" -vf "scale=w=min(iw\,$MaxWidth):h=min(ih\,$MaxHeight):force_original_aspect_ratio=decrease$FPSFilter" -map 0:v:0 -map "[aout]" -c:v libx264 -preset veryslow -crf $AverageCRF -c:a aac -b:a ${AudioBitrate} $OutputFile
if ($LastExitCode -ne 0) { Write-Error "Second pass failed."; exit 1 }
Write-Host "Encoding completed successfully. Output saved to $OutputFile."
# Check output file size compared to the target size
$OutputFileSize = (Get-Item $OutputFile).Length
$OutputFileSizeMB = [math]::Round($OutputFileSize / 1024 / 1024, 2)
Write-Host "Output file size: $OutputFileSizeMB MiB"
# Show the resulting file in file explorer
explorer.exe "/select,$OutputFile"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment