Over the years I’ve gathered a lot of videos from various sources. Some of these are uncompressed and take up various gigabytes of space, each! Even though storage is getting cheaper, I don’t really want to spend €200,- (each) for 16TB spinning disks, so the answer is High Efficiency Video Coding, or HEVC/H265.
Do note that the statement above will not hold up for an uncompressed backup of a UHD Blu-ray disk. There is absolutely no way that a 50GB+ copy will have the same quality as an 1GB compressed file. The statement above is about downloaded x264 content like Youtube clips, Web-Rips, etc.
I put a PowerShell script together below to do the following:
– Extract any .rar or .7z archives that may be in the input folder, prompting the user if they want to extract– Check which types of files are present
– Re-Encode to H265 through FFmpeg with hardware encoding (Nvidia GPU in my case)
– Rename the output file to replace the tag “h264”, “h.264” or “x264” by “x265”
– Log the results to a file
The script is doing some other bits and bobs, but these are the main. It’s #Free, so feel free to use, adapt, distribute as you see fit.
The pre-requisite software which must be installed on your system, is:
– FFMpeg (tested with ffmpeg version 2023-09-07 Gyan build)
– 7-Zip (Latest version)
[CmdletBinding()]
param(
[parameter(Mandatory=$False)][ValidateSet("mp4","mkv","ts","flv","avi","webm","wmv","mpg","m4v")]
[string]$InType,
[parameter(Mandatory=$False)][ValidateSet("slow","medium","fast")]
[string]$GPUPreset = "slow"
)
#Paths & Locations
$CurrentDirectory = Get-Location
$7Zip_path = "<put your path here>" #enter the location where 7zip is installed, default install location: C:\Program Files\7-Zip\7z.exe
$FFMpeg_Path = "<put your path here>" #enter the location where FFmpeg is installed, default install location: C:\Program Files\fmpeg\bin\ffmpeg.exe
$OutPut_Path = "X:\Output\" #enter the location of the folder where the converted file(s) should be written
$Logfile_Path = "X:\ConversionLogs\" #enter the location of the folder where the logs should be should be stored
####### Un-Rar and Un-Zip functions #######
# Check if there are RAR archives in the directory or subdirectories
$RarArchives = Get-ChildItem -Path $CurrentDirectory -Recurse -Filter "*.rar"
# Check if there are any RAR archives found
if ($RarArchives.Count -gt 0) {
# Prompt the user if they want to unrar the archives
$choice = Read-Host "RAR archives found. Do you want to unrar them (Y/N)?"
if ($choice -eq "Y" -or $choice -eq "y") {
foreach ($rarArchive in $RarArchives) {
# Unrar the multipart archive using 7-Zip with -y (answer yes) and -aoa (overwrite all)
$arguments = "x -y -aoa $($rarArchive.FullName)"
Start-Process -Wait -FilePath "$7Zip_path" -ArgumentList $arguments
}
Write-Host "INFO: Unrar process completed."
} else {
Write-Host "INFO: Skipping unrar process. Continuing..."
}
} else {
Write-Host "No RAR archives found in the directory or subdirectories." -ForegroundColor Black #I don't really care about this, hence it's black
}
# Check if there are 7z archives in the directory or subdirectories
$SevenZArchives = Get-ChildItem -Path $CurrentDirectory -Recurse -Filter "*.7z"
# Check if there are any 7z archives found
if ($SevenZArchives.Count -gt 0) {
# Prompt the user if they want to unzip the archives
$choice = Read-Host "7z archives found. Do you want to unzip them (Y/N)?"
if ($choice -eq "Y" -or $choice -eq "y") {
foreach ($sevenZArchive in $SevenZArchives) {
# Unzip the multipart 7z archive using 7-Zip with -y (answer yes) and -aoa (overwrite all)
$arguments = "x -y -aoa $($sevenZArchive.FullName)"
Start-Process -Wait -FilePath "$7Zip_path" -ArgumentList $arguments
}
Write-Host "INFO: Unzip process completed."
} else {
Write-Host "INFO: Skipping unzip process. Continuing..."
}
} else {
Write-Host "INFO: No 7z archives found in the directory or subdirectories." -ForegroundColor Black #I don't really care about this, hence it's black
}
####### Setup Conditions for Re-Encode #######
# Define the allowed file extensions without the leading period
$AllowedExtensions = @("mp4", "mkv", "ts", "flv", "avi", "webm", "wmv", "mpg", "m4v")
# Get all files in the current directory with allowed extensions
$FilesWithAllowedExtensions = Get-ChildItem -Path $CurrentDirectory -Recurse | Where-Object { $AllowedExtensions -contains $_.Extension.TrimStart('.').ToLower() }
# Check if there are no files with allowed extensions in the directory
if ($FilesWithAllowedExtensions.Count -eq 0) {
Write-Host "INFO: No files for Re-Encoding found in the current directory." -ForegroundColor Orange
# Prompt the user for a new path or an option to exit
$choice = Read-Host "Do you want to specify a different path (Y/N)?"
if ($choice -eq "Y" -or $choice -eq "y") {
$NewPath = Read-Host "Enter the new path to search for files"
$FilesWithAllowedExtensions = Get-ChildItem -Path $NewPath | Where-Object { $AllowedExtensions -contains $_.Extension.TrimStart('.').ToLower() }
if ($FilesWithAllowedExtensions.Count -eq 0) {
Write-Host "INFO: No files for Re-Encoding found in the current directory. Exiting"
exit
}
} else {
Write-Host "INFO: Exiting..."
exit
}
}
# Group files by their extension and count the occurrences
$ExtensionCounts = $FilesWithAllowedExtensions | Group-Object Extension | ForEach-Object { $_.Name.TrimStart('.').ToLower(), $_.Count }
# Check if there is only one allowed extension present
if ($ExtensionCounts.Count -eq 1) {
$InType = $ExtensionCounts[0]
} else {
# Display a choice for multiple allowed extensions
Write-Host "Multiple allowed extensions found:"
for ($i = 0; $i -lt $ExtensionCounts.Count; $i += 2) {
Write-Host "$($i/2 + 1). $($ExtensionCounts[$i]) ($($ExtensionCounts[$i + 1]))"
}
$choice = Read-Host "Choose an extension (1-$($ExtensionCounts.Count / 2))"
$chosenIndex = [int]$choice - 1
if ($chosenIndex -ge 0 -and $chosenIndex -lt ($ExtensionCounts.Count / 2)) {
$chosenExtension = $ExtensionCounts[$chosenIndex * 2]
$InType = $chosenExtension
} else {
Write-Host "Invalid choice. Exiting script."
exit
}
}
# Display the chosen extensions
$InType = $InType.TrimStart('.')
Write-Host "Chosen extension: $InType"
####### Requirements Check #######
# Check if FFMpeg exists in the same folder as the script
if (-Not (Test-Path -Path "$FFMpeg_Path")) {
Write-Host "Error: FFmpeg not found. Please edit this Powershell script and add the path to FFMpeg." -ForegroundColor Red
exit
}
# Check if Output path exists
if (-Not (Test-Path -Path "$OutPut_Path")) {
Write-Host "Error: Output folder not found. Please edit this Powershell script and add the path to write the re-encoded file to." -ForegroundColor Red
exit
}
# Check if Logfile path exists
if (-Not (Test-Path -Path "$Logfile_Path")) {
Write-Host "Error: LogFile folder not found. Please edit this Powershell script and add the path to save the log-files to." -ForegroundColor Red
exit
}
# Rename and replace illegal characters in the file names of the ORIGINAL files. This means the Original files will be renamed if found to have illegal characters
$files = Get-ChildItem -Path $CurrentDirectory -Recurse
# Loop through each file and check for '[' and ']' characters in the filename
foreach ($file in $files) {
$newFileName = $file.Name -replace '\[', '(' -replace '\]', ')'
# Check if the filename needs to be changed
if ($newFileName -ne $file.Name) {
$newFilePath = Join-Path -Path $CurrentDirectory -ChildPath $newFileName
Rename-Item -Path $file.FullName -NewName $newFileName
Write-Host "INFO: Renamed $($file.Name) to $($newFileName)" -ForegroundColor Green
}
}
If ($InType -eq "mp4") {
$OutExt = ".mkv"
} else {
$OutExt = ".mkv"
}
# Script logic using $InType, what videos for $InType can we find
$InExt = "*."+$InType
$oldVideos = Get-ChildItem -Include @($InExt) -Path $CurrentDirectory -Recurse;
$BeginTotalTime = Get-Date
If ($oldVideos.Length -gt 0) {
# Length is file size if only one file found
If ($null -eq $oldVideos.LongLength) {
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - No. of Videos found: 1")
} else {
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - No. of videos found: $($oldVideos.Length)")
}
$i = 0
# Process Videos found to $OutType
foreach ($oldVideo in $oldVideos) {
# New Video name and ext
$newVideo = Join-Path $OutPut_Path ($oldVideo.BaseName + "$OutExt")
# Rename the output file if it contains "h264," "x264," or "h.264"
$newVideo = $newVideo -replace "h264|x264|h\.264", "x265"
# Declare the command line arguments for ffmpeg
$ArgumentList = '-hide_banner -loglevel +level -hwaccel_device 0 -hwaccel auto -i "{0}" -vf yadif -map 0:v:0 -c:v:0 hevc_nvenc -preset {2} -crf 22 -level 5.1 -tier high -map 0:a -c:a copy -map 0:s? -c:s copy -map_chapters 0 -map_metadata 0 "{1}"' -f $oldVideo, $newVideo, $GPUPreset
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - Started - Processing $oldVideo") -ForegroundColor Blue
$BeginTime = Get-Date
# Kick off the ffmpeg process
Start-Process -RedirectStandardError "$newVideo.txt" -FilePath $FFMpeg_Path -ArgumentList $ArgumentList -Wait -NoNewWindow
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - Finished - Processing $oldVideo")
$EndTime = Get-Date
$TimeTaken = $EndTime - $BeginTime
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - Re-Encoding - Processing Completed in a Time of: " + $TimeTaken) -ForegroundColor Green
$i++
# Length is file size if only one file found
If ($null -eq $oldVideos.LongLength) {
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - No. of Videos Completed: $i out of 1") -ForegroundColor Yellow
} else {
Write-Host $("INFO: ["+$(Get-Date -Format F)+"] - No. of Videos Completed: $i out of $($oldvideos.length)") -ForegroundColor Yellow
}
}
} else {
Write-Host $("Error: ["+$(Get-Date -Format F)+"] - No Files of Type: "+$InType.ToUpper()+" Found.") -ForegroundColor Red
}
# Display the time on screen
$EndTotalTime = Get-Date
$TotalTimeTaken = $EndTotalTime - $BeginTotalTime
Write-Host $("INFO:["+$(Get-Date -Format F)+"] - Finished - All Processing Completed in a Time of: "+ $TotalTimeTaken) -ForegroundColor Blue
# Cleanup: Move log-files to another folder
$LogType = "*.txt"
# Get a list of Log files and initialise counter
$filesToMove = Get-ChildItem -Path $OutPut_Path -Filter $LogType
$filesMoved = 0
# Loop through each file and move it $Logfile_Path folder
foreach ($file in $filesToMove) {
Move-Item -Path $file.FullName -Destination $Logfile_Path -Force
$filesMoved++
}
Write-Host "INFO: Log files were cleaned-up. $filesMoved logs moved to $Logfile_Path" -ForegroundColor Green
Exit
Known issue: FFmpeg doesn’t automatically overwrite the output file if a file with the same name already exists. The logfile will pose the question to overwrite y/n, but it’s not displayed on screen. Since I don’t want auto-overwrite, I’m not starting the process with the -y parameter (which would fix this particular problem), but I’d like to have the choice. If this happens, just type y or n respectively in the PowerShell window and the script will continue or exit, depending on the answer.