Re-encoding movies in Powershell with ffmpeg
Lately I have collected some media that uses the AV1 codec. It’s a great codec but my TV’s piddly mediaplayer just can’t cope. I need to re-encode these files to something the TV can handle, for example x265.
I have plenty of Linux servers, even my NAS runs Linux, but my gaming PC has all the power; beefy CPU and fat GPU, so it would be nice if I can do it on there. It does run on Windows though. I have my NAS mapped as a drive via CIFS, so let’s open Powershell.
The first thing we do is check the version we’re running.
$PSVersionTable
If you’re running the ancient 5.1 version from August 2016, you’re going to have to update. Luckily it’s easy and 5.1 will still be there untouched if you need it.
winget install --id Microsoft.PowerShell --source winget
After the installation is complete, you can start it by running: pwsh.exe
If you are like me and use [Win]+X -> ‘Terminal’, then updating this is easy too. When the old version Powershell pops up:
- Select the Down arrow in the title bar
- Settings -> Default profile -> PowerShell -> Save
- Close the window and restart a terminal window
If you use the ‘$PSVersionTable’ again, you’ll see we’re in version 7.5.3 (at the time of this writing). We still need a few more things. For one, ffmpeg, which is most easy to get via the package manager Chocolatey. To use that, open an admin Powershell and run:
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
Followed by:
choco install ffmpeg -y
Now we can run our script. Copy the following code into a file called ‘convert.ps1’ in the directory that contains the mp4 files you wish to convert. (Note that you can run everything by hand by copy and pasting into the Powershell terminal. This way you can have a look in the intermediate files and learn how it works.) (As a cautionary note, two files called ‘1’ and ‘2’ will be created in the current directory and removed afterwards. Make sure you don’t have those files in your directory already if you’re attached to them.)
# Get current directory path
$pwdPath = (Get-Location).ProviderPath
# Make list of all .mp4 files with: videocodec width height relative_path_and_filename
Get-ChildItem -Path . -Filter *.mp4 -Recurse | ForEach-Object {
$relativePath = $_.FullName.Substring($pwdPath.Length + 1)
$p = (ffprobe `
-loglevel quiet `
-hide_banner `
-select_streams v:0 `
-of default=noprint_wrappers=1:nokey=1 `
-show_entries stream=codec_name,width,height `
".\$relativePath" | Join-String -Separator "`t")
$p = $p + "`t" + $relativePath
Write-Output $p
} > 1
# Filter the filelist
Get-Content "1" | ForEach-Object {
# Split each line by tab
$fields = $_ -split "`t"
# Apply the condition
if ($fields[0] -eq "av1" -or [int]$fields[1] -gt 1920 -or [int]$fields[2] -gt 1080) {
# Output the fourth field
Write-Output $fields[3]
}
} > 2
# Loop through each line (file path) in the list
Get-Content "2" | ForEach-Object {
$fullPath = $_.Trim() # full path from file
if(-not [string]::IsNullOrWhiteSpace($fullPath)) {
$TargetDir = Split-Path $fullPath -Parent
$TargetDir = ".`\converted`\" + $TargetDir
New-Item -ItemType Directory -Force -Path $TargetDir
$fileName = [System.IO.Path]::GetFileNameWithoutExtension($fullPath)
$extension = [System.IO.Path]::GetExtension($fullPath)
Write-Output "Source: $fullPath"
Write-Output "Target Path: $TargetDir"
Write-Output "Filename: $fileName"
Write-Output "Extension: $extension"
Write-Output "---------------------------"
# AMD HW encoding. Goes from 1.4x SW to 10x speed with HW
ffmpeg `
-hwaccel auto `
-i "$fullPath" `
-map 0 `
-c:v hevc_amf `
-preset balanced `
-b:v 1M `
-vf "scale='if(gt(iw,1920),1920,if(gt(ih,1080),-2,iw))': `
'if(gt(ih,1080),1080,if(gt(iw,1920),-2,ih))'" `
-c:a aac `
-b:a 128k `
-y `
"$TargetDir\$fileName$extension"
}
}
# Clean up temp files
Remove-Item -Path "1" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "2" -Force -ErrorAction SilentlyContinue
Exit
# SW encoding
ffmpeg `
-hwaccel auto `
-i "$fullPath" `
-map 0 `
-c:v libx265 `
-crf 22 `
-preset slow `
-vf "scale='if(gt(iw,1920),1920,if(gt(ih,1080),-2,iw))': `
'if(gt(ih,1080),1080,if(gt(iw,1920),-2,ih))'" `
-c:a aac `
-b:a 128k `
-y `
"$TargetDir\$fileName$extension"
Running this will:
- Create a list of all mp4 files in the current and subsequent directories.
- Record the video codec and screen dimensions of each MP4 file.
- Then a filter is run which will create a list of files to convert.
- The main loop starts, converting files and placing them in an identical directory tree in the ‘converted’ directory.
- The temp files are deleted.
If the process is complete, it’s a simply question of moving the files and directories in the ‘converted’ directory back one directory, overwriting the originals.
Note that the script is set to the x265 codec using AMD HW acceleration. See below on how to choose another GPU, or use the SW encoding command at the bottom of the script in place of the AMD one.
NVIDIA (NVENC): -c:v h264_nvenc (H.264) -c:v hevc_nvenc (HEVC/H.265) Intel (Quick Sync / QSV): -c:v h264_qsv -c:v hevc_qsv AMD (AMF): -c:v h264_amf -c:v hevc_amf Apple (VideoToolbox): -c:v h264_videotoolbox -c:v hevc_videotoolbox
Note that you’ll need to tweak, add or remove some parameters if you pick another GPU option, but ChatGPT can tell you what to change.
In the end, feel free to use this script as a starting point for your own needs.