| 1 | #!/bin/bash |
| 2 | # Desc: Encodes video in parallel by processing time ranges directly from the input file. |
| 3 | # Usage: ./ffmpeg_parallel_encode.sh input_video_file segment_duration_in_seconds |
| 4 | # Version: 0.1.5 |
| 5 | |
| 6 | # Limit the number of parallel jobs if necessary |
| 7 | MAX_JOBS=8; # Adjust this number based on your system's CPU capacity |
| 8 | MAX_MEM=512M; # Adjust this number based on your system's memory |
| 9 | |
| 10 | set -e; # Exit immediately if a command exits with a non-zero status |
| 11 | set -u; # Treat unset variables as an error |
| 12 | |
| 13 | yell() { echo "$0: $*" >&2; } # Print script path and all args to stderr |
| 14 | die() { yell "$*"; exit 111; } # Same as yell() but exits with status 111 |
| 15 | must() { "$@" || die "cannot $*"; } # Runs command, dies if it fails |
| 16 | |
| 17 | cleanup() { |
| 18 | if [[ -n "${temp_dir:-}" && -d "$temp_dir" ]]; then |
| 19 | echo "Cleaning up temporary files..."; |
| 20 | rm -rf "$temp_dir"; |
| 21 | fi |
| 22 | if [[ -n "${temp_list_file:-}" && -f "$temp_list_file" ]]; then |
| 23 | rm -f "$temp_list_file"; |
| 24 | fi; |
| 25 | }; # Function to clean up temporary files |
| 26 | |
| 27 | # Trap signals to ensure cleanup happens on script exit or interruption |
| 28 | trap cleanup EXIT INT TERM; |
| 29 | |
| 30 | if [[ "$#" -ne 2 ]]; then |
| 31 | yell "Usage: $0 input_video_file segment_duration_in_seconds"; |
| 32 | exit 1; |
| 33 | fi; |
| 34 | |
| 35 | input_file="$1"; |
| 36 | segment_time="$2"; |
| 37 | |
| 38 | # Check if input file exists |
| 39 | if [[ ! -f "$input_file" ]]; then |
| 40 | die "Input file '$input_file' does not exist."; |
| 41 | fi; |
| 42 | |
| 43 | base_name="$(basename "$input_file" | sed 's/\.[^.]*$//')"; # Strip extension |
| 44 | segment_prefix="${base_name}_segment"; |
| 45 | encode_suffix="_encoded"; |
| 46 | declare -a segment_filenames; |
| 47 | |
| 48 | # Set umask to ensure files and directories are not accessible by others |
| 49 | umask 0077; |
| 50 | |
| 51 | # Create a unique temporary directory for encoded files in the working directory |
| 52 | timestamp="$(date +%s)"; |
| 53 | temp_dir="$(mktemp -d "${base_name}_${timestamp}_XXXXXX")"; |
| 54 | |
| 55 | # Safety check: Ensure temp_dir is not empty and is a directory |
| 56 | if [[ -z "${temp_dir}" || ! -d "${temp_dir}" ]]; then |
| 57 | die "Temporary directory creation failed."; |
| 58 | fi; |
| 59 | |
| 60 | yell "Temporary directory created at $temp_dir"; |
| 61 | |
| 62 | # Get the total duration of the input video in seconds |
| 63 | yell "Getting total duration of input video..."; |
| 64 | duration_str="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file")"; |
| 65 | if [[ -z "$duration_str" ]]; then |
| 66 | die "Failed to get duration of input video."; |
| 67 | fi; |
| 68 | |
| 69 | duration="$(printf "%.0f" "$duration_str")"; # Convert to integer seconds |
| 70 | |
| 71 | yell "Total duration: $duration seconds"; |
| 72 | |
| 73 | # Calculate the number of segments |
| 74 | num_segments=$(( (duration + segment_time - 1) / segment_time )); # Ceiling division |
| 75 | |
| 76 | yell "Number of segments: $num_segments"; |
| 77 | |
| 78 | # Generate start times and segment filenames, write to a temporary file |
| 79 | yell "Preparing list of start times and filenames..."; |
| 80 | temp_list_file="$(mktemp -p "$temp_dir")"; |
| 81 | |
| 82 | # Use a separator that is unlikely to appear in the data (ASCII Unit Separator) |
| 83 | separator=$'\x1F'; |
| 84 | |
| 85 | for (( i=0; i<num_segments; i++ )); do |
| 86 | start_time=$(( i * segment_time )); |
| 87 | segment_filename="${segment_prefix}_$(printf "%03d" "$i")${encode_suffix}.mp4"; |
| 88 | segment_filepath="${temp_dir}/${segment_filename}"; |
| 89 | printf '%s%s%s\n' "$start_time" "$separator" "$segment_filepath" >> "$temp_list_file"; |
| 90 | segment_filenames[i]="$segment_filename"; # Store for later use |
| 91 | done; |
| 92 | |
| 93 | # Encode segments in parallel without writing unencoded segments to disk |
| 94 | yell "Encoding segments in parallel..."; |
| 95 | |
| 96 | # Use GNU Parallel with the custom separator |
| 97 | must parallel -j "$MAX_JOBS" --memsuspend "$MAX_MEM" --no-notice --bar --colsep "$separator" --arg-file "$temp_list_file" \ |
| 98 | ffmpeg -hide_banner -i "$input_file" -ss '{1}' -t "$segment_time" -c:v libx264 -preset fast -crf 23 -c:a copy '{2}'; |
| 99 | |
| 100 | # Step 3: Create a list file for concatenation |
| 101 | segments_file="${temp_dir}/segments.txt"; |
| 102 | yell "Preparing for concatenation..."; |
| 103 | for (( i=0; i<num_segments; i++ )); do |
| 104 | segment_filename="${segment_filenames[i]}"; |
| 105 | echo "file '$segment_filename'" >> "$segments_file"; |
| 106 | done; |
| 107 | |
| 108 | # Safety check: Ensure segments_file is not empty |
| 109 | if [[ ! -s "$segments_file" ]]; then |
| 110 | die "Segments file is empty."; |
| 111 | fi; |
| 112 | |
| 113 | # Step 4: Concatenate the encoded segments into a single output file |
| 114 | output_file="${base_name}_output.mp4"; |
| 115 | yell "Concatenating encoded segments into $output_file..."; |
| 116 | ( |
| 117 | cd "$temp_dir" || die "Failed to change directory to $temp_dir"; |
| 118 | must ffmpeg -f concat -safe 0 -i "segments.txt" -c copy "$output_file"; |
| 119 | mv "$output_file" .. |
| 120 | ) |
| 121 | |
| 122 | yell "Process completed successfully in ${SECONDS} seconds. Output video is $output_file"; |
| 123 | |
| 124 | # Author: Steven Baltakatei Sandoval |
| 125 | # License: GPLv3+ |