Post

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:

  1. Scans a directory for video files (.mp4, .mkv, .avi).
  2. Checks the file size and skips files smaller than 200 MB.
  3. Calculates the bitrate of the video.
  4. Determines the number of audio channels.
  5. Processes the video based on bitrate and audio channels using FFmpeg.
  6. 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."
This post is licensed under CC BY 4.0 by the author.