feat(user/ffmpeg_parallel_encode.sh):Add parallel | ffmpeg script
[BK-2020-03.git] / user / ffmpeg_parallel_encode.sh
... / ...
CommitLineData
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
7MAX_JOBS=8; # Adjust this number based on your system's CPU capacity
8MAX_MEM=512M; # Adjust this number based on your system's memory
9
10set -e; # Exit immediately if a command exits with a non-zero status
11set -u; # Treat unset variables as an error
12
13yell() { echo "$0: $*" >&2; } # Print script path and all args to stderr
14die() { yell "$*"; exit 111; } # Same as yell() but exits with status 111
15must() { "$@" || die "cannot $*"; } # Runs command, dies if it fails
16
17cleanup() {
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
28trap cleanup EXIT INT TERM;
29
30if [[ "$#" -ne 2 ]]; then
31 yell "Usage: $0 input_video_file segment_duration_in_seconds";
32 exit 1;
33fi;
34
35input_file="$1";
36segment_time="$2";
37
38# Check if input file exists
39if [[ ! -f "$input_file" ]]; then
40 die "Input file '$input_file' does not exist.";
41fi;
42
43base_name="$(basename "$input_file" | sed 's/\.[^.]*$//')"; # Strip extension
44segment_prefix="${base_name}_segment";
45encode_suffix="_encoded";
46declare -a segment_filenames;
47
48# Set umask to ensure files and directories are not accessible by others
49umask 0077;
50
51# Create a unique temporary directory for encoded files in the working directory
52timestamp="$(date +%s)";
53temp_dir="$(mktemp -d "${base_name}_${timestamp}_XXXXXX")";
54
55# Safety check: Ensure temp_dir is not empty and is a directory
56if [[ -z "${temp_dir}" || ! -d "${temp_dir}" ]]; then
57 die "Temporary directory creation failed.";
58fi;
59
60yell "Temporary directory created at $temp_dir";
61
62# Get the total duration of the input video in seconds
63yell "Getting total duration of input video...";
64duration_str="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file")";
65if [[ -z "$duration_str" ]]; then
66 die "Failed to get duration of input video.";
67fi;
68
69duration="$(printf "%.0f" "$duration_str")"; # Convert to integer seconds
70
71yell "Total duration: $duration seconds";
72
73# Calculate the number of segments
74num_segments=$(( (duration + segment_time - 1) / segment_time )); # Ceiling division
75
76yell "Number of segments: $num_segments";
77
78# Generate start times and segment filenames, write to a temporary file
79yell "Preparing list of start times and filenames...";
80temp_list_file="$(mktemp -p "$temp_dir")";
81
82# Use a separator that is unlikely to appear in the data (ASCII Unit Separator)
83separator=$'\x1F';
84
85for (( 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
91done;
92
93# Encode segments in parallel without writing unencoded segments to disk
94yell "Encoding segments in parallel...";
95
96# Use GNU Parallel with the custom separator
97must 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
101segments_file="${temp_dir}/segments.txt";
102yell "Preparing for concatenation...";
103for (( i=0; i<num_segments; i++ )); do
104 segment_filename="${segment_filenames[i]}";
105 echo "file '$segment_filename'" >> "$segments_file";
106done;
107
108# Safety check: Ensure segments_file is not empty
109if [[ ! -s "$segments_file" ]]; then
110 die "Segments file is empty.";
111fi;
112
113# Step 4: Concatenate the encoded segments into a single output file
114output_file="${base_name}_output.mp4";
115yell "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
122yell "Process completed successfully in ${SECONDS} seconds. Output video is $output_file";
123
124# Author: Steven Baltakatei Sandoval
125# License: GPLv3+