295 lines
9.3 KiB
Bash
Executable File
295 lines
9.3 KiB
Bash
Executable File
#!/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/<video-name>/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<NUM_SEGMENTS; i++)); do
|
|
CURRENT_SEGMENT=$((i + 1))
|
|
START_TIME=$((i * SEGMENT_DURATION))
|
|
|
|
# Determine actual segment duration (might be shorter for last segment)
|
|
if [[ $i -eq $((NUM_SEGMENTS - 1)) ]] && [[ "$REMAINDER" -gt 0 ]] && [[ "$REMAINDER" -ge 10 ]]; then
|
|
ACTUAL_DURATION=$REMAINDER
|
|
else
|
|
ACTUAL_DURATION=$SEGMENT_DURATION
|
|
fi
|
|
|
|
# Format segment number with leading zeros (3 digits)
|
|
SEGMENT_NUM=$(printf "%03d" $CURRENT_SEGMENT)
|
|
|
|
# Create output filename
|
|
# Format: <video-base-name> - gif<NNN> - [<duration>s] [<quality>] [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 ""
|