changemaker.lite/media-manager/scripts/create_triple_vertical_compilation.sh

362 lines
12 KiB
Bash
Executable File

#!/bin/bash
# Script to create 4K compilation with 3 vertical videos side by side
# Canvas: 3840x2160 (4K)
# Each vertical video: 1080x1920
# Positioning with 150px margins and gaps:
# Left: X=150, Y=120
# Center: X=1380, Y=120
# Right: X=2610, Y=120
# Uses NVIDIA NVENC GPU acceleration for fast encoding
# Enable strict error handling
set -o pipefail # Catch failures in piped commands
# Use environment variables if set (for Docker), otherwise use default paths
GIFS_DIR="${GIFS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/gifs}"
OUTPUT_DIR="${INBOX_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/inbox}"
# Directory exclusion (colon-separated list of directory names to exclude)
EXCLUDED_DIRS="${EXCLUDED_DIRS:-}"
# Filter function for excluding directories (handles newline-terminated input)
filter_excluded() {
if [[ -z "$EXCLUDED_DIRS" ]]; then
cat # Pass through unchanged
else
local pattern
pattern=$(echo "$EXCLUDED_DIRS" | sed 's/:/|/g')
grep -Ev "/($pattern)/"
fi
}
cd "$GIFS_DIR" || exit 1
# Check if ffmpeg is available
if ! command -v ffmpeg &> /dev/null; then
echo "Error: ffmpeg is not installed. Please install ffmpeg."
exit 1
fi
# Check if ffprobe is available
if ! command -v ffprobe &> /dev/null; then
echo "Error: ffprobe is not installed. Please install ffprobe."
exit 1
fi
# Check if NVENC is available
if ! ffmpeg -hide_banner -encoders 2>/dev/null | grep -q h264_nvenc; then
echo "Warning: NVENC encoder not available. Falling back to CPU encoding."
ENCODER="libx264"
ENCODER_OPTS="-preset medium -crf 23"
else
# Check if NVENC encoder is currently in use (e.g., by Steam)
enc_usage=$(nvidia-smi dmon -c 1 2>/dev/null | tail -1 | awk '{print $7}')
if [[ "$enc_usage" -gt 50 ]]; then
if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then
echo "GPU encoder in use (${enc_usage}%), proceeding anyway in non-interactive mode"
ENCODER="h264_nvenc"
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0"
else
echo ""
echo "⚠️ WARNING: GPU encoder is currently in use (${enc_usage}% utilization)"
echo " This is likely Steam or another application using NVENC."
echo ""
echo " Options:"
echo " 1) Close Steam/other apps and press Enter to continue with GPU"
echo " 2) Type 'cpu' to use CPU encoding instead (slower)"
echo " 3) Press Ctrl+C to cancel"
echo ""
read -p "Choice: " enc_choice
if [[ "$enc_choice" == "cpu" ]]; then
echo "Using CPU encoding..."
ENCODER="libx264"
ENCODER_OPTS="-preset medium -crf 23"
else
# Re-check encoder usage
enc_usage=$(nvidia-smi dmon -c 1 2>/dev/null | tail -1 | awk '{print $7}')
if [[ "$enc_usage" -gt 50 ]]; then
echo "⚠️ Encoder still in use (${enc_usage}%). Will try GPU but may fall back to CPU."
else
echo "✓ GPU encoder now available"
fi
ENCODER="h264_nvenc"
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0"
fi
fi
else
echo "✓ NVENC GPU encoder detected and available"
ENCODER="h264_nvenc"
ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -b_ref_mode 0"
fi
fi
# Get current date for filename
current_date=$(date +%d-%m-%Y)
echo ""
echo "========================================="
echo " Triple Vertical 4K Compilation"
echo " Encoder: $ENCODER"
echo " Date: $current_date"
echo "========================================="
echo ""
# Non-interactive mode: use NUM_SETS from environment or default to 100
if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then
NUM_SETS="${NUM_SETS:-100}"
echo "Non-interactive mode: Using $NUM_SETS sets"
else
# Interactive menu to select number of clip sets
echo "Select compilation size (sets of 3 videos):"
echo " 1) 30 sets (90 clips total)"
echo " 2) 60 sets (180 clips total)"
echo " 3) 100 sets (300 clips total, default)"
echo ""
read -p "Enter choice [1-3] (default: 3): " size_choice
echo ""
case "$size_choice" in
1)
NUM_SETS=30
;;
2)
NUM_SETS=60
;;
*)
NUM_SETS=100
;;
esac
fi
NUM_CLIPS=$((NUM_SETS * 3))
echo "Creating compilation with $NUM_SETS sets ($NUM_CLIPS clips total)"
# Log excluded directories if any
if [[ -n "$EXCLUDED_DIRS" ]]; then
echo "Excluding directories: $EXCLUDED_DIRS"
fi
echo ""
start_time=$(date +%s)
# Function to get video duration in seconds
get_duration_seconds() {
local file="$1"
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null | cut -d. -f1
}
# Function to format duration as H:MM
format_duration() {
local seconds="$1"
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
printf "%d:%02d" "$hours" "$minutes"
}
# Function to show progress bar
show_progress() {
local current=$1
local total=$2
local width=40
local percentage=$((current * 100 / total))
local filled=$((width * current / total))
local empty=$((width - filled))
printf "\r → Processing set %d/%d [" "$current" "$total"
printf "%${filled}s" | tr ' ' '█'
printf "%${empty}s" | tr ' ' '░'
printf "] %3d%%" "$percentage"
}
# Function to check if video has audio stream
has_audio() {
local file="$1"
local audio_streams=$(ffprobe -v error -select_streams a -show_entries stream=index -of csv=p=0 "$file" 2>/dev/null)
[[ -n "$audio_streams" ]]
}
# =========================================
# TRIPLE VERTICAL COMPILATION
# =========================================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Creating Triple Vertical 4K Compilation"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Find all vertical MP4 files and shuffle
echo " → Scanning for vertical clips..."
mapfile -t v_files < <(find "$GIFS_DIR" -path "*/vertical/*.mp4" -type f 2>/dev/null | filter_excluded | shuf)
v_total=${#v_files[@]}
echo " → Found $v_total vertical clips"
# We need clips in multiples of 3
clips_needed=$NUM_CLIPS
if [[ $v_total -lt $clips_needed ]]; then
# Round down to nearest multiple of 3
clips_needed=$((v_total / 3 * 3))
NUM_SETS=$((clips_needed / 3))
echo " → Using $clips_needed clips ($NUM_SETS sets) - not enough for requested amount"
fi
if [[ $NUM_SETS -gt 0 ]]; then
# Create temp directory for intermediate files
temp_dir=$(mktemp -d)
echo ""
echo " → Encoding sets of 3 videos..."
for ((set=0; set<NUM_SETS; set++)); do
show_progress "$((set + 1))" "$NUM_SETS"
# Get indices for this set
idx1=$((set * 3))
idx2=$((set * 3 + 1))
idx3=$((set * 3 + 2))
video1="${v_files[$idx1]}"
video2="${v_files[$idx2]}"
video3="${v_files[$idx3]}"
intermediate_file="$temp_dir/$(printf "%05d" $set).ts"
# Get the shortest duration among the 3 videos to sync them
dur1=$(get_duration_seconds "$video1")
dur2=$(get_duration_seconds "$video2")
dur3=$(get_duration_seconds "$video3")
# Find minimum duration
min_dur=$dur1
[[ $dur2 -lt $min_dur ]] && min_dur=$dur2
[[ $dur3 -lt $min_dur ]] && min_dur=$dur3
# FFmpeg complex filter to:
# 1. Scale each video to 1080x1920
# 2. Create black 4K canvas
# 3. Overlay videos at specified positions
# Positions: Left (150,120), Center (1380,120), Right (2610,120)
# Check which videos have audio, generate silent audio for those that don't
if has_audio "$video1"; then
audio1_filter="[0:a]aresample=async=1,asetpts=PTS-STARTPTS[a0]"
else
audio1_filter="anullsrc=r=48000:cl=stereo,atrim=0:${min_dur}[a0]"
fi
if has_audio "$video2"; then
audio2_filter="[1:a]aresample=async=1,asetpts=PTS-STARTPTS[a1]"
else
audio2_filter="anullsrc=r=48000:cl=stereo,atrim=0:${min_dur}[a1]"
fi
if has_audio "$video3"; then
audio3_filter="[2:a]aresample=async=1,asetpts=PTS-STARTPTS[a2]"
else
audio3_filter="anullsrc=r=48000:cl=stereo,atrim=0:${min_dur}[a2]"
fi
filter_complex="
[0:v]scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,fps=30,setpts=PTS-STARTPTS[v0];
[1:v]scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,fps=30,setpts=PTS-STARTPTS[v1];
[2:v]scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,fps=30,setpts=PTS-STARTPTS[v2];
color=black:s=3840x2160:d=${min_dur}:r=30[base];
[base][v0]overlay=150:120:shortest=1[tmp1];
[tmp1][v1]overlay=1380:120:shortest=1[tmp2];
[tmp2][v2]overlay=2610:120:shortest=1[vout];
${audio1_filter};
${audio2_filter};
${audio3_filter};
[a0][a1][a2]amix=inputs=3:duration=shortest[aout]
"
# Try NVENC encoding
nvenc_output=$(ffmpeg -nostdin \
-i "$video1" -i "$video2" -i "$video3" \
-filter_complex "$filter_complex" \
-map "[vout]" -map "[aout]" \
-c:v $ENCODER $ENCODER_OPTS \
-c:a aac -b:a 192k \
-t "$min_dur" \
-f mpegts \
-y "$intermediate_file" 2>&1)
nvenc_exit=$?
# Check if NVENC failed
if [[ $nvenc_exit -ne 0 ]] || [[ "$nvenc_output" == *"No capable devices"* ]] || [[ "$nvenc_output" == *"CUDA_ERROR"* ]]; then
ffmpeg -nostdin \
-i "$video1" -i "$video2" -i "$video3" \
-filter_complex "$filter_complex" \
-map "[vout]" -map "[aout]" \
-c:v libx264 -preset fast -crf 23 \
-c:a aac -b:a 192k \
-t "$min_dur" \
-f mpegts \
-loglevel error \
-y "$intermediate_file" 2>&1
fi
# Small delay to let GPU resources release
sleep 0.1
done
echo ""
# Concatenate all segments
echo " → Merging all segments..."
concat_list_file="$temp_dir/concat_list.txt"
for ts_file in "$temp_dir"/*.ts; do
echo "file '$(realpath "$ts_file")'" >> "$concat_list_file"
done
# Temporary output for metadata extraction
temp_output="$temp_dir/output.mp4"
ffmpeg -f concat -safe 0 -i "$concat_list_file" \
-c copy \
-movflags +faststart \
-loglevel error \
-y "$temp_output" 2>&1
ffmpeg_exit=$?
if [[ $ffmpeg_exit -eq 0 ]] && [[ -f "$temp_output" ]]; then
# Get final video metadata
total_duration=$(get_duration_seconds "$temp_output")
duration_formatted=$(format_duration "$total_duration")
# Create final filename with metadata (always 4K, always H for this format)
final_filename="Triple Vertical Comp [${current_date}] [${duration_formatted}] [4K] [E] [H].mp4"
final_path="$OUTPUT_DIR/$final_filename"
mv "$temp_output" "$final_path"
file_size_human=$(du -h "$final_path" | cut -f1)
echo " ✓ Success! Created: $final_filename ($file_size_human)"
else
echo " ✗ Failed to create compilation (exit code: $ffmpeg_exit)"
fi
# Cleanup
rm -rf "$temp_dir"
else
echo " ⚠ Not enough vertical clips found (need at least 3)"
fi
# Calculate elapsed time
end_time=$(date +%s)
elapsed=$((end_time - start_time))
hours=$((elapsed / 3600))
minutes=$(((elapsed % 3600) / 60))
seconds=$((elapsed % 60))
echo ""
echo ""
echo "========================================="
echo " Triple Vertical Compilation Complete!"
echo "========================================="
echo " Time elapsed: ${hours}h ${minutes}m ${seconds}s"
echo "========================================="
echo ""