# ================= CONFIG ================= $SourcePath = "" $LogPath = "C:\ffmpeg_encode.log" $CQ = 24 $Preset = "slow" $DryRun = $false # ========================================= $videoExts = ".mp4",".mkv",".avi",".mov",".ts",".m2ts",".wmv",".flv",".webm" if (!(Test-Path $LogPath)) { New-Item -ItemType File -Path $LogPath | Out-Null } function Write-Log { param ([string]$Message) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" "$timestamp - $Message" | Tee-Object -FilePath $LogPath -Append } function Format-Time { param ([double]$Seconds) if ($Seconds -lt 0) { return "00:00:00" } [TimeSpan]::FromSeconds([math]::Round($Seconds)).ToString("hh\:mm\:ss") } Write-Log "==== FFmpeg H.265 batch encode started ====" $files = Get-ChildItem $SourcePath -Recurse -File | Where-Object { $_.Extension -in $videoExts } $total = $files.Count $counter = 0 $totalSaved = 0 # Stats $encoded = 0 $remuxed = 0 $skipped = 0 $failed = 0 $encodeTimes = @() foreach ($file in $files) { $counter++ Write-Progress -Id 1 ` -Activity "Overall Progress" ` -Status "File $counter of $total" $input = $file.FullName $output = Join-Path $file.DirectoryName ($file.BaseName + ".mkv") $tmp = Join-Path $file.DirectoryName ($file.BaseName + ".tmp.mkv") $codec = & ffprobe -v error -select_streams v:0 ` -show_entries stream=codec_name ` -of default=noprint_wrappers=1:nokey=1 "$input" # Skip H.265 MKV if (($codec -eq "hevc") -and ($file.Extension -eq ".mkv")) { Write-Progress -Id 2 -ParentId 1 ` -Activity "Current File" ` -Status "SKIPPED (H.265 MKV): $($file.Name)" ` -PercentComplete 100 Write-Log "SKIPPED: $input" $skipped++ continue } if ($DryRun) { Write-Log "DRY-RUN: $input -> $output" continue } $origSize = (Get-Item $input).Length $duration = & ffprobe -v error -show_entries format=duration ` -of default=noprint_wrappers=1:nokey=1 "$input" $duration = [double]$duration $startTime = Get-Date $args = "-y -i `"$input`" -c:v hevc_nvenc -preset $Preset -rc vbr_hq -cq $CQ -c:a copy `"$tmp`" -progress pipe:1 -nostats" $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = "ffmpeg" $psi.Arguments = $args $psi.RedirectStandardOutput = $true $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $p = New-Object System.Diagnostics.Process $p.StartInfo = $psi $p.Start() | Out-Null while (-not $p.HasExited) { $line = $p.StandardOutput.ReadLine() if ($line -match "^out_time_ms=(\d+)$") { $doneSec = $matches[1] / 1000000 $percent = [math]::Min([math]::Round(($doneSec / $duration) * 100), 100) $elapsed = (Get-Date) - $startTime $rate = if ($doneSec -gt 0) { $elapsed.TotalSeconds / $doneSec } else { 0 } $etaFile = ($duration - $doneSec) * $rate $avgEncode = if ($encodeTimes.Count -gt 0) { ($encodeTimes | Measure-Object -Average).Average } else { 0 } $etaQueue = ($total - $counter) * $avgEncode Write-Progress -Id 2 -ParentId 1 ` -Activity "Current File" ` -Status "Encoding: $($file.Name) ($percent%) | File ETA: $(Format-Time $etaFile) | Queue ETA: $(Format-Time $etaQueue)" ` -PercentComplete $percent } } if (Test-Path $tmp) { try { Remove-Item $input -Force Rename-Item $tmp $output -Force $newSize = (Get-Item $output).Length $saved = $origSize - $newSize $totalSaved += $saved $encodeTime = ((Get-Date) - $startTime).TotalSeconds $encodeTimes += $encodeTime $encoded++ Write-Log "ENCODED: $input | Saved $([math]::Round($saved/1MB,2)) MB" } catch { Write-Log "FAILED TO REPLACE: $input" $failed++ } } else { Write-Log "FAILED: $input" $failed++ } } Write-Progress -Id 2 -Completed Write-Progress -Id 1 -Completed $totalSavedMB = [math]::Round($totalSaved / 1MB, 2) Write-Host "" Write-Host "===== SUMMARY =====" Write-Host "Total files : $total" Write-Host "Encoded : $encoded" Write-Host "Skipped : $skipped" Write-Host "Failed : $failed" Write-Host "Space saved : $totalSavedMB MB" Write-Host "===================" Write-Log "==== FFmpeg H.265 batch encode finished ===="