771 lines
25 KiB
Bash
Executable File
771 lines
25 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Script to re-encode video files for optimal Plex/web streaming
|
|
# Uses H.265/HEVC with web-optimized quality (CRF 25)
|
|
# Features: NVENC GPU acceleration, faststart for streaming, in-place replacement
|
|
# Adds [E] flag to filename after successful encoding
|
|
|
|
# Enable strict error handling
|
|
set -o pipefail # Catch failures in piped commands
|
|
|
|
# Use environment variable if set (for Docker), otherwise use default path
|
|
# Target public directory only (curated content for streaming)
|
|
ROOT_DIR="${SOURCE_LIBRARY:-/media/bunker-admin/Internal/plex/xxx/media/public}"
|
|
LOG_FILE="$ROOT_DIR/reencode_log_$(date +%Y%m%d_%H%M%S).txt"
|
|
|
|
# Minimum file size ratio (encoded/original) - below this suggests encoding failure
|
|
MIN_SIZE_RATIO=0.05
|
|
|
|
# Quality settings
|
|
CRF_VALUE=25
|
|
AUDIO_BITRATE="128k"
|
|
|
|
cd "$ROOT_DIR" || exit 1
|
|
|
|
# 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 encoder
|
|
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 $CRF_VALUE -x265-params log-level=error"
|
|
return
|
|
fi
|
|
|
|
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -q hevc_nvenc; then
|
|
# Actually test NVENC with a real encode (not just check if listed)
|
|
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
|
|
# Parse specific CUDA errors for helpful messages
|
|
echo ""
|
|
if echo "$test_result" | grep -q "CUDA_ERROR_UNKNOWN"; then
|
|
echo "WARNING: NVENC unavailable - GPU encoder is in a bad state"
|
|
echo " Error: CUDA context creation failed"
|
|
echo " Fix: Reboot, or log out/in to release GPU resources"
|
|
elif echo "$test_result" | grep -q "No capable devices"; then
|
|
echo "WARNING: NVENC unavailable - no capable GPU devices found"
|
|
else
|
|
echo "WARNING: NVENC test encode failed"
|
|
local err_line=$(echo "$test_result" | grep -i -E 'error|failed' | head -1)
|
|
if [[ -n "$err_line" ]]; then
|
|
echo " $err_line"
|
|
fi
|
|
fi
|
|
echo ""
|
|
echo "Falling back to CPU encoder (libx265)"
|
|
ENCODER="libx265"
|
|
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
|
|
return
|
|
fi
|
|
|
|
# NVENC works - check if currently busy (use session count, not utilization %)
|
|
local enc_sessions=$(nvidia-smi --query-gpu=encoder.stats.sessionCount --format=csv,noheader 2>/dev/null | head -1)
|
|
if [[ -n "$enc_sessions" ]] && [[ "$enc_sessions" -gt 0 ]]; then
|
|
echo ""
|
|
echo "WARNING: GPU encoder has $enc_sessions active session(s)"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " 1) Press Enter to continue with GPU (may queue or fail)"
|
|
echo " 2) Type 'cpu' to use CPU encoding (slower but reliable)"
|
|
echo " 3) Press Ctrl+C to cancel"
|
|
echo ""
|
|
read -p "Choice: " enc_choice
|
|
|
|
if [[ "$enc_choice" == "cpu" ]]; then
|
|
ENCODER="libx265"
|
|
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
echo "Using NVENC GPU encoder (hevc_nvenc)"
|
|
ENCODER="hevc_nvenc"
|
|
# -cq is NVENC's quality mode (similar to CRF), p4 is balanced preset
|
|
# -gpu 0 explicitly selects first GPU
|
|
# -tag:v hvc1 ensures Apple device compatibility
|
|
ENCODER_OPTS="-preset p4 -cq $CRF_VALUE -rc vbr -gpu 0 -tag:v hvc1"
|
|
else
|
|
echo "NVENC not available, using CPU encoder (libx265)"
|
|
ENCODER="libx265"
|
|
ENCODER_OPTS="-preset medium -crf $CRF_VALUE -x265-params log-level=error"
|
|
fi
|
|
}
|
|
|
|
# Check if file is already H.265/HEVC
|
|
is_hevc() {
|
|
local file="$1"
|
|
local codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
|
|
[[ "$codec" == "hevc" ]] || [[ "$codec" == "h265" ]]
|
|
}
|
|
|
|
# Check if filename already has [E] encoded flag
|
|
has_encoded_flag() {
|
|
local file="$1"
|
|
local filename=$(basename "$file")
|
|
[[ "$filename" =~ \[E\] ]]
|
|
}
|
|
|
|
# Add [E] flag to filename (before [H/V] orientation tag, compatible with organize_by_studio.sh)
|
|
add_encoded_flag() {
|
|
local file="$1"
|
|
local dir=$(dirname "$file")
|
|
local filename=$(basename "$file")
|
|
local new_filename
|
|
|
|
# Insert [E] before [H] or [V] orientation tag if present
|
|
if [[ "$filename" =~ ^(.+)\ \[([HV])\]\.mp4$ ]]; then
|
|
local base="${BASH_REMATCH[1]}"
|
|
local orientation="${BASH_REMATCH[2]}"
|
|
new_filename="${base} [E] [${orientation}].mp4"
|
|
else
|
|
# Fallback: add before .mp4 if no orientation tag found
|
|
local name="${filename%.mp4}"
|
|
new_filename="${name} [E].mp4"
|
|
fi
|
|
|
|
local new_path="${dir}/${new_filename}"
|
|
mv "$file" "$new_path"
|
|
echo "$new_path"
|
|
}
|
|
|
|
# Extract duration in seconds from filename tags [H:MM] or [Xs]
|
|
get_duration_from_filename() {
|
|
local file="$1"
|
|
local filename=$(basename "$file")
|
|
local seconds=0
|
|
|
|
# Try [H:MM] format (e.g., [1:23] = 1 hour 23 minutes)
|
|
if [[ "$filename" =~ \[([0-9]+):([0-9]+)\] ]]; then
|
|
# Use 10# prefix to force base-10 interpretation (avoids octal errors with 08, 09)
|
|
local hours=$((10#${BASH_REMATCH[1]}))
|
|
local minutes=$((10#${BASH_REMATCH[2]}))
|
|
seconds=$((hours * 3600 + minutes * 60))
|
|
# Try [Xs] format for short clips (e.g., [45s])
|
|
elif [[ "$filename" =~ \[([0-9]+)s\] ]]; then
|
|
seconds=$((10#${BASH_REMATCH[1]}))
|
|
fi
|
|
|
|
echo "$seconds"
|
|
}
|
|
|
|
# Format seconds as human-readable time
|
|
format_time() {
|
|
local seconds=$1
|
|
local hours=$((seconds / 3600))
|
|
local minutes=$(((seconds % 3600) / 60))
|
|
local secs=$((seconds % 60))
|
|
|
|
if [[ $hours -gt 0 ]]; then
|
|
printf "%dh %dm %ds" "$hours" "$minutes" "$secs"
|
|
elif [[ $minutes -gt 0 ]]; then
|
|
printf "%dm %ds" "$minutes" "$secs"
|
|
else
|
|
printf "%ds" "$secs"
|
|
fi
|
|
}
|
|
|
|
# Format video duration for display
|
|
format_video_duration() {
|
|
local seconds=$1
|
|
local hours=$((seconds / 3600))
|
|
local minutes=$(((seconds % 3600) / 60))
|
|
|
|
if [[ $hours -gt 0 ]]; then
|
|
printf "%dh %dm" "$hours" "$minutes"
|
|
else
|
|
printf "%dm" "$minutes"
|
|
fi
|
|
}
|
|
|
|
# Check if file has faststart (moov atom at beginning)
|
|
has_faststart() {
|
|
local file="$1"
|
|
# Check if moov atom appears before mdat atom
|
|
local atoms=$(ffprobe -v error -show_format -show_entries format_tags "$file" 2>&1 | head -20)
|
|
# Simple heuristic: if file streams well, it likely has faststart
|
|
# More accurate: use atomicparsley or similar, but ffprobe works for our needs
|
|
return 0 # We'll re-encode anyway to ensure proper settings
|
|
}
|
|
|
|
# Get video info
|
|
get_video_info() {
|
|
local file="$1"
|
|
local codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
|
|
local width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
|
|
local height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
|
|
local bitrate=$(ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
|
|
|
|
if [[ -n "$bitrate" ]] && [[ "$bitrate" != "N/A" ]]; then
|
|
bitrate=$((bitrate / 1000))k
|
|
else
|
|
bitrate="unknown"
|
|
fi
|
|
|
|
echo "${codec}:${width}x${height}:${bitrate}"
|
|
}
|
|
|
|
# Format file size for display
|
|
format_size() {
|
|
local bytes=$1
|
|
if [[ $bytes -ge 1073741824 ]]; then
|
|
echo "$(echo "scale=2; $bytes / 1073741824" | bc)G"
|
|
elif [[ $bytes -ge 1048576 ]]; then
|
|
echo "$(echo "scale=2; $bytes / 1048576" | bc)M"
|
|
elif [[ $bytes -ge 1024 ]]; then
|
|
echo "$(echo "scale=2; $bytes / 1024" | bc)K"
|
|
else
|
|
echo "${bytes}B"
|
|
fi
|
|
}
|
|
|
|
# Log message to file and stdout
|
|
log() {
|
|
echo "$1" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
# Debug log function
|
|
debug_log() {
|
|
if [[ "$DEBUG" == true ]]; then
|
|
log " [DEBUG] $1"
|
|
fi
|
|
}
|
|
|
|
# Check GPU status
|
|
check_gpu_status() {
|
|
if ! command -v nvidia-smi &> /dev/null; then
|
|
debug_log "nvidia-smi not found"
|
|
return 1
|
|
fi
|
|
|
|
local gpu_info=$(nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu,encoder.stats.sessionCount --format=csv,noheader 2>/dev/null)
|
|
if [[ -n "$gpu_info" ]]; then
|
|
debug_log "GPU Status: $gpu_info"
|
|
fi
|
|
|
|
# Check encoder sessions
|
|
local enc_sessions=$(nvidia-smi --query-gpu=encoder.stats.sessionCount --format=csv,noheader 2>/dev/null | head -1)
|
|
if [[ -n "$enc_sessions" ]] && [[ "$enc_sessions" -gt 0 ]]; then
|
|
debug_log "Active encoder sessions: $enc_sessions"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Test NVENC with a quick encode
|
|
test_nvenc() {
|
|
debug_log "Testing NVENC availability..."
|
|
|
|
# Create a tiny test pattern and try to encode it
|
|
local test_output="/tmp/nvenc_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 -eq 0 ]]; then
|
|
debug_log "NVENC test: SUCCESS"
|
|
return 0
|
|
else
|
|
debug_log "NVENC test: FAILED (exit $test_exit)"
|
|
debug_log "NVENC error: $(echo "$test_result" | grep -i -E 'error|failed|cannot' | head -3)"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Main encoding function
|
|
encode_file() {
|
|
local input="$1"
|
|
local temp_output="${input}.temp.mp4"
|
|
|
|
# Get original file size
|
|
local original_size=$(stat -c%s "$input" 2>/dev/null)
|
|
if [[ -z "$original_size" ]] || [[ "$original_size" -eq 0 ]]; then
|
|
log " ERROR: Cannot read input file"
|
|
return 1
|
|
fi
|
|
|
|
local original_size_fmt=$(format_size $original_size)
|
|
local video_info=$(get_video_info "$input")
|
|
|
|
log " Original: $video_info ($original_size_fmt)"
|
|
|
|
# Check GPU status if using NVENC
|
|
if [[ "$ENCODER" == "hevc_nvenc" ]] && [[ "$DEBUG" == true ]]; then
|
|
check_gpu_status
|
|
fi
|
|
|
|
local encode_output=""
|
|
local exit_code=1
|
|
local used_encoder="$ENCODER"
|
|
local attempt=0
|
|
|
|
# GPU encoding with retries
|
|
if [[ "$ENCODER" == "hevc_nvenc" ]]; then
|
|
for ((attempt=1; attempt<=GPU_RETRIES; attempt++)); do
|
|
debug_log "GPU encode attempt $attempt/$GPU_RETRIES"
|
|
|
|
# Clean up any previous temp file
|
|
rm -f "$temp_output"
|
|
|
|
# Build ffmpeg command
|
|
local ffmpeg_cmd="ffmpeg -nostdin -hide_banner -i \"$input\" \
|
|
-c:v hevc_nvenc $ENCODER_OPTS \
|
|
-c:a aac -b:a $AUDIO_BITRATE \
|
|
-movflags +faststart \
|
|
-y \"$temp_output\""
|
|
|
|
debug_log "Command: ffmpeg -c:v hevc_nvenc $ENCODER_OPTS -c:a aac -b:a $AUDIO_BITRATE"
|
|
|
|
# Run encoding
|
|
if [[ "$DEBUG" == true ]]; then
|
|
encode_output=$(eval "$ffmpeg_cmd" 2>&1)
|
|
exit_code=$?
|
|
# Show last few lines of output in debug mode
|
|
debug_log "Exit code: $exit_code"
|
|
echo "$encode_output" | tail -5 | while read -r line; do
|
|
debug_log "ffmpeg: $line"
|
|
done
|
|
else
|
|
encode_output=$(eval "$ffmpeg_cmd" 2>&1)
|
|
exit_code=$?
|
|
fi
|
|
|
|
# Check for success
|
|
if [[ $exit_code -eq 0 ]] && [[ -f "$temp_output" ]]; then
|
|
local temp_size=$(stat -c%s "$temp_output" 2>/dev/null)
|
|
if [[ -n "$temp_size" ]] && [[ "$temp_size" -gt 0 ]]; then
|
|
debug_log "GPU encode SUCCESS on attempt $attempt"
|
|
used_encoder="hevc_nvenc"
|
|
break
|
|
fi
|
|
fi
|
|
|
|
# Parse error for debugging
|
|
local nvenc_error=$(echo "$encode_output" | grep -i -E 'error|failed|cannot|no capable|cuda' | head -1)
|
|
if [[ -n "$nvenc_error" ]]; then
|
|
debug_log "NVENC error: $nvenc_error"
|
|
fi
|
|
|
|
# Wait before retry (increasing delay)
|
|
if [[ $attempt -lt $GPU_RETRIES ]]; then
|
|
local wait_time=$((attempt * 2))
|
|
debug_log "Waiting ${wait_time}s before retry..."
|
|
sleep $wait_time
|
|
fi
|
|
done
|
|
|
|
# If all GPU attempts failed, fall back to CPU
|
|
if [[ $exit_code -ne 0 ]] || [[ ! -f "$temp_output" ]]; then
|
|
# Always show the error (not just in debug mode)
|
|
local error_summary=$(echo "$encode_output" | grep -i -E 'error|failed|cannot|cuda' | head -1)
|
|
if [[ -n "$error_summary" ]]; then
|
|
log " NVENC failed: $error_summary"
|
|
else
|
|
log " NVENC failed after $GPU_RETRIES attempts"
|
|
fi
|
|
log " Falling back to CPU encoder..."
|
|
|
|
rm -f "$temp_output"
|
|
used_encoder="libx265"
|
|
|
|
local cpu_cmd="ffmpeg -nostdin -hide_banner -i \"$input\" \
|
|
-c:v libx265 -preset medium -crf $CRF_VALUE -x265-params log-level=error \
|
|
-c:a aac -b:a $AUDIO_BITRATE \
|
|
-movflags +faststart \
|
|
-y \"$temp_output\""
|
|
|
|
debug_log "Falling back to CPU: libx265 -preset medium -crf $CRF_VALUE"
|
|
|
|
if [[ "$DEBUG" == true ]]; then
|
|
encode_output=$(eval "$cpu_cmd" 2>&1)
|
|
exit_code=$?
|
|
debug_log "CPU encode exit code: $exit_code"
|
|
else
|
|
encode_output=$(eval "$cpu_cmd" 2>&1)
|
|
exit_code=$?
|
|
fi
|
|
fi
|
|
else
|
|
# CPU encoding (no retries needed)
|
|
debug_log "Using CPU encoder directly"
|
|
|
|
local cpu_cmd="ffmpeg -nostdin -hide_banner -i \"$input\" \
|
|
-c:v $ENCODER $ENCODER_OPTS \
|
|
-c:a aac -b:a $AUDIO_BITRATE \
|
|
-movflags +faststart \
|
|
-y \"$temp_output\""
|
|
|
|
if [[ "$DEBUG" == true ]]; then
|
|
encode_output=$(eval "$cpu_cmd" 2>&1)
|
|
exit_code=$?
|
|
debug_log "CPU encode exit code: $exit_code"
|
|
else
|
|
encode_output=$(eval "$cpu_cmd" 2>&1)
|
|
exit_code=$?
|
|
fi
|
|
used_encoder="$ENCODER"
|
|
fi
|
|
|
|
# Verify encoding success
|
|
if [[ $exit_code -ne 0 ]] || [[ ! -f "$temp_output" ]]; then
|
|
log " ERROR: Encoding failed (exit code: $exit_code)"
|
|
if [[ "$DEBUG" == true ]]; then
|
|
echo "$encode_output" | grep -i -E 'error|failed|invalid' | head -3 | while read -r line; do
|
|
debug_log "Error detail: $line"
|
|
done
|
|
fi
|
|
rm -f "$temp_output"
|
|
return 1
|
|
fi
|
|
|
|
# Check output file size
|
|
local new_size=$(stat -c%s "$temp_output" 2>/dev/null)
|
|
if [[ -z "$new_size" ]] || [[ "$new_size" -eq 0 ]]; then
|
|
log " ERROR: Output file is empty or missing"
|
|
rm -f "$temp_output"
|
|
return 1
|
|
fi
|
|
local new_size_fmt=$(format_size $new_size)
|
|
|
|
# Verify file isn't suspiciously small (encoding failure indicator)
|
|
local size_ratio=$(echo "scale=4; $new_size / $original_size" | bc)
|
|
if (( $(echo "$size_ratio < $MIN_SIZE_RATIO" | bc -l) )); then
|
|
log " ERROR: Output file too small ($(format_size $new_size)) - likely corrupted"
|
|
rm -f "$temp_output"
|
|
return 1
|
|
fi
|
|
|
|
# Verify output is playable
|
|
local verify_output=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$temp_output" 2>&1)
|
|
if [[ -z "$verify_output" ]] || [[ "$verify_output" == *"Invalid"* ]]; then
|
|
log " ERROR: Output file verification failed"
|
|
rm -f "$temp_output"
|
|
return 1
|
|
fi
|
|
|
|
# Calculate space savings
|
|
local saved=$((original_size - new_size))
|
|
local saved_fmt=$(format_size $saved)
|
|
local percent_saved=$(echo "scale=1; 100 - ($new_size * 100 / $original_size)" | bc)
|
|
|
|
# Replace original with encoded file
|
|
mv "$temp_output" "$input"
|
|
|
|
if [[ $saved -gt 0 ]]; then
|
|
log " Encoded: hevc/$used_encoder ($new_size_fmt) - saved $saved_fmt ($percent_saved%)"
|
|
else
|
|
log " Encoded: hevc/$used_encoder ($new_size_fmt) - increased by $(format_size $((-saved)))"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Show usage
|
|
show_usage() {
|
|
echo ""
|
|
echo "Usage: $0 [OPTIONS]"
|
|
echo ""
|
|
echo "Re-encodes video files to H.265/HEVC for streaming optimization."
|
|
echo "Adds [E] flag to filename after successful encoding."
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --dry-run Show what would be encoded without making changes"
|
|
echo " --cpu Force CPU encoding (skip NVENC, slower but reliable)"
|
|
echo " --debug Show verbose ffmpeg output and GPU diagnostics"
|
|
echo " --skip-hevc Skip files already encoded in H.265/HEVC (default)"
|
|
echo " --force-all Re-encode all files, even if already HEVC"
|
|
echo " --dir PATH Process only files in specified directory"
|
|
echo " --single FILE Process only a single file"
|
|
echo " --help Show this help message"
|
|
echo ""
|
|
echo "Files with [E] in filename are always skipped."
|
|
echo ""
|
|
}
|
|
|
|
# Parse arguments
|
|
DRY_RUN=false
|
|
SKIP_HEVC=true # Default: skip already-encoded HEVC files
|
|
FORCE_CPU=false
|
|
DEBUG=false
|
|
GPU_RETRIES=3 # Number of times to retry GPU encoding before falling back
|
|
TARGET_DIR=""
|
|
SINGLE_FILE=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
--skip-hevc)
|
|
SKIP_HEVC=true
|
|
shift
|
|
;;
|
|
--force-all)
|
|
SKIP_HEVC=false
|
|
shift
|
|
;;
|
|
--cpu)
|
|
FORCE_CPU=true
|
|
shift
|
|
;;
|
|
--debug)
|
|
DEBUG=true
|
|
shift
|
|
;;
|
|
--dir)
|
|
TARGET_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--single)
|
|
SINGLE_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--help)
|
|
show_usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Detect encoder
|
|
detect_encoder
|
|
|
|
# Run NVENC test if debug mode and using GPU
|
|
if [[ "$DEBUG" == true ]] && [[ "$ENCODER" == "hevc_nvenc" ]]; then
|
|
echo ""
|
|
echo "[DEBUG] Running NVENC diagnostics..."
|
|
check_gpu_status
|
|
if ! test_nvenc; then
|
|
echo "[DEBUG] WARNING: NVENC test failed - encoding may fall back to CPU"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "========================================="
|
|
echo " Streaming-Optimized Re-encoder"
|
|
echo "========================================="
|
|
echo " Encoder: $ENCODER (H.265/HEVC)"
|
|
echo " Quality: CRF $CRF_VALUE (web-optimized)"
|
|
echo " Audio: AAC @ $AUDIO_BITRATE"
|
|
echo " Mode: In-place replacement + [E] flag"
|
|
if [[ "$SKIP_HEVC" == true ]]; then
|
|
echo " Skipping: Files with [E] flag or HEVC codec"
|
|
fi
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo " DRY RUN: No files will be modified"
|
|
fi
|
|
if [[ "$DEBUG" == true ]]; then
|
|
echo " DEBUG MODE: Verbose output enabled"
|
|
echo " GPU Retries: $GPU_RETRIES attempts before CPU fallback"
|
|
fi
|
|
echo " Log: $LOG_FILE"
|
|
echo "========================================="
|
|
echo ""
|
|
|
|
# Initialize counters
|
|
total_files=0
|
|
processed_files=0
|
|
skipped_files=0
|
|
flagged_files=0
|
|
failed_files=0
|
|
total_saved=0
|
|
total_video_duration=0
|
|
encoded_video_duration=0
|
|
start_time=$(date +%s)
|
|
|
|
# Build file list
|
|
if [[ -n "$SINGLE_FILE" ]]; then
|
|
files=("$SINGLE_FILE")
|
|
elif [[ -n "$TARGET_DIR" ]]; then
|
|
mapfile -t files < <(find "$TARGET_DIR" -type f -name "*.mp4" ! -name "*.temp.mp4" | sort)
|
|
else
|
|
# Find all MP4 files, excluding certain directories and temp files
|
|
mapfile -t files < <(find . -type f -name "*.mp4" \
|
|
! -name "*.temp.mp4" \
|
|
! -path "*/scripts/*" \
|
|
! -path "*/.temp/*" \
|
|
| sort)
|
|
fi
|
|
|
|
total_files=${#files[@]}
|
|
log "Found $total_files MP4 files to process"
|
|
|
|
# Pre-scan to calculate total duration needing encoding
|
|
echo "Scanning files to estimate encoding time..."
|
|
files_to_encode=0
|
|
for file in "${files[@]}"; do
|
|
# Skip files with [E] flag
|
|
if has_encoded_flag "$file"; then
|
|
continue
|
|
fi
|
|
# Skip HEVC files (they just get flagged, no encoding)
|
|
if [[ "$SKIP_HEVC" == true ]] && is_hevc "$file"; then
|
|
continue
|
|
fi
|
|
# This file needs encoding - add its duration
|
|
duration=$(get_duration_from_filename "$file")
|
|
total_video_duration=$((total_video_duration + duration))
|
|
((files_to_encode++))
|
|
done
|
|
|
|
if [[ $files_to_encode -gt 0 ]]; then
|
|
log "Files to encode: $files_to_encode ($(format_video_duration $total_video_duration) of video)"
|
|
else
|
|
log "No files need encoding"
|
|
fi
|
|
log ""
|
|
|
|
# Confirmation prompt (skip in dry-run mode)
|
|
if [[ "$DRY_RUN" == false ]] && [[ $files_to_encode -gt 0 ]]; then
|
|
echo ""
|
|
echo "Ready to start encoding $files_to_encode files."
|
|
echo "This will replace original files with H.265/HEVC versions."
|
|
echo ""
|
|
read -p "Proceed? [y/N] " confirm
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
echo "Cancelled."
|
|
exit 0
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# Process each file
|
|
for file in "${files[@]}"; do
|
|
((processed_files++))
|
|
|
|
# Clean up path for display
|
|
display_path="${file#./}"
|
|
|
|
log "[$processed_files/$total_files] $display_path"
|
|
|
|
# Check if already has [E] encoded flag
|
|
if has_encoded_flag "$file"; then
|
|
log " Skipped: Already has [E] flag"
|
|
((skipped_files++))
|
|
continue
|
|
fi
|
|
|
|
# Check if already HEVC (but no [E] flag - add the flag)
|
|
if [[ "$SKIP_HEVC" == true ]] && is_hevc "$file"; then
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
log " Would add [E] flag: Already HEVC encoded"
|
|
else
|
|
new_path=$(add_encoded_flag "$file")
|
|
log " Added [E] flag: Already HEVC encoded -> $(basename "$new_path")"
|
|
((flagged_files++))
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
# Get original size for space savings calculation
|
|
original_size=$(stat -c%s "$file")
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
video_info=$(get_video_info "$file")
|
|
log " Would encode: $video_info ($(format_size $original_size))"
|
|
continue
|
|
fi
|
|
|
|
# Get video duration for ETA calculation
|
|
file_duration=$(get_duration_from_filename "$file")
|
|
|
|
# Encode the file
|
|
encode_start=$(date +%s)
|
|
if encode_file "$file"; then
|
|
encode_end=$(date +%s)
|
|
new_size=$(stat -c%s "$file")
|
|
saved=$((original_size - new_size))
|
|
total_saved=$((total_saved + saved))
|
|
|
|
# Track encoded duration for ETA
|
|
encoded_video_duration=$((encoded_video_duration + file_duration))
|
|
|
|
# Add [E] flag to filename
|
|
new_path=$(add_encoded_flag "$file")
|
|
log " Renamed: $(basename "$new_path")"
|
|
|
|
# Calculate and show ETA
|
|
if [[ $encoded_video_duration -gt 0 ]] && [[ $total_video_duration -gt 0 ]]; then
|
|
elapsed_so_far=$((encode_end - start_time))
|
|
remaining_video=$((total_video_duration - encoded_video_duration))
|
|
|
|
if [[ $elapsed_so_far -gt 0 ]]; then
|
|
# Calculate encoding speed (video seconds per real second)
|
|
speed=$(echo "scale=2; $encoded_video_duration / $elapsed_so_far" | bc)
|
|
# Estimate remaining time
|
|
if (( $(echo "$speed > 0" | bc -l) )); then
|
|
eta_seconds=$(echo "scale=0; $remaining_video / $speed" | bc)
|
|
log " ETA: $(format_time $eta_seconds) remaining ($(format_video_duration $remaining_video) of video left, ${speed}x speed)"
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
((failed_files++))
|
|
log " FAILED - file unchanged"
|
|
fi
|
|
|
|
echo ""
|
|
done
|
|
|
|
# Calculate elapsed time
|
|
end_time=$(date +%s)
|
|
elapsed=$((end_time - start_time))
|
|
hours=$((elapsed / 3600))
|
|
minutes=$(((elapsed % 3600) / 60))
|
|
seconds=$((elapsed % 60))
|
|
|
|
# Summary
|
|
echo ""
|
|
echo "========================================="
|
|
echo " Re-encoding Complete!"
|
|
echo "========================================="
|
|
log " Total files: $total_files"
|
|
log " Re-encoded: $((processed_files - skipped_files - flagged_files - failed_files))"
|
|
log " Flagged (HEVC + added [E]): $flagged_files"
|
|
log " Skipped (already has [E]): $skipped_files"
|
|
log " Failed: $failed_files"
|
|
if [[ $encoded_video_duration -gt 0 ]]; then
|
|
log " Video encoded: $(format_video_duration $encoded_video_duration)"
|
|
if [[ $elapsed -gt 0 ]]; then
|
|
avg_speed=$(echo "scale=2; $encoded_video_duration / $elapsed" | bc)
|
|
log " Average speed: ${avg_speed}x realtime"
|
|
fi
|
|
fi
|
|
if [[ $total_saved -gt 0 ]]; then
|
|
log " Total space saved: $(format_size $total_saved)"
|
|
elif [[ $total_saved -lt 0 ]]; then
|
|
log " Total size increase: $(format_size $((-total_saved)))"
|
|
fi
|
|
log " Time elapsed: ${hours}h ${minutes}m ${seconds}s"
|
|
log " Log saved to: $LOG_FILE"
|
|
echo "========================================="
|
|
echo ""
|