Encode Media with FFMPEG
Usage Guide for Video Processing Bash Script
This guide explains how to use the provided Bash script to process video files in a specified directory. The script evaluates and processes video files based on their bitrate and audio channels, applying specific FFmpeg commands to convert or skip them based on defined criteria.
Prerequisites
- Bash Shell: Ensure you are running a Unix-like operating system with Bash.
- FFmpeg & bc: FFmpeg & bc must be installed on your system. You can install it via package managers such as apt for Debian-based systems:
1
sudo apt-get install ffmpeg bc # Debian-based systems
Script Overview
The script performs the following actions:
- Scans a directory for video files (.mp4, .mkv, .avi).
- Checks the file size and skips files smaller than 200 MB.
- Calculates the bitrate of the video.
- Determines the number of audio channels.
- Processes the video based on bitrate and audio channels using FFmpeg.
- Logs processed and failed files in separate log files.
Script Functions
- calculate_bitrate(file_path): Calculates the bitrate of the video file.
- get_audio_channels(file_path): Gets the number of audio channels.
- get_duration(file_path): Gets the duration of the video file.
- get_ffmpeg_format(file_path): Determines the FFmpeg format based on the file extension.
Script Usage
- Save the Script: Save the script to a file, for example, process_videos.sh.
- Make the Script Executable: Change the permissions to make the script executable.
1
chmod +x process_videos.sh
Example Usage
1
./process_videos.sh /home/user/videos
This command processes all video files in /home/user/videos and its subdirectories.
Detailed Steps
Ensure Log Files Exist: The script checks for the presence of processedFiles.txt and failedFiles.txt, creating them if they do not exist.
Process Each Video File:
- Check File Size: Skips files smaller than 200 MB.
- Calculate Bitrate: Uses FFmpeg to calculate the bitrate.
- Get Audio Channels: Uses FFmpeg to find out the number of audio channels.
- FFmpeg Format: Determines the appropriate FFmpeg format based on the file extension.
- Processing Based on Bitrate:
- High Bitrate: If the bitrate is higher than 3800 kbps, the video is re-encoded with specific FFmpeg settings.
- Low Bitrate and Multiple Audio Channels: If the bitrate is lower and the file has more than 2 audio channels, the audio channels are reduced to 2 and re-encoded.
Check and Log Duration Match: After processing, the script checks if the duration of the processed file matches the original within a small margin. If the durations match, the processed file replaces the original; otherwise, it logs the file in failedFiles.txt.
Logging
- processedFiles.txt: Keeps track of successfully processed files.
- failedFiles.txt: Logs files that failed to process correctly due to duration mismatches or other issues.
The Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/bin/bash
# Function to calculate bitrate
calculate_bitrate() {
local file_path="$1"
local file_size_bytes
file_size_bytes=$(stat -c%s "$file_path")
echo "Filesize = $file_size_bytes"
local video_duration
video_duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path")
local bitrate_kbps
bitrate_kbps=$(echo "scale=2; ($file_size_bytes * 8) / $video_duration / 1024" | bc)
echo "$bitrate_kbps"
}
# Function to get the number of audio channels
get_audio_channels() {
local file_path="$1"
ffprobe -v error -select_streams a:0 -show_entries stream=channels -of default=noprint_wrappers=1:nokey=1 "$file_path"
}
# Function to get the duration of the video
get_duration() {
local file_path="$1"
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path"
}
# Function to get the ffmpeg format based on file extension
get_ffmpeg_format() {
local file_path="$1"
local extension="${file_path##*.}"
case "$extension" in
mkv) echo "matroska" ;;
mp4) echo "mp4" ;;
*) echo "$extension" ;;
esac
}
# Main script logic
processed_files_log="processedFiles.txt"
failed_files_log="failedFiles.txt"
top_level_directory="${1:-.}"
# Ensure the processed files log exists
if [[ ! -f "$processed_files_log" ]]; then
touch "$processed_files_log"
fi
processed_files=$(cat "$processed_files_log")
find "$top_level_directory" -type f \( -iname "*.mp4" -o -iname "*.mkv" -o -iname "*.avi" \) | while read -r file; do
if grep -Fxq "$file" <<< "$processed_files"; then
echo "Skipping already processed file $file."
continue
fi
file_size_mb=$(echo "scale=2; $(stat -c%s "$file") / 1048576" | bc)
if (( $(echo "$file_size_mb < 200" | bc -l) )); then
echo "Skipping $file due to its size ($file_size_mb MB) being under 200 MB."
echo "$file" >> "$processed_files_log"
continue
fi
bitrate=$(calculate_bitrate "$file")
audio_channels=$(get_audio_channels "$file")
original_duration=$(get_duration "$file")
ffmpeg_format=$(get_ffmpeg_format "$file")
if (( $(echo "$bitrate > 3800" | bc -l) )); then
if [[ -f "${file}.tmp" ]]; then
rm -f "${file}.tmp"
echo "The file '${file}.tmp' has been deleted."
else
echo "The file '${file}.tmp' does not exist."
fi
ffmpeg -hwaccel auto -i "$file" -nostdin -b:v 2M -minrate 1M -maxrate 10M -c:v libx265 -pix_fmt yuv420p10le -x265-params rc-lookahead=120 -profile:v main10 -c:a aac -b:a 128k -ac 2 -af loudnorm -y -f "$ffmpeg_format" "${file}.tmp"
new_duration=$(get_duration "${file}.tmp")
duration_diff=$(echo "scale=4; ($new_duration - $original_duration) / $original_duration" | bc)
if (( $(echo "$duration_diff < 0.02 && $duration_diff > -0.02" | bc -l) )); then
mv -f "${file}.tmp" "$file"
echo "Duration match for $file"
echo "$file" >> "$processed_files_log"
else
echo "Duration mismatch for $file"
rm -f "${file}.tmp"
echo "$file" >> "$failed_files_log"
fi
elif (( $(echo "$bitrate <= 3800" | bc -l) )); then
if (( audio_channels > 2 )); then
if [[ -f "${file}.tmp" ]]; then
rm -f "${file}.tmp"
echo "The file '${file}.tmp' has been deleted."
else
echo "The file '${file}.tmp' does not exist."
fi
ffmpeg -i "$file" -nostdin -c:v copy -c:a aac -ac 2 -filter:a loudnorm -f "$ffmpeg_format" "${file}.tmp"
new_duration=$(get_duration "${file}.tmp")
duration_diff=$(echo "scale=4; ($new_duration - $original_duration) / $original_duration" | bc)
if (( $(echo "$duration_diff < 0.02 && $duration_diff > -0.02" | bc -l) )); then
mv -f "${file}.tmp" "$file"
echo "Duration match for $file"
echo "$file" >> "$processed_files_log"
else
echo "Duration mismatch for $file"
rm -f "${file}.tmp"
echo "$file" >> "$failed_files_log"
fi
else
echo "$file" >> "$processed_files_log"
fi
fi
done
echo "Processing complete."
The Code: Bash: NVidia Encoding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/bin/bash
# Function to calculate bitrate
calculate_bitrate() {
local file_path="$1"
local file_size_bytes
file_size_bytes=$(stat -c%s "$file_path")
echo "Filesize = $file_size_bytes"
local video_duration
video_duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path")
local bitrate_kbps
bitrate_kbps=$(echo "scale=2; ($file_size_bytes * 8) / $video_duration / 1024" | bc)
echo "$bitrate_kbps"
}
# Function to get the number of audio channels
get_audio_channels() {
local file_path="$1"
ffprobe -v error -select_streams a:0 -show_entries stream=channels -of default=noprint_wrappers=1:nokey=1 "$file_path"
}
# Function to get the duration of the video
get_duration() {
local file_path="$1"
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path"
}
# Function to get the ffmpeg format based on file extension
get_ffmpeg_format() {
local file_path="$1"
local extension="${file_path##*.}"
case "$extension" in
mkv) echo "matroska" ;;
mp4) echo "mp4" ;;
*) echo "$extension" ;;
esac
}
# Main script logic
processed_files_log="processedFiles.txt"
failed_files_log="failedFiles.txt"
top_level_directory="${1:-.}"
# Ensure the processed files log exists
if [[ ! -f "$processed_files_log" ]]; then
touch "$processed_files_log"
fi
processed_files=$(cat "$processed_files_log")
find "$top_level_directory" -type f \( -iname "*.mp4" -o -iname "*.mkv" -o -iname "*.avi" \) | while read -r file; do
if grep -Fxq "$file" <<< "$processed_files"; then
echo "Skipping already processed file $file."
continue
fi
file_size_mb=$(echo "scale=2; $(stat -c%s "$file") / 1048576" | bc)
if (( $(echo "$file_size_mb < 200" | bc -l) )); then
echo "Skipping $file due to its size ($file_size_mb MB) being under 200 MB."
echo "$file" >> "$processed_files_log"
continue
fi
bitrate=$(calculate_bitrate "$file")
audio_channels=$(get_audio_channels "$file")
original_duration=$(get_duration "$file")
ffmpeg_format=$(get_ffmpeg_format "$file")
if (( $(echo "$bitrate > 3800" | bc -l) )); then
if [[ -f "${file}.tmp" ]]; then
rm -f "${file}.tmp"
echo "The file '${file}.tmp' has been deleted."
else
echo "The file '${file}.tmp' does not exist."
fi
ffmpeg -hwaccel nvdec -i "$file" -nostdin -b:v 2M -minrate 1M -maxrate 10M -c:v hevc_nvenc -pix_fmt yuv420p10le -profile:v main10 -c:a aac -b:a 128k -ac 2 -af loudnorm -y -f "$ffmpeg_format" "${file}.tmp"
new_duration=$(get_duration "${file}.tmp")
duration_diff=$(echo "scale=4; ($new_duration - $original_duration) / $original_duration" | bc)
if (( $(echo "$duration_diff < 0.02 && $duration_diff > -0.02" | bc -l) )); then
mv -f "${file}.tmp" "$file"
echo "Duration match for $file"
echo "$file" >> "$processed_files_log"
else
echo "Duration mismatch for $file"
rm -f "${file}.tmp"
echo "$file" >> "$failed_files_log"
fi
elif (( $(echo "$bitrate <= 3800" | bc -l) )); then
if (( audio_channels > 2 )); then
if [[ -f "${file}.tmp" ]]; then
rm -f "${file}.tmp"
echo "The file '${file}.tmp' has been deleted."
else
echo "The file '${file}.tmp' does not exist."
fi
ffmpeg -i "$file" -nostdin -c:v copy -c:a aac -ac 2 -filter:a loudnorm -f "$ffmpeg_format" "${file}.tmp"
new_duration=$(get_duration "${file}.tmp")
duration_diff=$(echo "scale=4; ($new_duration - $original_duration) / $original_duration" | bc)
if (( $(echo "$duration_diff < 0.02 && $duration_diff > -0.02" | bc -l) )); then
mv -f "${file}.tmp" "$file"
echo "Duration match for $file"
echo "$file" >> "$processed_files_log"
else
echo "Duration mismatch for $file"
rm -f "${file}.tmp"
echo "$file" >> "$failed_files_log"
fi
else
echo "$file" >> "$processed_files_log"
fi
fi
done
echo "Processing complete."
The Code: PowerShell Software Encoding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
function Calculate-Bitrate {
param (
[string]$FilePath
)
$fileSizeBytes = (Get-Item -LiteralPath $FilePath).Length
Write-Host "Filesize = $fileSizeBytes"
$videoDuration = ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $FilePath
$bitrateKbps = [math]::Round(($fileSizeBytes * 8) / $videoDuration / 1024, 2)
return $bitrateKbps
}
function Get-AudioChannels {
param (
[string]$FilePath
)
return & ffprobe -v error -select_streams a:0 -show_entries stream=channels -of default=noprint_wrappers=1:nokey=1 $FilePath
}
function Get-Duration {
param (
[string]$FilePath
)
return & ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $FilePath
}
function Get-FFmpegFormat {
param (
[string]$FilePath
)
$extension = [System.IO.Path]::GetExtension($FilePath).TrimStart('.')
switch ($extension) {
'mkv' { return 'matroska' }
'mp4' { return 'mp4' }
default { return $extension }
}
}
$processedFilesLog = "processedFiles.txt"
$failedFilesLog = "failedFiles.txt"
$topLevelDirectory = if ($args[0]) { $args[0] } else { '.' }
if (-not (Test-Path -LiteralPath $processedFilesLog)) {
New-Item -ItemType File -Path $processedFilesLog -Force
}
$processedFiles = Get-Content -Path $processedFilesLog
Get-ChildItem -Path $topLevelDirectory -Recurse -File | Where-Object { $_.Extension -match 'mp4|mkv|avi' } | ForEach-Object {
$file = $_.FullName
if ($processedFiles -contains $file) {
Write-Host "Skipping already processed file $file."
return
}
$fileSizeMb = ($_.Length / 1MB)
if ($fileSizeMb -lt 200) {
Write-Host "Skipping $file due to its size ($fileSizeMb MB) being under 200 MB."
Add-Content -Path $processedFilesLog -Value $file
return
}
$bitrate = Calculate-Bitrate -FilePath $file
$audioChannels = Get-AudioChannels -FilePath $file
$originalDuration = Get-Duration -FilePath $file
$ffmpegFormat = Get-FFmpegFormat -FilePath $file
if ([math]::Round($bitrate, 2) -gt 3800) {
if (Test-Path -LiteralPath "${file}.tmp") {
Remove-Item -LiteralPath "${file}.tmp"
Write-Output "The file '${file}.tmp' has been deleted."
} else {
Write-Output "The file '${file}.tmp' does not exist."
}
& ffmpeg -hwaccel auto -i $file -nostdin -b:v 2M -minrate 1M -maxrate 10M -c:v libx265 -pix_fmt yuv420p10le -x265-params rc-lookahead=120 -profile:v main10 -c:a aac -b:a 128k -ac 2 -af loudnorm -y -f $ffmpegFormat "${file}.tmp"
$newDuration = Get-Duration -FilePath "${file}.tmp"
$durationDiff = [math]::Round(($newDuration - $originalDuration) / $originalDuration, 4)
if ($durationDiff -lt 0.02 -and $durationDiff -gt -0.02) {
Move-Item -LiteralPath "${file}.tmp" -Destination $file -Force
Write-Host "Duration match for $file"
Add-Content -Path $processedFilesLog -Value $file
} else {
Write-Host "Duration mismatch for $file"
Remove-Item -LiteralPath "${file}.tmp"
Add-Content -Path $failedFilesLog -Value $file
}
} elseif ([math]::Round($bitrate, 2) -le 3800) {
if ($audioChannels -gt 2) {
if (Test-Path -LiteralPath "${file}.tmp") {
Remove-Item -LiteralPath "${file}.tmp"
Write-Output "The file '${file}.tmp' has been deleted."
} else {
Write-Output "The file '${file}.tmp' does not exist."
}
& ffmpeg -i $file -nostdin -c:v copy -c:a aac -ac 2 -filter:a loudnorm -f $ffmpegFormat "${file}.tmp"
$newDuration = Get-Duration -FilePath "${file}.tmp"
$durationDiff = [math]::Round(($newDuration - $originalDuration) / $originalDuration, 4)
if ($durationDiff -lt 0.02 -and $durationDiff -gt -0.02) {
Move-Item -LiteralPath "${file}.tmp" -Destination $file -Force
Write-Host "Duration match for $file"
Add-Content -Path $processedFilesLog -Value $file
} else {
Write-Host "Duration mismatch for $file"
Remove-Item -LiteralPath "${file}.tmp"
Add-Content -Path $failedFilesLog -Value $file
}
} else {
Add-Content -Path $processedFilesLog -Value $file
}
}
}
Write-Host "Processing complete."