Commit | Line | Data |
---|---|---|
b829e4c7 SBS |
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+ |