Midjourney Video to Seamless Loop using FFmpeg - Complete Automation Script
Posted on - June 18, 2025 by Andy Cinquin
MidjourneyFFmpegSeamless LoopAI VideoWebP AnimationVideo ProcessingAutomationBash Script
An automatic Bash script to convert videos to seamlessly looping animated WebP files on Fedora, integrated into Nautilus context menu.
✨ Features
- Automatic conversion: Converts any video to animated WebP
- Seamless looping: Creates fade effect between end and beginning for perfect loops
- Context menu: Accessible directly via right-click on video files
- Notifications: Shows progress and results via system notifications
- Auto cleanup: Automatically removes temporary files
- Quality optimization: Balanced settings between quality and file size
🎯 Supported Formats
Input: MP4, AVI, MKV, MOV, WMV, FLV, WebM, 3GP, M4V Output: Animated WebP (30fps, infinite loop)
📋 Requirements
- Fedora Linux (tested on Fedora 38+)
- GNOME Desktop Environment with Nautilus
- Sudo access for installation
🚀 Quick Installation
-
Create the files below in a folder:
mkdir video-to-webp-converter cd video-to-webp-converter
-
Copy the content of each script (see sections below)
-
Make the installation script executable:
chmod +x install.sh
-
Run the installation:
./install.sh
🎬 Usage
Via context menu (recommended)
- Open Nautilus (Files)
- Navigate to a folder containing videos
- Right-click on any video file
- Select "Convert to Animated WebP"
- Wait for conversion to complete (notifications shown)
- WebP file will be created with
_loop.webp
suffix
Via command line
video-to-webp.sh "path/to/your/video.mp4"
📁 COMPLETE SCRIPTS TO CREATE
1. 📝 File: video-to-webp.sh
#!/bin/bash
# Script to convert video to looping animated WebP
# Author: Assistant
# Usage: video-to-webp.sh "input_video.mp4"
set -euo pipefail
# Configuration
FPS_INTERMEDIATE=24
FPS_OUTPUT=30
CRF=18
PRESET="veryfast"
FADE_DURATION=0.5
FADE_OFFSET=3.5
# Function to show notifications
notify_user() {
local message="$1"
local urgency="${2:-normal}"
if command -v notify-send &> /dev/null; then
notify-send -u "$urgency" "Video to WebP" "$message"
fi
echo "$message"
}
# Function to cleanup temporary files
cleanup() {
local temp_file="$1"
if [[ -f "$temp_file" ]]; then
rm -f "$temp_file"
fi
}
# Function to get video duration
get_video_duration() {
local input_file="$1"
ffprobe -v quiet -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file" 2>/dev/null || echo "0"
}
# Function to convert video to WebP
convert_video_to_webp() {
local input_file="$1"
local base_name="${input_file%.*}"
local output_file="${base_name}_loop.webp"
local temp_cfr="${base_name}_tmp_cfr.mp4"
local temp_loop="${base_name}_tmp_loop.mp4"
# Validate input file
if [[ ! -f "$input_file" ]]; then
notify_user "Error: Input file '$input_file' not found" "critical"
return 1
fi
# Check if ffmpeg is available
if ! command -v ffmpeg &> /dev/null; then
notify_user "Error: ffmpeg is not installed. Install it with: sudo dnf install ffmpeg" "critical"
return 1
fi
# Get video duration
local duration
duration=$(get_video_duration "$input_file")
if (( $(echo "$duration < 1" | bc -l) )); then
notify_user "Error: Unable to determine video duration or video too short" "critical"
return 1
fi
notify_user "Starting conversion of '$input_file' to WebP..."
# Step 1: Convert to constant frame rate
notify_user "Step 1/3: Converting to constant frame rate..."
if ! ffmpeg -y -i "$input_file" \
-r "$FPS_INTERMEDIATE" \
-c:v libx264 \
-crf "$CRF" \
-preset "$PRESET" \
-an \
"$temp_cfr" 2>/dev/null; then
notify_user "Error: Failed to convert to CFR" "critical"
cleanup "$temp_cfr"
return 1
fi
# Step 2: Create seamless loop with crossfade
notify_user "Step 2/3: Creating seamless loop..."
if ! ffmpeg -y -i "$temp_cfr" \
-filter_complex \
"[0:v]split=2[main1][main2]; \
[main1]trim=end=1,setpts=PTS-STARTPTS,fps=$FPS_INTERMEDIATE[begin]; \
[main2]trim=start=1,setpts=PTS-STARTPTS,fps=$FPS_INTERMEDIATE[end]; \
[end][begin]xfade=transition=fade:duration=$FADE_DURATION:offset=$FADE_OFFSET,format=yuv420p[v]" \
-map "[v]" \
-c:v libx264 \
-crf "$CRF" \
-preset "$PRESET" \
"$temp_loop" 2>/dev/null; then
notify_user "Error: Failed to create loop" "critical"
cleanup "$temp_cfr"
cleanup "$temp_loop"
return 1
fi
# Step 3: Convert to WebP
notify_user "Step 3/3: Converting to animated WebP..."
if ! ffmpeg -y -i "$temp_loop" \
-vf "fps=$FPS_OUTPUT" \
-loop 0 \
-quality 80 \
-method 6 \
-lossless 0 \
"$output_file" 2>/dev/null; then
notify_user "Error: Failed to convert to WebP" "critical"
cleanup "$temp_cfr"
cleanup "$temp_loop"
return 1
fi
# Cleanup temporary files
cleanup "$temp_cfr"
cleanup "$temp_loop"
# Get file sizes for comparison
local input_size output_size
input_size=$(du -h "$input_file" | cut -f1)
output_size=$(du -h "$output_file" | cut -f1)
notify_user "✅ Conversion completed successfully!
Input: $input_size → Output: $output_size
Saved as: $(basename "$output_file")"
# Open file manager to show the result
if command -v nautilus &> /dev/null; then
nautilus "$(dirname "$output_file")" &
fi
}
# Main execution
main() {
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <input_video_file>"
echo "Example: $0 'my_video.mp4'"
exit 1
fi
local input_file="$1"
# Convert to absolute path
input_file=$(realpath "$input_file")
# Start conversion
convert_video_to_webp "$input_file"
}
# Trap cleanup on exit
trap 'cleanup "${base_name}_tmp_cfr.mp4" 2>/dev/null || true; cleanup "${base_name}_tmp_loop.mp4" 2>/dev/null || true' EXIT
# Run main function
main "$@"
2. 🗂️ File: video-to-webp.desktop
[Desktop Entry]
Type=Action
Icon=video-x-generic
Name[en]=Convert to Animated WebP
Name[fr]=Convertir en WebP animé
Tooltip[en]=Convert video to looping animated WebP
Tooltip[fr]=Convertir la vidéo en WebP animé en boucle
MimeType=video/mp4;video/avi;video/mkv;video/mov;video/wmv;video/flv;video/webm;video/3gp;video/m4v;video/x-msvideo;video/quicktime;
Profiles=profile-zero;
[X-Action-Profile profile-zero]
Exec=/usr/local/bin/video-to-webp.sh %f
Name[en]=Convert to Animated WebP Loop
Name[fr]=Convertir en WebP animé en boucle
Icon=video-x-generic
Description[en]=Convert selected video to an animated WebP with seamless loop
Description[fr]=Convertir la vidéo sélectionnée en WebP animé avec boucle fluide
MimeTypes=video/mp4;video/avi;video/mkv;video/mov;video/wmv;video/flv;video/webm;video/3gp;video/m4v;video/x-msvideo;video/quicktime;
SelectionCount==1
3. 🚀 File: install.sh
#!/bin/bash
# Installation script for Video to WebP converter
# For Fedora Linux with Nautilus file manager
set -euo pipefail
SCRIPT_NAME="video-to-webp.sh"
DESKTOP_FILE="video-to-webp.desktop"
INSTALL_DIR="/usr/local/bin"
NAUTILUS_ACTIONS_DIR="$HOME/.local/share/file-manager/actions"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to install dependencies
install_dependencies() {
print_status "$BLUE" "🔍 Checking dependencies..."
local missing_packages=()
# Check for ffmpeg
if ! command_exists ffmpeg; then
missing_packages+=("ffmpeg")
fi
# Check for bc (for float calculations)
if ! command_exists bc; then
missing_packages+=("bc")
fi
# Check for notify-send
if ! command_exists notify-send; then
missing_packages+=("libnotify")
fi
if [[ ${#missing_packages[@]} -gt 0 ]]; then
print_status "$YELLOW" "📦 Installing missing packages: ${missing_packages[*]}"
# Enable RPM Fusion repositories if ffmpeg is missing
if [[ " ${missing_packages[*]} " =~ " ffmpeg " ]]; then
print_status "$BLUE" "🔧 Enabling RPM Fusion repositories for ffmpeg..."
sudo dnf install -y \
https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm 2>/dev/null || true
fi
sudo dnf install -y "${missing_packages[@]}"
print_status "$GREEN" "✅ Dependencies installed successfully"
else
print_status "$GREEN" "✅ All dependencies are already installed"
fi
}
# Function to install the main script
install_script() {
print_status "$BLUE" "📝 Installing video conversion script..."
if [[ ! -f "$SCRIPT_NAME" ]]; then
print_status "$RED" "❌ Error: $SCRIPT_NAME not found in current directory"
exit 1
fi
# Copy script to system directory
sudo cp "$SCRIPT_NAME" "$INSTALL_DIR/"
sudo chmod +x "$INSTALL_DIR/$SCRIPT_NAME"
print_status "$GREEN" "✅ Script installed to $INSTALL_DIR/$SCRIPT_NAME"
}
# Function to install Nautilus action
install_nautilus_action() {
print_status "$BLUE" "🗂️ Installing Nautilus context menu action..."
if [[ ! -f "$DESKTOP_FILE" ]]; then
print_status "$RED" "❌ Error: $DESKTOP_FILE not found in current directory"
exit 1
fi
# Create actions directory if it doesn't exist
mkdir -p "$NAUTILUS_ACTIONS_DIR"
# Copy desktop file
cp "$DESKTOP_FILE" "$NAUTILUS_ACTIONS_DIR/"
print_status "$GREEN" "✅ Nautilus action installed to $NAUTILUS_ACTIONS_DIR/$DESKTOP_FILE"
}
# Function to restart Nautilus
restart_nautilus() {
print_status "$BLUE" "🔄 Restarting Nautilus to apply changes..."
# Kill all nautilus processes
pkill -f nautilus 2>/dev/null || true
# Wait a moment
sleep 2
# Start nautilus in background
nautilus &>/dev/null &
print_status "$GREEN" "✅ Nautilus restarted"
}
# Function to create test video
create_test_video() {
local test_file="test_animation.mp4"
if [[ ! -f "$test_file" ]]; then
print_status "$BLUE" "🎬 Creating test video for demonstration..."
# Create a simple test animation using ffmpeg
ffmpeg -f lavfi -i "testsrc=duration=3:size=320x240:rate=30" \
-f lavfi -i "sine=frequency=1000:duration=3" \
-c:v libx264 -crf 23 -c:a aac \
"$test_file" 2>/dev/null
if [[ -f "$test_file" ]]; then
print_status "$GREEN" "✅ Test video created: $test_file"
fi
fi
}
# Function to display usage instructions
show_usage_instructions() {
print_status "$GREEN" "🎉 Installation completed successfully!"
echo
print_status "$BLUE" "📋 How to use:"
echo "1. Open Nautilus (Files) file manager"
echo "2. Navigate to a folder containing video files"
echo "3. Right-click on any video file (.mp4, .avi, .mkv, etc.)"
echo "4. Select 'Convert to Animated WebP' from the context menu"
echo "5. Wait for the conversion to complete"
echo "6. The WebP file will be created in the same directory"
echo
print_status "$YELLOW" "💡 Tips:"
echo "• The script creates seamless looping animations"
echo "• Output files are named with '_loop.webp' suffix"
echo "• You'll get notifications during the conversion process"
echo "• The file manager will open automatically when done"
echo
print_status "$BLUE" "🔧 Manual usage:"
echo "You can also run the script manually from terminal:"
echo "video-to-webp.sh 'path/to/your/video.mp4'"
}
# Main installation process
main() {
print_status "$GREEN" "🚀 Starting Video to WebP Converter installation on Fedora"
echo
# Check if running as root (we don't want that for the full script)
if [[ $EUID -eq 0 ]]; then
print_status "$RED" "❌ Please don't run this script as root. It will ask for sudo when needed."
exit 1
fi
# Check if we're on Fedora
if [[ ! -f /etc/fedora-release ]]; then
print_status "$YELLOW" "⚠️ Warning: This script is designed for Fedora. It may work on other RPM-based distributions."
read -p "Continue anyway? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0
fi
fi
# Check if Nautilus is available
if ! command_exists nautilus; then
print_status "$RED" "❌ Error: Nautilus file manager not found. This script requires GNOME desktop environment."
exit 1
fi
# Run installation steps
install_dependencies
install_script
install_nautilus_action
restart_nautilus
create_test_video
show_usage_instructions
print_status "$GREEN" "✨ Installation completed! You can now right-click on video files to convert them to WebP!"
}
# Run main function
main "$@"
4. 🧪 File: test.sh
#!/bin/bash
# Test script for Video to WebP converter
# Creates a test video and converts it to verify installation
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to create test video
create_test_video() {
local test_file="test_sample.mp4"
print_status "$BLUE" "🎬 Creating test video..."
# Create a colorful test pattern with text overlay
ffmpeg -y \
-f lavfi -i "testsrc2=duration=5:size=640x480:rate=30" \
-f lavfi -i "sine=frequency=440:duration=5" \
-vf "drawtext=text='Test Animation':x=(w-text_w)/2:y=(h-text_h)/2:fontsize=48:fontcolor=white:box=1:boxcolor=black@0.5" \
-c:v libx264 -crf 23 -preset fast \
-c:a aac -b:a 128k \
"$test_file" 2>/dev/null
if [[ -f "$test_file" ]]; then
print_status "$GREEN" "✅ Test video created: $test_file"
return 0
else
print_status "$RED" "❌ Failed to create test video"
return 1
fi
}
# Function to test the converter
test_converter() {
local test_file="test_sample.mp4"
local expected_output="test_sample_loop.webp"
print_status "$BLUE" "🔄 Testing video converter..."
if [[ ! -f "$test_file" ]]; then
print_status "$RED" "❌ Test video not found"
return 1
fi
# Test the converter script
if command -v video-to-webp.sh &>/dev/null; then
print_status "$BLUE" "📝 Running conversion test..."
if video-to-webp.sh "$test_file"; then
if [[ -f "$expected_output" ]]; then
local input_size output_size
input_size=$(du -h "$test_file" | cut -f1)
output_size=$(du -h "$expected_output" | cut -f1)
print_status "$GREEN" "✅ Conversion test successful!"
print_status "$GREEN" " Input: $input_size → Output: $output_size"
return 0
else
print_status "$RED" "❌ Output file not created"
return 1
fi
else
print_status "$RED" "❌ Conversion failed"
return 1
fi
else
print_status "$RED" "❌ video-to-webp.sh script not found in PATH"
print_status "$YELLOW" " Run the installer first: ./install.sh"
return 1
fi
}
# Function to check Nautilus integration
check_nautilus_integration() {
local action_file="$HOME/.local/share/file-manager/actions/video-to-webp.desktop"
print_status "$BLUE" "🗂️ Checking Nautilus integration..."
if [[ -f "$action_file" ]]; then
print_status "$GREEN" "✅ Nautilus action file found"
# Check if the action file is valid
if grep -q "video-to-webp.sh" "$action_file"; then
print_status "$GREEN" "✅ Action file points to correct script"
else
print_status "$YELLOW" "⚠️ Action file may have incorrect path"
fi
return 0
else
print_status "$RED" "❌ Nautilus action file not found"
print_status "$YELLOW" " Expected: $action_file"
return 1
fi
}
# Function to check dependencies
check_dependencies() {
print_status "$BLUE" "🔍 Checking dependencies..."
local deps=("ffmpeg" "ffprobe" "bc" "notify-send")
local missing=()
for dep in "${deps[@]}"; do
if command -v "$dep" &>/dev/null; then
print_status "$GREEN" "✅ $dep found"
else
print_status "$RED" "❌ $dep missing"
missing+=("$dep")
fi
done
if [[ ${#missing[@]} -eq 0 ]]; then
print_status "$GREEN" "✅ All dependencies satisfied"
return 0
else
print_status "$RED" "❌ Missing dependencies: ${missing[*]}"
return 1
fi
}
# Function to cleanup test files
cleanup_test_files() {
local files=("test_sample.mp4" "test_sample_loop.webp" "test_sample_tmp_cfr.mp4" "test_sample_tmp_loop.mp4")
print_status "$BLUE" "🧹 Cleaning up test files..."
for file in "${files[@]}"; do
if [[ -f "$file" ]]; then
rm -f "$file"
print_status "$GREEN" " Removed: $file"
fi
done
}
# Function to show test results
show_test_results() {
echo
print_status "$BLUE" "📋 Test Summary:"
echo "==================="
if check_dependencies; then
echo "✅ Dependencies: OK"
else
echo "❌ Dependencies: MISSING"
fi
if check_nautilus_integration; then
echo "✅ Nautilus Integration: OK"
else
echo "❌ Nautilus Integration: MISSING"
fi
if command -v video-to-webp.sh &>/dev/null; then
echo "✅ Script Installation: OK"
else
echo "❌ Script Installation: MISSING"
fi
echo "==================="
}
# Main test function
main() {
print_status "$GREEN" "🧪 Starting Video to WebP Converter Test Suite"
echo
# Check if we should cleanup first
if [[ "${1:-}" == "--cleanup" ]]; then
cleanup_test_files
exit 0
fi
# Run checks
local overall_success=true
if ! check_dependencies; then
overall_success=false
fi
if ! check_nautilus_integration; then
overall_success=false
fi
# Only run conversion test if basic checks pass
if [[ "$overall_success" == true ]]; then
if create_test_video && test_converter; then
print_status "$GREEN" "🎉 All tests passed! The converter is working properly."
else
overall_success=false
fi
fi
show_test_results
# Cleanup option
echo
read -p "Clean up test files? (Y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$|^$ ]]; then
cleanup_test_files
fi
if [[ "$overall_success" == true ]]; then
print_status "$GREEN" "✨ Installation verified! You can now use the converter."
echo
print_status "$BLUE" "💡 How to use:"
echo "1. Open Nautilus file manager"
echo "2. Right-click on any video file"
echo "3. Select 'Convert to Animated WebP'"
exit 0
else
print_status "$RED" "❌ Some tests failed. Please check the installation."
echo
print_status "$YELLOW" "💡 Try running the installer again:"
echo "./install.sh"
exit 1
fi
}
# Show usage if needed
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
echo "Video to WebP Converter Test Suite"
echo "Usage: $0 [--cleanup|--help]"
echo
echo "Options:"
echo " --cleanup Remove test files and exit"
echo " --help Show this help message"
echo
echo "This script tests the installation of the video-to-webp converter."
exit 0
fi
# Run main function
main "$@"
📋 INSTALLATION INSTRUCTIONS
🚀 One-Time Installation:
# 1. Create folder and all files
mkdir video-to-webp-converter
cd video-to-webp-converter
# 2. Create each file with its content (copy-paste from this README)
nano video-to-webp.sh # Paste main script content
nano video-to-webp.desktop # Paste desktop file content
nano install.sh # Paste installation script content
nano test.sh # Paste test script content
# 3. Make scripts executable
chmod +x *.sh
# 4. Run installation
./install.sh
# 5. (Optional) Test installation
./test.sh
you can also, copy the script into your folder share nautilus.
for me it's "/home/andycinquin/.local/share/nautilus/scripts"

⚙️ Advanced Configuration
Modify settings in
video-to-webp.sh
:FPS_INTERMEDIATE=24 # Intermediate FPS for processing
FPS_OUTPUT=30 # Final WebP FPS
CRF=18 # Video quality (lower = better quality)
PRESET="veryfast" # Encoding speed
FADE_DURATION=0.5 # Fade duration for loop
FADE_OFFSET=3.5 # Fade offset
🔧 Conversion Process
The script automatically performs 3 steps:
- Normalization: Convert to constant framerate (24fps)
- Seamless loop: Create fade between end and beginning
- WebP export: Final conversion to animated WebP (30fps)
🐛 Troubleshooting
Context menu doesn't appear
pkill nautilus && nautilus &
FFmpeg not found
sudo dnf install ffmpeg
Permission errors
# Check script permissions
ls -la /usr/local/bin/video-to-webp.sh
# Should show: -rwxr-xr-x
Complete reinstallation
sudo rm -f /usr/local/bin/video-to-webp.sh
rm -f ~/.local/share/file-manager/actions/video-to-webp.desktop
./install.sh
📊 Example Results
- 10MB MP4 video → ~2-3MB WebP
- Visual quality: Excellent for short animations
- Conversion time: ~30 seconds for 10 seconds of video
- Compatibility: Works in all modern browsers
🎯 Ideal Use Cases
- GIF replacement: Reduced size, superior quality
- Web animations: Optimized performance
- Social media: Modern and efficient format
- Presentations: Smooth and lightweight animations
🛠️ Uninstallation
To completely remove the system:
# Remove main script
sudo rm -f /usr/local/bin/video-to-webp.sh
# Remove Nautilus action
rm -f ~/.local/share/file-manager/actions/video-to-webp.desktop
# Restart Nautilus
pkill nautilus && nautilus &
📁 File Structure
video-to-webp-converter/
├── video-to-webp.sh # Main conversion script
├── video-to-webp.desktop # Nautilus action file
├── install.sh # Automatic installation script
└── test.sh # Test and verification script
📝 Technical Notes
- Seamless loop: Uses FFmpeg's
xfade
for imperceptible fade - Optimization: CRF 18 to balance quality/size
- Cleanup: Automatic removal of temporary files
- Security: Input validation and robust error handling
🤝 Contributing
Feel free to report bugs or suggest improvements!
Author: Assistant
License: MIT
Version: 1.0.0
License: MIT
Version: 1.0.0
✨ Complete portable kit! Copy each script, install and enjoy one-click animated WebP conversion! ✨
🚀 Thanks for reading!
If you enjoyed this article, feel free to share it around.
💡 Got an idea? Let's talk !☕