Update
This commit is contained in:
0
bash/webm_
Normal file
0
bash/webm_
Normal file
352
bash/webm_auto_crop.sh
Executable file
352
bash/webm_auto_crop.sh
Executable file
@@ -0,0 +1,352 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to auto-crop and scale WebM files based on content analysis of all frames
|
||||
# Usage: ./webm_auto_crop.sh [directory_path]
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 [directory_path]"
|
||||
echo " directory_path: Path to directory containing WebM files (default: current directory)"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " - Analyze frames in each WebM file to detect content boundaries"
|
||||
echo " - Calculate unified crop region that covers all frames"
|
||||
echo " - Crop and scale the entire video to fill original dimensions"
|
||||
echo " - Preserve audio when available"
|
||||
echo " - Save results in a 'crop' subdirectory"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if ffmpeg is installed
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "Error: ffmpeg is not installed or not in PATH"
|
||||
echo "Please install ffmpeg to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if ffprobe is installed
|
||||
if ! command -v ffprobe &> /dev/null; then
|
||||
echo "Error: ffprobe is not installed or not in PATH"
|
||||
echo "Please install ffprobe (part of ffmpeg) to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if bc is installed (needed for calculations)
|
||||
if ! command -v bc &> /dev/null; then
|
||||
echo "Error: bc (basic calculator) is not installed or not in PATH"
|
||||
echo "Please install bc to use this script"
|
||||
echo " macOS: brew install bc"
|
||||
echo " Ubuntu/Debian: sudo apt install bc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Content detection method:"
|
||||
if command -v identify &> /dev/null && command -v convert &> /dev/null; then
|
||||
echo " ✓ Using ImageMagick (optimal for transparency)"
|
||||
else
|
||||
echo " ⚠ Using ffmpeg cropdetect (fallback method)"
|
||||
echo " For better results with transparent content, install ImageMagick:"
|
||||
echo " macOS: brew install imagemagick"
|
||||
echo " Ubuntu/Debian: sudo apt install imagemagick"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Set directory path (use current directory if not specified)
|
||||
if [ $# -eq 0 ]; then
|
||||
DIRECTORY="."
|
||||
elif [ $# -eq 1 ]; then
|
||||
DIRECTORY="$1"
|
||||
else
|
||||
echo "Error: Too many arguments"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$DIRECTORY" ]; then
|
||||
echo "Error: Directory '$DIRECTORY' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert to absolute path
|
||||
DIRECTORY=$(realpath "$DIRECTORY")
|
||||
echo "Processing WebM files in: $DIRECTORY"
|
||||
|
||||
# Create crop output directory
|
||||
CROP_DIR="$DIRECTORY/crop"
|
||||
if [ ! -d "$CROP_DIR" ]; then
|
||||
mkdir -p "$CROP_DIR"
|
||||
echo "Created output directory: $CROP_DIR"
|
||||
else
|
||||
echo "Using existing output directory: $CROP_DIR"
|
||||
fi
|
||||
|
||||
# Find all WebM files in the directory
|
||||
WEBM_FILES=($(find "$DIRECTORY" -maxdepth 1 -name "*.webm" -type f))
|
||||
|
||||
# Check if any WebM files were found
|
||||
if [ ${#WEBM_FILES[@]} -eq 0 ]; then
|
||||
echo "No WebM files found in '$DIRECTORY'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#WEBM_FILES[@]} WebM file(s)"
|
||||
echo ""
|
||||
|
||||
# Counter for processed files
|
||||
PROCESSED=0
|
||||
FAILED=0
|
||||
|
||||
# Function to detect content bounding box for a frame with transparency
|
||||
detect_content_bounds() {
|
||||
local frame_file="$1"
|
||||
local bounds_file="$2"
|
||||
|
||||
# Use ImageMagick to find the bounding box of non-transparent content
|
||||
if command -v identify &> /dev/null && command -v convert &> /dev/null; then
|
||||
# Get the trim bounding box (removes transparent borders) - works with WebP
|
||||
TRIM_INFO=$(convert "$frame_file" -format "%@" info: 2>/dev/null)
|
||||
if [ -n "$TRIM_INFO" ] && [[ "$TRIM_INFO" =~ ^([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+)$ ]]; then
|
||||
# Parse geometry: WxH+X+Y
|
||||
echo "${BASH_REMATCH[1]}:${BASH_REMATCH[2]}:${BASH_REMATCH[3]}:${BASH_REMATCH[4]}" > "$bounds_file"
|
||||
else
|
||||
echo "" > "$bounds_file"
|
||||
fi
|
||||
else
|
||||
# Fallback: use ffmpeg cropdetect with adjusted threshold for transparency
|
||||
ffmpeg -i "$frame_file" \
|
||||
-vf "cropdetect=limit=0.1:round=2" \
|
||||
-f null \
|
||||
- 2>&1 | grep "crop=" | tail -1 | sed 's/.*crop=\([0-9:]*\).*/\1/' > "$bounds_file" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to parse crop parameters
|
||||
parse_crop() {
|
||||
local crop_str="$1"
|
||||
if [[ "$crop_str" =~ ^([0-9]+):([0-9]+):([0-9]+):([0-9]+)$ ]]; then
|
||||
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]}"
|
||||
else
|
||||
echo "0 0 0 0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Process each WebM file
|
||||
for webm_file in "${WEBM_FILES[@]}"; do
|
||||
# Get the base filename without path and extension
|
||||
base_name=$(basename "$webm_file" .webm)
|
||||
|
||||
# Output WebM file path
|
||||
webm_output="$CROP_DIR/${base_name}.webm"
|
||||
|
||||
echo "Processing: $(basename "$webm_file")"
|
||||
|
||||
# Detect codec type (VP8 or VP9) to use correct decoder
|
||||
CODEC_INFO=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
# Set appropriate decoder based on codec
|
||||
if [ "$CODEC_INFO" = "vp9" ]; then
|
||||
DECODER="libvpx-vp9"
|
||||
ENCODER="libvpx-vp9"
|
||||
else
|
||||
DECODER="libvpx"
|
||||
ENCODER="libvpx"
|
||||
fi
|
||||
|
||||
# Get video properties - use EXACT same method as webm_to_webp.sh
|
||||
ORIG_WIDTH=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=width -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
ORIG_HEIGHT=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=height -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
if [ -z "$ORIG_WIDTH" ] || [ -z "$ORIG_HEIGHT" ]; then
|
||||
echo " ✗ Could not get video dimensions"
|
||||
((FAILED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " → Original dimensions: ${ORIG_WIDTH}x${ORIG_HEIGHT}"
|
||||
|
||||
# Get total frame count - EXACT same method as webm_to_webp.sh
|
||||
FRAME_COUNT=$(ffprobe -v quiet -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
if [ -z "$FRAME_COUNT" ] || [ "$FRAME_COUNT" -eq 0 ]; then
|
||||
echo " ✗ Could not determine frame count"
|
||||
((FAILED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " → Total frames: $FRAME_COUNT"
|
||||
|
||||
# Check if video has audio
|
||||
AUDIO_STREAMS=$(ffprobe -v quiet -select_streams a -show_entries stream=index -of csv=p=0 "$webm_file" 2>/dev/null | wc -l)
|
||||
|
||||
# Create temporary directory for frame analysis
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
|
||||
echo " → Analyzing frames to find optimal crop region..."
|
||||
|
||||
# Calculate sample count - sample every 3rd frame to ensure we catch all extreme positions
|
||||
SAMPLE_COUNT=$((FRAME_COUNT / 3))
|
||||
if [ "$SAMPLE_COUNT" -gt 300 ]; then
|
||||
SAMPLE_COUNT=300 # Cap at 300 to avoid excessive processing
|
||||
elif [ "$SAMPLE_COUNT" -lt 50 ]; then
|
||||
SAMPLE_COUNT=$FRAME_COUNT # For very short videos, sample all frames
|
||||
fi
|
||||
|
||||
# Initialize crop bounds (will find union of all bounds)
|
||||
MIN_X=$ORIG_WIDTH
|
||||
MIN_Y=$ORIG_HEIGHT
|
||||
MAX_X=0
|
||||
MAX_Y=0
|
||||
VALID_FRAMES=0
|
||||
|
||||
echo " → Analyzing $SAMPLE_COUNT sample frames from $FRAME_COUNT total frames..."
|
||||
|
||||
# Sample frames evenly throughout the video with much higher density
|
||||
for ((i=0; i<SAMPLE_COUNT; i++)); do
|
||||
# Calculate frame position (evenly distributed)
|
||||
FRAME_POS=$((i * FRAME_COUNT / SAMPLE_COUNT))
|
||||
|
||||
# Extract frame at specific position
|
||||
TEMP_FRAME="$TEMP_DIR/frame_${FRAME_POS}.webp"
|
||||
BOUNDS_FILE="$TEMP_DIR/bounds_${FRAME_POS}.txt"
|
||||
|
||||
echo " Extracting frame #${FRAME_POS}..."
|
||||
|
||||
# Extract frame - EXACT same command as webm_to_webp.sh
|
||||
TEMP_FRAME="$TEMP_DIR/frame_${FRAME_POS}.webp"
|
||||
ffmpeg -c:v "$DECODER" \
|
||||
-i "$webm_file" \
|
||||
-vf "select=eq(n\\,$FRAME_POS)" \
|
||||
-vframes 1 \
|
||||
-c:v libwebp \
|
||||
-lossless 1 \
|
||||
-y \
|
||||
"$TEMP_FRAME" \
|
||||
-v quiet 2>/dev/null
|
||||
|
||||
if [ -f "$TEMP_FRAME" ]; then
|
||||
# Check if frame was extracted successfully
|
||||
FRAME_SIZE=$(stat -f%z "$TEMP_FRAME" 2>/dev/null || stat -c%s "$TEMP_FRAME" 2>/dev/null)
|
||||
echo " Frame file size: $FRAME_SIZE bytes"
|
||||
|
||||
# Detect content bounds
|
||||
detect_content_bounds "$TEMP_FRAME" "$BOUNDS_FILE"
|
||||
|
||||
if [ -f "$BOUNDS_FILE" ]; then
|
||||
CROP_PARAMS=$(cat "$BOUNDS_FILE")
|
||||
echo " Crop params: '$CROP_PARAMS'"
|
||||
|
||||
if [ -n "$CROP_PARAMS" ] && [[ "$CROP_PARAMS" =~ ^([0-9]+):([0-9]+):([0-9]+):([0-9]+)$ ]]; then
|
||||
read -r CROP_W CROP_H CROP_X CROP_Y <<< $(parse_crop "$CROP_PARAMS")
|
||||
|
||||
if [ "$CROP_W" -gt 0 ] && [ "$CROP_H" -gt 0 ]; then
|
||||
# Calculate bounds of this frame's content
|
||||
FRAME_MIN_X=$CROP_X
|
||||
FRAME_MIN_Y=$CROP_Y
|
||||
FRAME_MAX_X=$((CROP_X + CROP_W))
|
||||
FRAME_MAX_Y=$((CROP_Y + CROP_H))
|
||||
|
||||
# Update union bounds
|
||||
if [ "$FRAME_MIN_X" -lt "$MIN_X" ]; then MIN_X=$FRAME_MIN_X; fi
|
||||
if [ "$FRAME_MIN_Y" -lt "$MIN_Y" ]; then MIN_Y=$FRAME_MIN_Y; fi
|
||||
if [ "$FRAME_MAX_X" -gt "$MAX_X" ]; then MAX_X=$FRAME_MAX_X; fi
|
||||
if [ "$FRAME_MAX_Y" -gt "$MAX_Y" ]; then MAX_Y=$FRAME_MAX_Y; fi
|
||||
|
||||
((VALID_FRAMES++))
|
||||
echo " ✓ Frame #${FRAME_POS}: ${CROP_W}x${CROP_H} at (${CROP_X},${CROP_Y})"
|
||||
else
|
||||
echo " ✗ Frame #${FRAME_POS}: Invalid crop dimensions"
|
||||
fi
|
||||
else
|
||||
echo " ✗ Frame #${FRAME_POS}: No valid crop params detected"
|
||||
fi
|
||||
else
|
||||
echo " ✗ Frame #${FRAME_POS}: Bounds file not created"
|
||||
fi
|
||||
else
|
||||
echo " ✗ Frame #${FRAME_POS}: Extraction failed"
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo " → Valid frames found: $VALID_FRAMES out of $SAMPLE_COUNT"
|
||||
|
||||
# Calculate final crop dimensions
|
||||
FINAL_CROP_W=$((MAX_X - MIN_X))
|
||||
FINAL_CROP_H=$((MAX_Y - MIN_Y))
|
||||
|
||||
# Validate crop dimensions
|
||||
if [ "$VALID_FRAMES" -eq 0 ]; then
|
||||
echo " ✗ No content detected in any frame - video may be fully transparent"
|
||||
((FAILED++))
|
||||
continue
|
||||
elif [ "$FINAL_CROP_W" -le 0 ] || [ "$FINAL_CROP_H" -le 0 ] || [ "$MIN_X" -lt 0 ] || [ "$MIN_Y" -lt 0 ]; then
|
||||
echo " ✗ Could not determine valid crop region (bounds: ${MIN_X},${MIN_Y} to ${MAX_X},${MAX_Y})"
|
||||
((FAILED++))
|
||||
continue
|
||||
elif [ "$FINAL_CROP_W" -ge "$ORIG_WIDTH" ] && [ "$FINAL_CROP_H" -ge "$ORIG_HEIGHT" ]; then
|
||||
echo " ⚠ Content already fills entire frame - skipping crop"
|
||||
((FAILED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " → Crop region: ${FINAL_CROP_W}x${FINAL_CROP_H} at offset (${MIN_X},${MIN_Y})"
|
||||
|
||||
# Calculate scale to fit inside original dimensions while maintaining aspect ratio
|
||||
SCALE_W=$(echo "scale=6; $ORIG_WIDTH / $FINAL_CROP_W" | bc)
|
||||
SCALE_H=$(echo "scale=6; $ORIG_HEIGHT / $FINAL_CROP_H" | bc)
|
||||
|
||||
# Use the smaller scale factor to ensure content fits completely inside
|
||||
if (( $(echo "$SCALE_W < $SCALE_H" | bc -l) )); then
|
||||
SCALE_FACTOR=$SCALE_W
|
||||
else
|
||||
SCALE_FACTOR=$SCALE_H
|
||||
fi
|
||||
|
||||
# Calculate scaled dimensions
|
||||
SCALED_W=$(echo "$FINAL_CROP_W * $SCALE_FACTOR" | bc | cut -d. -f1)
|
||||
SCALED_H=$(echo "$FINAL_CROP_H * $SCALE_FACTOR" | bc | cut -d. -f1)
|
||||
|
||||
echo " → Scale factor: ${SCALE_FACTOR}x (content will be ${SCALED_W}x${SCALED_H})"
|
||||
|
||||
# Build ffmpeg filter chain - crop, scale to fit, then pad to original size
|
||||
FILTER_CHAIN="crop=${FINAL_CROP_W}:${FINAL_CROP_H}:${MIN_X}:${MIN_Y},scale=${SCALED_W}:${SCALED_H}:flags=lanczos,pad=${ORIG_WIDTH}:${ORIG_HEIGHT}:(ow-iw)/2:(oh-ih)/2:color=0x00000000"
|
||||
|
||||
# Build ffmpeg command
|
||||
if [ "$AUDIO_STREAMS" -gt 0 ]; then
|
||||
echo " → Processing with audio..."
|
||||
FFMPEG_CMD="ffmpeg -c:v $DECODER -i \"$webm_file\" -vf \"$FILTER_CHAIN\" -c:v $ENCODER -b:v 1M -c:a libopus -b:a 128k -y \"$webm_output\""
|
||||
else
|
||||
echo " → Processing without audio..."
|
||||
FFMPEG_CMD="ffmpeg -c:v $DECODER -i \"$webm_file\" -vf \"$FILTER_CHAIN\" -c:v $ENCODER -b:v 1M -an -y \"$webm_output\""
|
||||
fi
|
||||
|
||||
# Execute ffmpeg command
|
||||
echo " → Running command: $FFMPEG_CMD"
|
||||
if eval "$FFMPEG_CMD" -v error -stats; then
|
||||
echo " ✓ Successfully created: ${base_name}.webm"
|
||||
((PROCESSED++))
|
||||
else
|
||||
echo " ✗ Failed to process: $(basename "$webm_file")"
|
||||
((FAILED++))
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo "=== Processing Complete ==="
|
||||
echo "Successfully processed: $PROCESSED files"
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo "Failed to process: $FAILED files"
|
||||
fi
|
||||
echo "Output directory: $CROP_DIR"
|
||||
echo ""
|
||||
|
||||
# List generated files
|
||||
if [ $PROCESSED -gt 0 ]; then
|
||||
echo "Generated cropped WebM files:"
|
||||
ls -la "$CROP_DIR"/*.webm 2>/dev/null | while read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
350
bash/webm_compress.sh
Executable file
350
bash/webm_compress.sh
Executable file
@@ -0,0 +1,350 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to compress WebM files using VP9 codec
|
||||
# Usage: ./webm_compress.sh [directory_path] [compression_level] [audio_mode] [fps]
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 [directory_path] [compression_level] [audio_mode] [fps]"
|
||||
echo " directory_path: Path to directory containing WebM files (default: current directory)"
|
||||
echo " compression_level: Compression level 3-5 (default: 3)"
|
||||
echo " - 3: Lossless, best compression (default)"
|
||||
echo " - 4: Lossy, high quality (CRF 23)"
|
||||
echo " - 5: Lossy, maximum compression (CRF 35)"
|
||||
echo " audio_mode: Audio channel mode (default: stereo)"
|
||||
echo " - stereo: Keep stereo audio (2 channels)"
|
||||
echo " - mono: Convert to mono audio (1 channel, smaller files)"
|
||||
echo " fps: Target frame rate (default: 30)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Current directory, lossless, stereo, 30fps"
|
||||
echo " $0 /path/to/files # Custom directory, defaults for other params"
|
||||
echo " $0 /path/to/files 4 # High quality lossy compression"
|
||||
echo " $0 /path/to/files 4 mono # High quality + mono audio"
|
||||
echo " $0 /path/to/files 4 mono 15 # High quality + mono + 15fps"
|
||||
echo " $0 /path/to/files 5 stereo 24 # Max compression + stereo + 24fps"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " - Find all .webm files in the specified directory"
|
||||
echo " - Compress using VP9 codec with specified quality level"
|
||||
echo " - Preserve alpha transparency when present"
|
||||
echo " - Adjust frame rate and audio channels as specified"
|
||||
echo " - Save results in a 'webm_compressed' subdirectory"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to format file size in human-readable format
|
||||
format_size() {
|
||||
local size=$1
|
||||
if command -v numfmt &> /dev/null; then
|
||||
numfmt --to=iec "$size"
|
||||
else
|
||||
# Fallback function for systems without numfmt
|
||||
if [ "$size" -lt 1024 ]; then
|
||||
echo "${size}B"
|
||||
elif [ "$size" -lt 1048576 ]; then
|
||||
echo "$(( size / 1024 ))K"
|
||||
elif [ "$size" -lt 1073741824 ]; then
|
||||
echo "$(( size / 1048576 ))M"
|
||||
else
|
||||
echo "$(( size / 1073741824 ))G"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if ffmpeg is installed
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "Error: ffmpeg is not installed or not in PATH"
|
||||
echo "Please install ffmpeg to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if libvpx decoders are available (needed for alpha channel support)
|
||||
LIBVPX_CHECK=$(ffmpeg -decoders 2>/dev/null | grep libvpx)
|
||||
if [ -z "$LIBVPX_CHECK" ]; then
|
||||
echo "Warning: libvpx decoders not found. Alpha channels may not be preserved properly."
|
||||
echo "Your ffmpeg installation may not support VP8/VP9 alpha transparency."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if bc is available (needed for floating point calculations)
|
||||
if ! command -v bc &> /dev/null; then
|
||||
echo "Error: bc (basic calculator) is not installed or not in PATH"
|
||||
echo "Please install bc to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set default values
|
||||
DIRECTORY="."
|
||||
COMPRESSION=3
|
||||
AUDIO_MODE="stereo"
|
||||
FPS=30
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
DIRECTORY="."
|
||||
elif [ $# -eq 1 ]; then
|
||||
DIRECTORY="$1"
|
||||
elif [ $# -eq 2 ]; then
|
||||
DIRECTORY="$1"
|
||||
COMPRESSION="$2"
|
||||
elif [ $# -eq 3 ]; then
|
||||
DIRECTORY="$1"
|
||||
COMPRESSION="$2"
|
||||
AUDIO_MODE="$3"
|
||||
elif [ $# -eq 4 ]; then
|
||||
DIRECTORY="$1"
|
||||
COMPRESSION="$2"
|
||||
AUDIO_MODE="$3"
|
||||
FPS="$4"
|
||||
else
|
||||
echo "Error: Too many arguments"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Validate compression parameter
|
||||
if ! [[ "$COMPRESSION" =~ ^[3-5]$ ]]; then
|
||||
echo "Error: compression level must be between 3-5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate audio mode parameter
|
||||
if [ "$AUDIO_MODE" != "stereo" ] && [ "$AUDIO_MODE" != "mono" ]; then
|
||||
echo "Error: audio_mode must be 'stereo' or 'mono'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate fps parameter
|
||||
if ! [[ "$FPS" =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$FPS <= 0" | bc -l) )); then
|
||||
echo "Error: fps must be a positive number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set compression values based on level
|
||||
case $COMPRESSION in
|
||||
3) VP9_PARAMS="-c:v libvpx-vp9 -lossless 1 -cpu-used 0"; COMP_DESC="Lossless, best compression" ;;
|
||||
4) VP9_PARAMS="-c:v libvpx-vp9 -crf 23 -b:v 0 -cpu-used 1"; COMP_DESC="Lossy, high quality (CRF 23)" ;;
|
||||
5) VP9_PARAMS="-c:v libvpx-vp9 -crf 35 -b:v 0 -cpu-used 2"; COMP_DESC="Lossy, maximum compression (CRF 35)" ;;
|
||||
esac
|
||||
|
||||
# Set audio parameters based on mode
|
||||
if [ "$AUDIO_MODE" = "mono" ]; then
|
||||
AUDIO_PARAMS="-c:a libopus -ac 1 -b:a 64k"
|
||||
AUDIO_DESC="mono (64k)"
|
||||
else
|
||||
AUDIO_PARAMS="-c:a libopus -ac 2 -b:a 128k"
|
||||
AUDIO_DESC="stereo (128k)"
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$DIRECTORY" ]; then
|
||||
echo "Error: Directory '$DIRECTORY' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert to absolute path
|
||||
DIRECTORY=$(realpath "$DIRECTORY")
|
||||
echo "Processing WebM files in: $DIRECTORY"
|
||||
echo "Compression: Level $COMPRESSION ($COMP_DESC)"
|
||||
echo "Audio: $AUDIO_DESC"
|
||||
echo "Target FPS: $FPS"
|
||||
|
||||
# Create compressed output directory
|
||||
COMPRESSED_DIR="$DIRECTORY/webm_compressed"
|
||||
if [ ! -d "$COMPRESSED_DIR" ]; then
|
||||
mkdir -p "$COMPRESSED_DIR"
|
||||
echo "Created output directory: $COMPRESSED_DIR"
|
||||
else
|
||||
echo "Using existing output directory: $COMPRESSED_DIR"
|
||||
fi
|
||||
|
||||
# Find all WebM files in the directory
|
||||
WEBM_FILES=($(find "$DIRECTORY" -maxdepth 1 -name "*.webm" -type f))
|
||||
|
||||
# Check if any WebM files were found
|
||||
if [ ${#WEBM_FILES[@]} -eq 0 ]; then
|
||||
echo "No WebM files found in '$DIRECTORY'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#WEBM_FILES[@]} WebM file(s)"
|
||||
echo "Note: Existing compressed files will be overwritten"
|
||||
echo ""
|
||||
|
||||
# Counter for processed files
|
||||
PROCESSED=0
|
||||
FAILED=0
|
||||
OVERWRITTEN=0
|
||||
NEW_FILES=0
|
||||
TOTAL_ORIGINAL_SIZE=0
|
||||
TOTAL_COMPRESSED_SIZE=0
|
||||
|
||||
# Process each WebM file
|
||||
for webm_file in "${WEBM_FILES[@]}"; do
|
||||
# Get the base filename without path and extension
|
||||
base_name=$(basename "$webm_file" .webm)
|
||||
|
||||
# Output compressed WebM file path
|
||||
compressed_output="$COMPRESSED_DIR/${base_name}.webm"
|
||||
|
||||
echo "Processing: $(basename "$webm_file")"
|
||||
|
||||
# Get original file size
|
||||
ORIGINAL_SIZE=$(stat -f%z "$webm_file" 2>/dev/null || stat -c%s "$webm_file" 2>/dev/null)
|
||||
TOTAL_ORIGINAL_SIZE=$((TOTAL_ORIGINAL_SIZE + ORIGINAL_SIZE))
|
||||
|
||||
# Check if output file already exists
|
||||
FILE_EXISTS=false
|
||||
if [ -f "$compressed_output" ]; then
|
||||
echo " → Overwriting existing file: ${base_name}.webm"
|
||||
FILE_EXISTS=true
|
||||
fi
|
||||
|
||||
# Detect codec type (VP8 or VP9) and other properties
|
||||
CODEC_INFO=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
PIXEL_FORMAT=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
HAS_ALPHA=$(echo "$PIXEL_FORMAT" | grep -iE "(yuva|rgba|gbra|argb|abgr|bgra|a444|a422|a420)")
|
||||
|
||||
# Check for alpha_mode metadata (more reliable for WebM)
|
||||
ALPHA_MODE=$(ffprobe -v quiet -select_streams v:0 -show_entries stream_tags=alpha_mode -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
echo " → Original codec: $CODEC_INFO (format: $PIXEL_FORMAT)"
|
||||
|
||||
# Set the appropriate decoder for alpha preservation
|
||||
case $CODEC_INFO in
|
||||
"vp8")
|
||||
DECODER_PARAMS="-c:v libvpx"
|
||||
echo " → Using libvpx decoder for VP8"
|
||||
;;
|
||||
"vp9")
|
||||
DECODER_PARAMS="-c:v libvpx-vp9"
|
||||
echo " → Using libvpx-vp9 decoder for VP9"
|
||||
;;
|
||||
*)
|
||||
DECODER_PARAMS=""
|
||||
echo " → Using default decoder for $CODEC_INFO"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$HAS_ALPHA" ] || [ "$ALPHA_MODE" = "1" ]; then
|
||||
echo " → Alpha channel detected, preserving transparency"
|
||||
# For alpha content, use special VP9 settings and let ffmpeg choose pixel format
|
||||
case $COMPRESSION in
|
||||
3)
|
||||
# Lossless doesn't work well with alpha in VP9, use very high quality instead
|
||||
ALPHA_VP9_PARAMS="-c:v libvpx-vp9 -crf 0 -b:v 0 -cpu-used 0"
|
||||
COMP_DESC_ALPHA="Near-lossless (CRF 0) for alpha preservation"
|
||||
;;
|
||||
4)
|
||||
ALPHA_VP9_PARAMS="-c:v libvpx-vp9 -crf 23 -b:v 0 -cpu-used 1"
|
||||
COMP_DESC_ALPHA="Lossy, high quality (CRF 23)"
|
||||
;;
|
||||
5)
|
||||
ALPHA_VP9_PARAMS="-c:v libvpx-vp9 -crf 35 -b:v 0 -cpu-used 2"
|
||||
COMP_DESC_ALPHA="Lossy, maximum compression (CRF 35)"
|
||||
;;
|
||||
esac
|
||||
# Don't force pixel format for alpha - let ffmpeg auto-detect
|
||||
ALPHA_PARAMS=""
|
||||
VIDEO_FILTER="fps=$FPS"
|
||||
FINAL_VP9_PARAMS="$ALPHA_VP9_PARAMS"
|
||||
echo " → Using alpha-compatible settings: $COMP_DESC_ALPHA"
|
||||
HAS_ALPHA_CONTENT=true
|
||||
else
|
||||
echo " → No alpha channel detected"
|
||||
ALPHA_PARAMS="-pix_fmt yuv420p"
|
||||
VIDEO_FILTER="fps=$FPS"
|
||||
FINAL_VP9_PARAMS="$VP9_PARAMS"
|
||||
HAS_ALPHA_CONTENT=false
|
||||
echo " → Using standard VP9 compression"
|
||||
fi
|
||||
|
||||
echo " → Compressing with VP9..."
|
||||
|
||||
# Compress the WebM file - use decoder params BEFORE input for alpha preservation
|
||||
if ffmpeg $DECODER_PARAMS -i "$webm_file" \
|
||||
-vf "$VIDEO_FILTER" \
|
||||
$FINAL_VP9_PARAMS \
|
||||
$ALPHA_PARAMS \
|
||||
$AUDIO_PARAMS \
|
||||
-threads 0 \
|
||||
-row-mt 1 \
|
||||
-y \
|
||||
"$compressed_output" \
|
||||
-v quiet -stats 2>/dev/null; then
|
||||
|
||||
# Get compressed file size
|
||||
COMPRESSED_SIZE=$(stat -f%z "$compressed_output" 2>/dev/null || stat -c%s "$compressed_output" 2>/dev/null)
|
||||
TOTAL_COMPRESSED_SIZE=$((TOTAL_COMPRESSED_SIZE + COMPRESSED_SIZE))
|
||||
|
||||
# Verify alpha preservation if original had alpha
|
||||
if [ "$HAS_ALPHA_CONTENT" = true ]; then
|
||||
OUTPUT_PIXEL_FORMAT=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 "$compressed_output" 2>/dev/null)
|
||||
OUTPUT_HAS_ALPHA=$(echo "$OUTPUT_PIXEL_FORMAT" | grep -iE "(yuva|rgba|gbra|argb|abgr|bgra|a444|a422|a420)")
|
||||
OUTPUT_ALPHA_MODE=$(ffprobe -v quiet -select_streams v:0 -show_entries stream_tags=alpha_mode -of csv=p=0 "$compressed_output" 2>/dev/null)
|
||||
|
||||
if [ -n "$OUTPUT_HAS_ALPHA" ] || [ "$OUTPUT_ALPHA_MODE" = "1" ]; then
|
||||
echo " ✓ Alpha channel preserved (format: $OUTPUT_PIXEL_FORMAT, alpha_mode: $OUTPUT_ALPHA_MODE)"
|
||||
else
|
||||
echo " ⚠ Warning: Alpha channel may have been lost (output format: $OUTPUT_PIXEL_FORMAT)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Calculate compression ratio
|
||||
RATIO=$(echo "scale=1; $COMPRESSED_SIZE * 100 / $ORIGINAL_SIZE" | bc)
|
||||
SAVINGS=$(echo "scale=1; 100 - $RATIO" | bc)
|
||||
|
||||
if [ "$FILE_EXISTS" = true ]; then
|
||||
echo " ✓ Successfully overwritten: ${base_name}.webm"
|
||||
((OVERWRITTEN++))
|
||||
else
|
||||
echo " ✓ Successfully created: ${base_name}.webm"
|
||||
((NEW_FILES++))
|
||||
fi
|
||||
echo " → Size: $(format_size $ORIGINAL_SIZE) → $(format_size $COMPRESSED_SIZE) (${RATIO}% of original, ${SAVINGS}% savings)"
|
||||
((PROCESSED++))
|
||||
else
|
||||
echo " ✗ Failed to compress: $(basename "$webm_file")"
|
||||
((FAILED++))
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Calculate overall compression statistics
|
||||
if [ $TOTAL_ORIGINAL_SIZE -gt 0 ]; then
|
||||
OVERALL_RATIO=$(echo "scale=1; $TOTAL_COMPRESSED_SIZE * 100 / $TOTAL_ORIGINAL_SIZE" | bc)
|
||||
OVERALL_SAVINGS=$(echo "scale=1; 100 - $OVERALL_RATIO" | bc)
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo "=== Compression Complete ==="
|
||||
echo "Total files processed: $PROCESSED"
|
||||
if [ $NEW_FILES -gt 0 ]; then
|
||||
echo "New files created: $NEW_FILES"
|
||||
fi
|
||||
if [ $OVERWRITTEN -gt 0 ]; then
|
||||
echo "Existing files overwritten: $OVERWRITTEN"
|
||||
fi
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo "Failed to process: $FAILED files"
|
||||
fi
|
||||
echo "Output directory: $COMPRESSED_DIR"
|
||||
echo "Settings used - Compression: Level $COMPRESSION ($COMP_DESC), Audio: $AUDIO_DESC, FPS: $FPS"
|
||||
echo "Note: Alpha-containing files may use different compression settings for transparency preservation"
|
||||
|
||||
if [ $PROCESSED -gt 0 ]; then
|
||||
echo ""
|
||||
echo "=== Compression Statistics ==="
|
||||
echo "Total original size: $(format_size $TOTAL_ORIGINAL_SIZE)"
|
||||
echo "Total compressed size: $(format_size $TOTAL_COMPRESSED_SIZE)"
|
||||
echo "Overall compression: ${OVERALL_RATIO}% of original (${OVERALL_SAVINGS}% space saved)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# List generated files
|
||||
if [ $PROCESSED -gt 0 ]; then
|
||||
echo "Compressed WebM files:"
|
||||
ls -la "$COMPRESSED_DIR"/*.webm 2>/dev/null | while read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
146
bash/webm_fps_analyse.sh
Executable file
146
bash/webm_fps_analyse.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WebM FPS Analyzer Script
|
||||
# Analyzes FPS of all WebM files in specified directory (or current directory)
|
||||
|
||||
# Usage function
|
||||
usage() {
|
||||
echo "Usage: $0 [directory_path]"
|
||||
echo " directory_path: Path to directory containing WebM files (optional, defaults to current directory)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Analyze current directory"
|
||||
echo " $0 /path/to/webm/files # Analyze specific directory"
|
||||
echo " $0 ~/Videos # Analyze home Videos directory"
|
||||
}
|
||||
|
||||
# Check if ffprobe is available
|
||||
if ! command -v ffprobe &> /dev/null; then
|
||||
echo "Error: ffprobe is not installed or not in PATH"
|
||||
echo "Please install ffmpeg to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Handle command line arguments
|
||||
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Set target directory
|
||||
if [[ -n "$1" ]]; then
|
||||
target_dir="$1"
|
||||
# Expand tilde if present
|
||||
target_dir="${target_dir/#\~/$HOME}"
|
||||
|
||||
# Check if directory exists
|
||||
if [[ ! -d "$target_dir" ]]; then
|
||||
echo "Error: Directory '$target_dir' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if directory is readable
|
||||
if [[ ! -r "$target_dir" ]]; then
|
||||
echo "Error: Directory '$target_dir' is not readable"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
target_dir="."
|
||||
fi
|
||||
|
||||
# Initialize variables
|
||||
declare -a fps_values=()
|
||||
declare -a filenames=()
|
||||
total_fps=0
|
||||
count=0
|
||||
min_fps=""
|
||||
max_fps=""
|
||||
|
||||
echo "Analyzing WebM files in directory: $target_dir"
|
||||
echo "========================================"
|
||||
|
||||
# Find all .webm files and process them
|
||||
for file in "$target_dir"/*.webm; do
|
||||
# Check if any .webm files exist
|
||||
if [[ ! -e "$file" ]]; then
|
||||
echo "No WebM files found in directory: $target_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get just the filename for display
|
||||
filename=$(basename "$file")
|
||||
|
||||
echo "Processing: $filename"
|
||||
|
||||
# Extract FPS using ffprobe
|
||||
fps=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$file" 2>/dev/null)
|
||||
|
||||
# Check if fps extraction was successful
|
||||
if [[ -z "$fps" ]]; then
|
||||
echo " ⚠️ Could not determine FPS for $filename"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Convert fraction to decimal if needed (e.g., "30/1" to "30")
|
||||
if [[ "$fps" == *"/"* ]]; then
|
||||
# Use bc for floating point division if available, otherwise use awk
|
||||
if command -v bc &> /dev/null; then
|
||||
fps_decimal=$(echo "scale=3; $fps" | bc)
|
||||
else
|
||||
fps_decimal=$(awk "BEGIN {printf \"%.3f\", $fps}")
|
||||
fi
|
||||
else
|
||||
fps_decimal="$fps"
|
||||
fi
|
||||
|
||||
echo " 📊 FPS: $fps_decimal"
|
||||
|
||||
# Store values
|
||||
fps_values+=("$fps_decimal")
|
||||
filenames+=("$filename")
|
||||
|
||||
# Update statistics
|
||||
if [[ -z "$min_fps" ]] || (( $(echo "$fps_decimal < $min_fps" | awk '{print ($1 < $2)}') )); then
|
||||
min_fps="$fps_decimal"
|
||||
fi
|
||||
|
||||
if [[ -z "$max_fps" ]] || (( $(echo "$fps_decimal > $max_fps" | awk '{print ($1 > $2)}') )); then
|
||||
max_fps="$fps_decimal"
|
||||
fi
|
||||
|
||||
total_fps=$(awk "BEGIN {printf \"%.3f\", $total_fps + $fps_decimal}")
|
||||
((count++))
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Check if any files were processed
|
||||
if [[ $count -eq 0 ]]; then
|
||||
echo "No valid WebM files could be processed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Calculate average
|
||||
average_fps=$(awk "BEGIN {printf \"%.3f\", $total_fps / $count}")
|
||||
|
||||
# Display results
|
||||
echo "========================================"
|
||||
echo "ANALYSIS RESULTS"
|
||||
echo "========================================"
|
||||
echo "Files processed: $count"
|
||||
echo ""
|
||||
echo "📈 STATISTICS:"
|
||||
echo " Minimum FPS: $min_fps"
|
||||
echo " Maximum FPS: $max_fps"
|
||||
echo " Average FPS: $average_fps"
|
||||
echo ""
|
||||
echo "📋 INDIVIDUAL FILE DETAILS:"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Display individual file FPS
|
||||
for i in "${!filenames[@]}"; do
|
||||
printf "%-40s | %s FPS\n" "${filenames[$i]}" "${fps_values[$i]}"
|
||||
done
|
||||
|
||||
echo "----------------------------------------"
|
||||
echo "Analysis complete! 🎬"
|
||||
287
bash/webm_to_gif.sh
Executable file
287
bash/webm_to_gif.sh
Executable file
@@ -0,0 +1,287 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to convert WebM files to GIF with alpha transparency
|
||||
# Usage: ./webm_to_gif.sh [directory_path] [fps] [resize_factor] [duration] [background] [compression]
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 [directory_path] [fps] [resize_factor] [duration] [background] [compression]"
|
||||
echo " directory_path: Path to directory containing WebM files (default: current directory)"
|
||||
echo " fps: Target frames per second for GIF (default: 7)"
|
||||
echo " resize_factor: Scale factor for resizing (default: 0.2 = 5x smaller)"
|
||||
echo " duration: Duration in seconds to convert from start (default: 3)"
|
||||
echo " background: Background color for transparency (default: transparent)"
|
||||
echo " - 'transparent' for alpha transparency"
|
||||
echo " - Hex colors: '#000000', '#ffffff', '#ff0000', etc."
|
||||
echo " - Named colors: 'black', 'white', 'red', 'blue', etc."
|
||||
echo " compression: Compression level 1-5 (default: 3)"
|
||||
echo " - 1: Highest quality, largest files (256 colors)"
|
||||
echo " - 2: High quality (128 colors)"
|
||||
echo " - 3: Balanced (64 colors)"
|
||||
echo " - 4: Good compression (32 colors)"
|
||||
echo " - 5: Maximum compression (16 colors)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # All defaults"
|
||||
echo " $0 /path/to/files # Custom dir"
|
||||
echo " $0 /path/to/files 15 # Custom dir and fps"
|
||||
echo " $0 /path/to/files 15 0.5 # Custom dir, fps, and scale"
|
||||
echo " $0 /path/to/files 15 0.5 5 # All params except background/compression"
|
||||
echo " $0 /path/to/files 15 0.5 5 black # Black background"
|
||||
echo " $0 /path/to/files 15 0.5 5 black 5 # Black background, max compression"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " - Find all .webm files in the specified directory"
|
||||
echo " - Convert to GIF format maintaining alpha transparency"
|
||||
echo " - Use VP9 decoder for optimal alpha preservation"
|
||||
echo " - Save results in a 'gif' subdirectory"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if ffmpeg is installed
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "Error: ffmpeg is not installed or not in PATH"
|
||||
echo "Please install ffmpeg to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set default values
|
||||
DIRECTORY="."
|
||||
FPS=7
|
||||
RESIZE_FACTOR=0.2
|
||||
DURATION=3
|
||||
BACKGROUND="transparent"
|
||||
COMPRESSION=3
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
DIRECTORY="."
|
||||
elif [ $# -eq 1 ]; then
|
||||
DIRECTORY="$1"
|
||||
elif [ $# -eq 2 ]; then
|
||||
DIRECTORY="$1"
|
||||
FPS="$2"
|
||||
elif [ $# -eq 3 ]; then
|
||||
DIRECTORY="$1"
|
||||
FPS="$2"
|
||||
RESIZE_FACTOR="$3"
|
||||
elif [ $# -eq 4 ]; then
|
||||
DIRECTORY="$1"
|
||||
FPS="$2"
|
||||
RESIZE_FACTOR="$3"
|
||||
DURATION="$4"
|
||||
elif [ $# -eq 5 ]; then
|
||||
DIRECTORY="$1"
|
||||
FPS="$2"
|
||||
RESIZE_FACTOR="$3"
|
||||
DURATION="$4"
|
||||
BACKGROUND="$5"
|
||||
elif [ $# -eq 6 ]; then
|
||||
DIRECTORY="$1"
|
||||
FPS="$2"
|
||||
RESIZE_FACTOR="$3"
|
||||
DURATION="$4"
|
||||
BACKGROUND="$5"
|
||||
COMPRESSION="$6"
|
||||
else
|
||||
echo "Error: Too many arguments"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Validate fps parameter
|
||||
if ! [[ "$FPS" =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$FPS <= 0" | bc -l) )); then
|
||||
echo "Error: fps must be a positive number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate resize factor parameter
|
||||
if ! [[ "$RESIZE_FACTOR" =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$RESIZE_FACTOR <= 0" | bc -l) )); then
|
||||
echo "Error: resize_factor must be a positive number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate duration parameter
|
||||
if ! [[ "$DURATION" =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$DURATION <= 0" | bc -l) )); then
|
||||
echo "Error: duration must be a positive number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate background parameter
|
||||
if [ "$BACKGROUND" != "transparent" ]; then
|
||||
# Check if it's a valid hex color or named color
|
||||
if ! [[ "$BACKGROUND" =~ ^#[0-9a-fA-F]{6}$ ]] && ! [[ "$BACKGROUND" =~ ^[a-zA-Z]+$ ]]; then
|
||||
echo "Error: background must be 'transparent', a hex color (#000000), or a named color (black, white, etc.)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate compression parameter
|
||||
if ! [[ "$COMPRESSION" =~ ^[1-5]$ ]]; then
|
||||
echo "Error: compression must be between 1-5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set compression values based on level
|
||||
case $COMPRESSION in
|
||||
1) MAX_COLORS=256; DITHER_SCALE=2 ;; # Highest quality
|
||||
2) MAX_COLORS=128; DITHER_SCALE=3 ;; # High quality
|
||||
3) MAX_COLORS=64; DITHER_SCALE=4 ;; # Balanced (default)
|
||||
4) MAX_COLORS=32; DITHER_SCALE=5 ;; # Good compression
|
||||
5) MAX_COLORS=16; DITHER_SCALE=5 ;; # Maximum compression
|
||||
esac
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$DIRECTORY" ]; then
|
||||
echo "Error: Directory '$DIRECTORY' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert to absolute path
|
||||
DIRECTORY=$(realpath "$DIRECTORY")
|
||||
echo "Processing WebM files in: $DIRECTORY"
|
||||
echo "Target FPS: $FPS"
|
||||
echo "Resize factor: ${RESIZE_FACTOR}x ($(echo "scale=1; 1/$RESIZE_FACTOR" | bc)x smaller)"
|
||||
echo "Duration: first $DURATION seconds"
|
||||
echo "Background: $BACKGROUND"
|
||||
echo "Compression: Level $COMPRESSION ($MAX_COLORS colors, dither scale $DITHER_SCALE)"
|
||||
|
||||
# Create gif output directory
|
||||
GIF_DIR="$DIRECTORY/gif"
|
||||
if [ ! -d "$GIF_DIR" ]; then
|
||||
mkdir -p "$GIF_DIR"
|
||||
echo "Created output directory: $GIF_DIR"
|
||||
else
|
||||
echo "Using existing output directory: $GIF_DIR"
|
||||
fi
|
||||
|
||||
# Find all WebM files in the directory
|
||||
WEBM_FILES=($(find "$DIRECTORY" -maxdepth 1 -name "*.webm" -type f))
|
||||
|
||||
# Check if any WebM files were found
|
||||
if [ ${#WEBM_FILES[@]} -eq 0 ]; then
|
||||
echo "No WebM files found in '$DIRECTORY'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#WEBM_FILES[@]} WebM file(s)"
|
||||
echo "Note: Existing GIF files will be overwritten to ensure proper transparency"
|
||||
echo ""
|
||||
|
||||
# Counter for processed files
|
||||
PROCESSED=0
|
||||
FAILED=0
|
||||
OVERWRITTEN=0
|
||||
NEW_FILES=0
|
||||
|
||||
# Process each WebM file
|
||||
for webm_file in "${WEBM_FILES[@]}"; do
|
||||
# Get the base filename without path and extension
|
||||
base_name=$(basename "$webm_file" .webm)
|
||||
|
||||
# Output GIF file path
|
||||
gif_output="$GIF_DIR/${base_name}.gif"
|
||||
|
||||
echo "Processing: $(basename "$webm_file")"
|
||||
|
||||
# Check if output file already exists
|
||||
FILE_EXISTS=false
|
||||
if [ -f "$gif_output" ]; then
|
||||
echo " → Overwriting existing file: ${base_name}.gif"
|
||||
FILE_EXISTS=true
|
||||
fi
|
||||
|
||||
# Detect codec type (VP8 or VP9) to use correct decoder
|
||||
CODEC_INFO=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
# Set appropriate decoder based on codec (VP9 preferred for alpha)
|
||||
if [ "$CODEC_INFO" = "vp9" ]; then
|
||||
DECODER="libvpx-vp9"
|
||||
echo " → Using VP9 decoder (optimal for alpha preservation)"
|
||||
else
|
||||
DECODER="libvpx" # Default to VP8 decoder
|
||||
echo " → Using VP8 decoder"
|
||||
fi
|
||||
|
||||
# Create temporary files for palette generation
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
PALETTE_FILE="$TEMP_DIR/palette.png"
|
||||
|
||||
echo " → Generating optimized palette with transparency support..."
|
||||
|
||||
# Set up filter chains based on background setting
|
||||
if [ "$BACKGROUND" = "transparent" ]; then
|
||||
PALETTE_FILTER="fps=$FPS,scale=iw*$RESIZE_FACTOR:ih*$RESIZE_FACTOR:flags=lanczos,palettegen=transparency_color=ffffff:reserve_transparent=on:max_colors=$MAX_COLORS"
|
||||
CONVERT_FILTER="fps=$FPS,scale=iw*$RESIZE_FACTOR:ih*$RESIZE_FACTOR:flags=lanczos[x];[x][1:v]paletteuse=alpha_threshold=128:dither=bayer:bayer_scale=$DITHER_SCALE"
|
||||
echo " → Using transparent background (alpha channel preserved)"
|
||||
else
|
||||
PALETTE_FILTER="fps=$FPS,scale=iw*$RESIZE_FACTOR:ih*$RESIZE_FACTOR:flags=lanczos,split[a][b];[a]geq=r=0:g=0:b=0:a=255,drawbox=color=$BACKGROUND@1.0:replace=1:t=fill[bg];[bg][b]overlay=format=auto,palettegen=max_colors=$MAX_COLORS"
|
||||
CONVERT_FILTER="fps=$FPS,scale=iw*$RESIZE_FACTOR:ih*$RESIZE_FACTOR:flags=lanczos,split[a][b];[a]geq=r=0:g=0:b=0:a=255,drawbox=color=$BACKGROUND@1.0:replace=1:t=fill[bg];[bg][b]overlay=format=auto[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=$DITHER_SCALE"
|
||||
echo " → Using background color: $BACKGROUND (fills transparent areas only)"
|
||||
fi
|
||||
|
||||
# Generate palette with transparency support
|
||||
if ffmpeg -c:v "$DECODER" \
|
||||
-i "$webm_file" \
|
||||
-t "$DURATION" \
|
||||
-vf "$PALETTE_FILTER" \
|
||||
-y \
|
||||
"$PALETTE_FILE" \
|
||||
-v quiet 2>/dev/null; then
|
||||
|
||||
echo " → Converting to GIF..."
|
||||
|
||||
# Convert to GIF using the generated palette
|
||||
if ffmpeg -c:v "$DECODER" \
|
||||
-i "$webm_file" \
|
||||
-i "$PALETTE_FILE" \
|
||||
-t "$DURATION" \
|
||||
-lavfi "$CONVERT_FILTER" \
|
||||
-y \
|
||||
"$gif_output" \
|
||||
-v quiet 2>/dev/null; then
|
||||
|
||||
if [ "$FILE_EXISTS" = true ]; then
|
||||
echo " ✓ Successfully overwritten: ${base_name}.gif"
|
||||
((OVERWRITTEN++))
|
||||
else
|
||||
echo " ✓ Successfully created: ${base_name}.gif"
|
||||
((NEW_FILES++))
|
||||
fi
|
||||
((PROCESSED++))
|
||||
else
|
||||
echo " ✗ Failed to convert to GIF: $(basename "$webm_file")"
|
||||
((FAILED++))
|
||||
fi
|
||||
else
|
||||
echo " ✗ Failed to generate palette for: $(basename "$webm_file")"
|
||||
((FAILED++))
|
||||
fi
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf "$TEMP_DIR"
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo "=== Processing Complete ==="
|
||||
echo "Total files processed: $PROCESSED"
|
||||
if [ $NEW_FILES -gt 0 ]; then
|
||||
echo "New files created: $NEW_FILES"
|
||||
fi
|
||||
if [ $OVERWRITTEN -gt 0 ]; then
|
||||
echo "Existing files overwritten: $OVERWRITTEN"
|
||||
fi
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo "Failed to process: $FAILED files"
|
||||
fi
|
||||
echo "Output directory: $GIF_DIR"
|
||||
echo "Settings used - FPS: $FPS, Scale: ${RESIZE_FACTOR}x, Duration: ${DURATION}s, Background: $BACKGROUND, Compression: $COMPRESSION"
|
||||
echo ""
|
||||
|
||||
# List generated files
|
||||
if [ $PROCESSED -gt 0 ]; then
|
||||
echo "Generated GIF files:"
|
||||
ls -la "$GIF_DIR"/*.gif 2>/dev/null | while read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
277
bash/webm_to_webp.sh
Executable file
277
bash/webm_to_webp.sh
Executable file
@@ -0,0 +1,277 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to extract first frame from WebM files and convert to WebP with alpha transparency
|
||||
# Usage: ./webm_to_webp.sh [directory_path] [background] [compression]
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 [directory_path] [background] [compression]"
|
||||
echo " directory_path: Path to directory containing WebM files (default: current directory)"
|
||||
echo " background: Background color for transparency (default: transparent)"
|
||||
echo " - 'transparent' for alpha transparency"
|
||||
echo " - Hex colors: '#000000', '#ffffff', '#ff0000', etc."
|
||||
echo " - Named colors: 'black', 'white', 'red', 'blue', etc."
|
||||
echo " compression: Compression level 1-5 (default: 3)"
|
||||
echo " - 1: Lossless, fast (largest files)"
|
||||
echo " - 2: Lossless, balanced"
|
||||
echo " - 3: Lossless, best compression (default)"
|
||||
echo " - 4: Lossy, high quality (quality 90)"
|
||||
echo " - 5: Lossy, maximum compression (quality 75)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Current directory, transparent, lossless best"
|
||||
echo " $0 /path/to/files # Custom directory, defaults"
|
||||
echo " $0 /path/to/files black # Black background, lossless best"
|
||||
echo " $0 /path/to/files '#ff0000' 5 # Red background, maximum compression"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " - Find all .webm files in the specified directory"
|
||||
echo " - Extract the frame with maximum coverage from each WebM file"
|
||||
echo " - Convert to WebP format maintaining alpha transparency"
|
||||
echo " - Save results in a 'webp' subdirectory"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if ffmpeg is installed
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "Error: ffmpeg is not installed or not in PATH"
|
||||
echo "Please install ffmpeg to use this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set default values
|
||||
DIRECTORY="."
|
||||
BACKGROUND="transparent"
|
||||
COMPRESSION=3
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
DIRECTORY="."
|
||||
elif [ $# -eq 1 ]; then
|
||||
DIRECTORY="$1"
|
||||
elif [ $# -eq 2 ]; then
|
||||
DIRECTORY="$1"
|
||||
BACKGROUND="$2"
|
||||
elif [ $# -eq 3 ]; then
|
||||
DIRECTORY="$1"
|
||||
BACKGROUND="$2"
|
||||
COMPRESSION="$3"
|
||||
else
|
||||
echo "Error: Too many arguments"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Validate background parameter
|
||||
if [ "$BACKGROUND" != "transparent" ]; then
|
||||
# Check if it's a valid hex color or named color
|
||||
if ! [[ "$BACKGROUND" =~ ^#[0-9a-fA-F]{6}$ ]] && ! [[ "$BACKGROUND" =~ ^[a-zA-Z]+$ ]]; then
|
||||
echo "Error: background must be 'transparent', a hex color (#000000), or a named color (black, white, etc.)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate compression parameter
|
||||
if ! [[ "$COMPRESSION" =~ ^[1-5]$ ]]; then
|
||||
echo "Error: compression must be between 1-5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set compression values based on level
|
||||
case $COMPRESSION in
|
||||
1) WEBP_LOSSLESS=1; WEBP_COMP_LEVEL=0; WEBP_QUALITY=100; WEBP_METHOD=0; COMP_DESC="Lossless, fast" ;;
|
||||
2) WEBP_LOSSLESS=1; WEBP_COMP_LEVEL=3; WEBP_QUALITY=100; WEBP_METHOD=0; COMP_DESC="Lossless, balanced" ;;
|
||||
3) WEBP_LOSSLESS=1; WEBP_COMP_LEVEL=6; WEBP_QUALITY=100; WEBP_METHOD=0; COMP_DESC="Lossless, best compression" ;;
|
||||
4) WEBP_LOSSLESS=0; WEBP_COMP_LEVEL=4; WEBP_QUALITY=90; WEBP_METHOD=4; COMP_DESC="Lossy, high quality (q90)" ;;
|
||||
5) WEBP_LOSSLESS=0; WEBP_COMP_LEVEL=4; WEBP_QUALITY=75; WEBP_METHOD=6; COMP_DESC="Lossy, maximum compression (q75)" ;;
|
||||
esac
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$DIRECTORY" ]; then
|
||||
echo "Error: Directory '$DIRECTORY' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert to absolute path
|
||||
DIRECTORY=$(realpath "$DIRECTORY")
|
||||
echo "Processing WebM files in: $DIRECTORY"
|
||||
echo "Background: $BACKGROUND"
|
||||
echo "Compression: Level $COMPRESSION ($COMP_DESC)"
|
||||
|
||||
# Create webp output directory
|
||||
WEBP_DIR="$DIRECTORY/webp"
|
||||
if [ ! -d "$WEBP_DIR" ]; then
|
||||
mkdir -p "$WEBP_DIR"
|
||||
echo "Created output directory: $WEBP_DIR"
|
||||
else
|
||||
echo "Using existing output directory: $WEBP_DIR"
|
||||
fi
|
||||
|
||||
# Find all WebM files in the directory
|
||||
WEBM_FILES=($(find "$DIRECTORY" -maxdepth 1 -name "*.webm" -type f))
|
||||
|
||||
# Check if any WebM files were found
|
||||
if [ ${#WEBM_FILES[@]} -eq 0 ]; then
|
||||
echo "No WebM files found in '$DIRECTORY'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#WEBM_FILES[@]} WebM file(s)"
|
||||
echo "Note: Existing WebP files will be overwritten to ensure proper transparency"
|
||||
echo ""
|
||||
|
||||
# Counter for processed files
|
||||
PROCESSED=0
|
||||
FAILED=0
|
||||
OVERWRITTEN=0
|
||||
NEW_FILES=0
|
||||
|
||||
# Process each WebM file
|
||||
for webm_file in "${WEBM_FILES[@]}"; do
|
||||
# Get the base filename without path and extension
|
||||
base_name=$(basename "$webm_file" .webm)
|
||||
|
||||
# Output WebP file path
|
||||
webp_output="$WEBP_DIR/${base_name}.webp"
|
||||
|
||||
echo "Processing: $(basename "$webm_file")"
|
||||
|
||||
# Check if output file already exists
|
||||
FILE_EXISTS=false
|
||||
if [ -f "$webp_output" ]; then
|
||||
echo " → Overwriting existing file: ${base_name}.webp"
|
||||
FILE_EXISTS=true
|
||||
fi
|
||||
|
||||
# Detect codec type (VP8 or VP9) to use correct decoder
|
||||
CODEC_INFO=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
# Set appropriate decoder based on codec
|
||||
if [ "$CODEC_INFO" = "vp9" ]; then
|
||||
DECODER="libvpx-vp9"
|
||||
echo " → Using VP9 decoder (optimal for alpha preservation)"
|
||||
else
|
||||
DECODER="libvpx" # Default to VP8 decoder
|
||||
echo " → Using VP8 decoder"
|
||||
fi
|
||||
|
||||
echo " → Finding frame with maximum coverage..."
|
||||
|
||||
# Get total frame count and duration
|
||||
FRAME_COUNT=$(ffprobe -v quiet -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 "$webm_file" 2>/dev/null)
|
||||
|
||||
if [ -z "$FRAME_COUNT" ] || [ "$FRAME_COUNT" -eq 0 ]; then
|
||||
echo " ✗ Could not determine frame count"
|
||||
((FAILED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Sample 10 frames evenly distributed throughout the video
|
||||
SAMPLE_COUNT=10
|
||||
if [ "$FRAME_COUNT" -lt "$SAMPLE_COUNT" ]; then
|
||||
SAMPLE_COUNT=$FRAME_COUNT
|
||||
fi
|
||||
|
||||
MAX_SIZE=0
|
||||
BEST_FRAME=0
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
|
||||
echo " → Analyzing $SAMPLE_COUNT sample frames from $FRAME_COUNT total frames..."
|
||||
|
||||
# Sample frames evenly throughout the video
|
||||
for ((i=0; i<SAMPLE_COUNT; i++)); do
|
||||
# Calculate frame position (evenly distributed)
|
||||
FRAME_POS=$((i * FRAME_COUNT / SAMPLE_COUNT))
|
||||
|
||||
# Extract single frame as WebP to temp location
|
||||
TEMP_FRAME="$TEMP_DIR/frame_${FRAME_POS}.webp"
|
||||
|
||||
ffmpeg -c:v "$DECODER" \
|
||||
-i "$webm_file" \
|
||||
-vf "select=eq(n\\,$FRAME_POS)" \
|
||||
-vframes 1 \
|
||||
-c:v libwebp \
|
||||
-lossless 1 \
|
||||
-y \
|
||||
"$TEMP_FRAME" \
|
||||
-v quiet 2>/dev/null
|
||||
|
||||
if [ -f "$TEMP_FRAME" ]; then
|
||||
# Use file size as coverage indicator (larger = more content)
|
||||
FRAME_SIZE=$(stat -f%z "$TEMP_FRAME" 2>/dev/null || stat -c%s "$TEMP_FRAME" 2>/dev/null)
|
||||
|
||||
if [ -n "$FRAME_SIZE" ] && [ "$FRAME_SIZE" -gt "$MAX_SIZE" ]; then
|
||||
MAX_SIZE=$FRAME_SIZE
|
||||
BEST_FRAME=$FRAME_POS
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo " → Best frame: #$BEST_FRAME (size: $MAX_SIZE bytes)"
|
||||
|
||||
# Set up filter chain based on background setting
|
||||
if [ "$BACKGROUND" = "transparent" ]; then
|
||||
FILTER_CHAIN="select=eq(n\\,$BEST_FRAME)"
|
||||
echo " → Extracting frame with alpha transparency preserved"
|
||||
else
|
||||
FILTER_CHAIN="select=eq(n\\,$BEST_FRAME),split[a][b];[a]geq=r=0:g=0:b=0:a=255,drawbox=color=$BACKGROUND@1.0:replace=1:t=fill[bg];[bg][b]overlay=format=auto"
|
||||
echo " → Extracting frame with background color: $BACKGROUND"
|
||||
fi
|
||||
|
||||
# Build WebP encoding parameters based on compression level
|
||||
if [ "$WEBP_LOSSLESS" -eq 1 ]; then
|
||||
WEBP_PARAMS="-c:v libwebp -lossless 1 -compression_level $WEBP_COMP_LEVEL"
|
||||
else
|
||||
WEBP_PARAMS="-c:v libwebp -lossless 0 -quality $WEBP_QUALITY -method $WEBP_METHOD -compression_level $WEBP_COMP_LEVEL"
|
||||
fi
|
||||
|
||||
# Extract the frame with maximum coverage
|
||||
if ffmpeg -c:v "$DECODER" \
|
||||
-i "$webm_file" \
|
||||
-vf "$FILTER_CHAIN" \
|
||||
-vframes 1 \
|
||||
$WEBP_PARAMS \
|
||||
-an \
|
||||
-y \
|
||||
"$webp_output" \
|
||||
-v quiet -stats; then
|
||||
if [ "$FILE_EXISTS" = true ]; then
|
||||
echo " ✓ Successfully overwritten: ${base_name}.webp"
|
||||
((OVERWRITTEN++))
|
||||
else
|
||||
echo " ✓ Successfully created: ${base_name}.webp"
|
||||
((NEW_FILES++))
|
||||
fi
|
||||
((PROCESSED++))
|
||||
else
|
||||
echo " ✗ Failed to process: $(basename "$webm_file")"
|
||||
((FAILED++))
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo "=== Processing Complete ==="
|
||||
echo "Total files processed: $PROCESSED"
|
||||
if [ $NEW_FILES -gt 0 ]; then
|
||||
echo "New files created: $NEW_FILES"
|
||||
fi
|
||||
if [ $OVERWRITTEN -gt 0 ]; then
|
||||
echo "Existing files overwritten: $OVERWRITTEN"
|
||||
fi
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo "Failed to process: $FAILED files"
|
||||
fi
|
||||
echo "Output directory: $WEBP_DIR"
|
||||
echo "Settings used - Background: $BACKGROUND, Compression: Level $COMPRESSION ($COMP_DESC)"
|
||||
echo ""
|
||||
|
||||
# List generated files
|
||||
if [ $PROCESSED -gt 0 ]; then
|
||||
echo "Generated WebP files:"
|
||||
ls -la "$WEBP_DIR"/*.webp 2>/dev/null | while read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
Reference in New Issue
Block a user