#!/bin/bash # Script to generate GIF segments from a source video # Cuts video into segments of target length, auto-detects orientation # Outputs to local/gifs//horizontal/ or /vertical/ # Uses NVIDIA NVENC GPU acceleration when available # Enable strict error handling set -o pipefail # Catch failures in piped commands # ============================================================ # Environment Variables # ============================================================ SOURCE_VIDEO="${SOURCE_VIDEO:-}" SEGMENT_DURATION="${SEGMENT_DURATION:-59}" GIFS_OUTPUT_DIR="${GIFS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/gifs}" # ============================================================ # Validate Inputs # ============================================================ if [[ -z "$SOURCE_VIDEO" ]]; then echo "Error: SOURCE_VIDEO environment variable is required" exit 1 fi if [[ ! -f "$SOURCE_VIDEO" ]]; then echo "Error: Source video not found: $SOURCE_VIDEO" exit 1 fi # ============================================================ # Check Dependencies # ============================================================ if ! command -v ffmpeg &> /dev/null; then echo "Error: ffmpeg is not installed" exit 1 fi if ! command -v ffprobe &> /dev/null; then echo "Error: ffprobe is not installed" exit 1 fi # ============================================================ # Detect NVENC Availability # ============================================================ detect_encoder() { # Check if CPU mode is forced if [[ "$FORCE_CPU" == "true" ]]; then echo "Using CPU encoder (libx265) [--cpu flag]" ENCODER="libx265" ENCODER_OPTS="-preset medium -crf 23 -x265-params log-level=error" return fi if ffmpeg -hide_banner -encoders 2>/dev/null | grep -q hevc_nvenc; then # Test NVENC with a real encode local test_output="/tmp/nvenc_detect_test_$$.mp4" local test_result test_result=$(ffmpeg -y -f lavfi -i "color=black:s=256x256:d=0.1" \ -c:v hevc_nvenc -preset p4 -gpu 0 \ "$test_output" 2>&1) local test_exit=$? rm -f "$test_output" if [[ $test_exit -ne 0 ]]; then echo "WARNING: NVENC test failed, falling back to CPU encoder" ENCODER="libx265" ENCODER_OPTS="-preset medium -crf 23 -x265-params log-level=error" return fi echo "Using NVENC GPU encoder (hevc_nvenc)" ENCODER="hevc_nvenc" ENCODER_OPTS="-preset p4 -cq 23 -rc vbr -gpu 0 -tag:v hvc1" else echo "NVENC not available, using CPU encoder (libx265)" ENCODER="libx265" ENCODER_OPTS="-preset medium -crf 23 -x265-params log-level=error" fi } # ============================================================ # Get Video Metadata via ffprobe # ============================================================ get_video_duration() { local file="$1" ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null | cut -d. -f1 } get_video_dimensions() { local file="$1" ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x "$file" 2>/dev/null } get_video_quality() { local file="$1" local dimensions=$(get_video_dimensions "$file") local height=$(echo "$dimensions" | cut -d'x' -f2) if [[ "$height" -ge 2160 ]]; then echo "4K" elif [[ "$height" -ge 1440 ]]; then echo "1440p" elif [[ "$height" -ge 1080 ]]; then echo "1080p" elif [[ "$height" -ge 720 ]]; then echo "720p" elif [[ "$height" -ge 480 ]]; then echo "480p" else echo "SD" fi } detect_orientation() { local file="$1" local dimensions=$(get_video_dimensions "$file") local width=$(echo "$dimensions" | cut -d'x' -f1) local height=$(echo "$dimensions" | cut -d'x' -f2) if [[ "$width" -gt "$height" ]]; then echo "H" else echo "V" fi } # ============================================================ # Main Script # ============================================================ echo "" echo "=========================================" echo " GIF Segment Generator" echo "=========================================" echo " Source: $SOURCE_VIDEO" echo " Segment Duration: ${SEGMENT_DURATION}s" echo "=========================================" echo "" # Detect encoder detect_encoder echo "" # Get video metadata TOTAL_DURATION=$(get_video_duration "$SOURCE_VIDEO") QUALITY=$(get_video_quality "$SOURCE_VIDEO") ORIENTATION=$(detect_orientation "$SOURCE_VIDEO") if [[ -z "$TOTAL_DURATION" ]] || [[ "$TOTAL_DURATION" -eq 0 ]]; then echo "Error: Could not determine video duration" exit 1 fi echo "Video Information:" echo " Duration: ${TOTAL_DURATION}s" echo " Quality: $QUALITY" echo " Orientation: $ORIENTATION ($([ "$ORIENTATION" == "H" ] && echo "Horizontal" || echo "Vertical"))" echo "" # Calculate number of segments NUM_SEGMENTS=$((TOTAL_DURATION / SEGMENT_DURATION)) REMAINDER=$((TOTAL_DURATION % SEGMENT_DURATION)) # Include partial last segment if it's at least 10 seconds if [[ "$REMAINDER" -ge 10 ]]; then NUM_SEGMENTS=$((NUM_SEGMENTS + 1)) fi if [[ "$NUM_SEGMENTS" -eq 0 ]]; then echo "Error: Video is too short to create segments (${TOTAL_DURATION}s < ${SEGMENT_DURATION}s)" exit 1 fi echo "Will create $NUM_SEGMENTS segments" echo "" # Extract base name from source video (without extension and metadata tags) SOURCE_BASENAME=$(basename "$SOURCE_VIDEO" .mp4) # Remove existing metadata tags like [H:MM] [Quality] [E] [H/V] CLEAN_BASENAME=$(echo "$SOURCE_BASENAME" | sed -E 's/ *\[[^][]*\]//g' | sed 's/ *$//') # Create output directory with -gifs suffix OUTPUT_DIR="${GIFS_OUTPUT_DIR}/${CLEAN_BASENAME}-gifs" echo "Output directory: $OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" # Track progress CURRENT_SEGMENT=0 SUCCESSFUL=0 FAILED=0 start_time=$(date +%s) # Generate segments for ((i=0; i - gif - [s] [] [E] [H/V].mp4 OUTPUT_FILENAME="${CLEAN_BASENAME} - gif${SEGMENT_NUM} - [${ACTUAL_DURATION}s] [${QUALITY}] [E] [${ORIENTATION}].mp4" OUTPUT_PATH="${OUTPUT_DIR}/${OUTPUT_FILENAME}" echo "" echo "[$CURRENT_SEGMENT/$NUM_SEGMENTS] Creating segment..." echo " Start: ${START_TIME}s, Duration: ${ACTUAL_DURATION}s" echo " Output: $(basename "$OUTPUT_PATH")" # Skip if already exists if [[ -f "$OUTPUT_PATH" ]]; then echo " Skipped: Already exists" SUCCESSFUL=$((SUCCESSFUL + 1)) continue fi # Encode segment with GPU (with CPU fallback) TEMP_OUTPUT="${OUTPUT_PATH}.temp.mp4" encode_output=$(ffmpeg -nostdin -hide_banner \ -ss "$START_TIME" -i "$SOURCE_VIDEO" \ -t "$ACTUAL_DURATION" \ -c:v $ENCODER $ENCODER_OPTS \ -c:a aac -b:a 128k \ -movflags +faststart \ -y "$TEMP_OUTPUT" 2>&1) encode_exit=$? # Check for NVENC failure and retry with CPU if [[ $encode_exit -ne 0 ]] && [[ "$ENCODER" == "hevc_nvenc" ]]; then echo " NVENC failed, retrying with CPU encoder..." rm -f "$TEMP_OUTPUT" encode_output=$(ffmpeg -nostdin -hide_banner \ -ss "$START_TIME" -i "$SOURCE_VIDEO" \ -t "$ACTUAL_DURATION" \ -c:v libx265 -preset medium -crf 23 -x265-params log-level=error \ -c:a aac -b:a 128k \ -movflags +faststart \ -y "$TEMP_OUTPUT" 2>&1) encode_exit=$? fi # Verify and finalize if [[ $encode_exit -eq 0 ]] && [[ -f "$TEMP_OUTPUT" ]]; then file_size=$(stat -c%s "$TEMP_OUTPUT" 2>/dev/null) if [[ -n "$file_size" ]] && [[ "$file_size" -gt 10000 ]]; then mv "$TEMP_OUTPUT" "$OUTPUT_PATH" file_size_human=$(du -h "$OUTPUT_PATH" | cut -f1) echo " Success! (${file_size_human})" SUCCESSFUL=$((SUCCESSFUL + 1)) else echo " Error: Output file too small (likely corrupted)" rm -f "$TEMP_OUTPUT" FAILED=$((FAILED + 1)) fi else echo " Error: Encoding failed (exit code: $encode_exit)" rm -f "$TEMP_OUTPUT" FAILED=$((FAILED + 1)) fi # Small delay between segments to prevent GPU overload sleep 0.2 done # 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 " GIF Generation Complete!" echo "=========================================" echo " Source: $(basename "$SOURCE_VIDEO")" echo " Output: $OUTPUT_DIR" echo " Segments: $SUCCESSFUL successful, $FAILED failed" echo " Time elapsed: ${hours}h ${minutes}m ${seconds}s" echo "=========================================" echo ""