Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:52 +08:00
commit 6ec3196ecc
434 changed files with 125248 additions and 0 deletions

View File

@@ -0,0 +1,358 @@
---
name: media-processing
description: Process multimedia files with FFmpeg (video/audio encoding, conversion, streaming, filtering, hardware acceleration) and ImageMagick (image manipulation, format conversion, batch processing, effects, composition). Use when converting media formats, encoding videos with specific codecs (H.264, H.265, VP9), resizing/cropping images, extracting audio from video, applying filters and effects, optimizing file sizes, creating streaming manifests (HLS/DASH), generating thumbnails, batch processing images, creating composite images, or implementing media processing pipelines. Supports 100+ formats, hardware acceleration (NVENC, QSV), and complex filtergraphs.
license: MIT
---
# Media Processing Skill
Process video, audio, and images using FFmpeg and ImageMagick command-line tools for conversion, optimization, streaming, and manipulation tasks.
## When to Use This Skill
Use when:
- Converting media formats (video, audio, images)
- Encoding video with codecs (H.264, H.265, VP9, AV1)
- Processing images (resize, crop, effects, watermarks)
- Extracting audio from video
- Creating streaming manifests (HLS/DASH)
- Generating thumbnails and previews
- Batch processing media files
- Optimizing file sizes and quality
- Applying filters and effects
- Creating composite images or videos
## Tool Selection Guide
### FFmpeg: Video/Audio Processing
Use FFmpeg for:
- Video encoding, conversion, transcoding
- Audio extraction, conversion, mixing
- Live streaming (RTMP, HLS, DASH)
- Video filters (scale, crop, rotate, overlay)
- Hardware-accelerated encoding
- Media file inspection (ffprobe)
- Frame extraction, concatenation
- Codec selection and optimization
### ImageMagick: Image Processing
Use ImageMagick for:
- Image format conversion (PNG, JPEG, WebP, GIF)
- Resizing, cropping, transformations
- Batch image processing (mogrify)
- Visual effects (blur, sharpen, sepia)
- Text overlays and watermarks
- Image composition and montages
- Color adjustments, filters
- Thumbnail generation
### Decision Matrix
| Task | Tool | Why |
|------|------|-----|
| Video encoding | FFmpeg | Native video codec support |
| Audio extraction | FFmpeg | Direct stream manipulation |
| Image resize | ImageMagick | Optimized for still images |
| Batch images | ImageMagick | mogrify for in-place edits |
| Video thumbnails | FFmpeg | Frame extraction built-in |
| GIF creation | FFmpeg or ImageMagick | FFmpeg for video source, ImageMagick for images |
| Streaming | FFmpeg | Live streaming protocols |
| Image effects | ImageMagick | Rich filter library |
## Installation
### macOS
```bash
brew install ffmpeg imagemagick
```
### Ubuntu/Debian
```bash
sudo apt-get install ffmpeg imagemagick
```
### Windows
```bash
# Using winget
winget install ffmpeg
winget install ImageMagick.ImageMagick
# Or download binaries
# FFmpeg: https://ffmpeg.org/download.html
# ImageMagick: https://imagemagick.org/script/download.php
```
### Verify Installation
```bash
ffmpeg -version
ffprobe -version
magick -version
# or
convert -version
```
## Quick Start Examples
### Video Conversion
```bash
# Convert format (copy streams, fast)
ffmpeg -i input.mkv -c copy output.mp4
# Re-encode with H.264
ffmpeg -i input.avi -c:v libx264 -crf 22 -c:a aac output.mp4
# Resize video to 720p
ffmpeg -i input.mp4 -vf scale=-1:720 -c:a copy output.mp4
```
### Audio Extraction
```bash
# Extract audio (no re-encoding)
ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
# Convert to MP3
ffmpeg -i video.mp4 -vn -q:a 0 audio.mp3
```
### Image Processing
```bash
# Convert format
magick input.png output.jpg
# Resize maintaining aspect ratio
magick input.jpg -resize 800x600 output.jpg
# Create square thumbnail
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 thumb.jpg
```
### Batch Image Resize
```bash
# Resize all JPEGs to 800px width
mogrify -resize 800x -quality 85 *.jpg
# Output to separate directory
mogrify -path ./output -resize 800x600 *.jpg
```
### Video Thumbnail
```bash
# Extract frame at 5 seconds
ffmpeg -ss 00:00:05 -i video.mp4 -vframes 1 -vf scale=320:-1 thumb.jpg
```
### HLS Streaming
```bash
# Generate HLS playlist
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 22 -g 48 \
-c:a aac -b:a 128k \
-f hls -hls_time 6 -hls_playlist_type vod \
playlist.m3u8
```
### Image Watermark
```bash
# Add watermark to corner
magick input.jpg watermark.png -gravity southeast \
-geometry +10+10 -composite output.jpg
```
## Common Workflows
### Optimize Video for Web
```bash
# H.264 with good compression
ffmpeg -i input.mp4 \
-c:v libx264 -preset slow -crf 23 \
-c:a aac -b:a 128k \
-movflags +faststart \
output.mp4
```
### Create Responsive Images
```bash
# Generate multiple sizes
for size in 320 640 1024 1920; do
magick input.jpg -resize ${size}x -quality 85 "output-${size}w.jpg"
done
```
### Extract Video Segment
```bash
# From 1:30 to 3:00 (re-encode for precision)
ffmpeg -i input.mp4 -ss 00:01:30 -to 00:03:00 \
-c:v libx264 -c:a aac output.mp4
```
### Batch Image Optimization
```bash
# Convert PNG to optimized JPEG
mogrify -path ./optimized -format jpg -quality 85 -strip *.png
```
### Video GIF Creation
```bash
# High quality GIF with palette
ffmpeg -i input.mp4 -vf "fps=15,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" output.gif
```
### Image Blur Effect
```bash
# Gaussian blur
magick input.jpg -gaussian-blur 0x8 output.jpg
```
## Advanced Techniques
### Multi-Pass Video Encoding
```bash
# Pass 1 (analysis)
ffmpeg -y -i input.mkv -c:v libx264 -b:v 2600k -pass 1 -an -f null /dev/null
# Pass 2 (encoding)
ffmpeg -i input.mkv -c:v libx264 -b:v 2600k -pass 2 -c:a aac output.mp4
```
### Hardware-Accelerated Encoding
```bash
# NVIDIA NVENC
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset fast -crf 22 output.mp4
# Intel QuickSync
ffmpeg -hwaccel qsv -c:v h264_qsv -i input.mp4 -c:v h264_qsv output.mp4
```
### Complex Image Pipeline
```bash
# Resize, crop, border, adjust
magick input.jpg \
-resize 1000x1000^ \
-gravity center \
-crop 1000x1000+0+0 +repage \
-bordercolor black -border 5x5 \
-brightness-contrast 5x10 \
-quality 90 \
output.jpg
```
### Video Filter Chains
```bash
# Scale, denoise, watermark
ffmpeg -i video.mp4 -i logo.png \
-filter_complex "[0:v]scale=1280:720,hqdn3d[v];[v][1:v]overlay=10:10" \
-c:a copy output.mp4
```
### Animated GIF from Images
```bash
# Create with delay
magick -delay 100 -loop 0 frame*.png animated.gif
# Optimize size
magick animated.gif -fuzz 5% -layers Optimize optimized.gif
```
## Media Analysis
### Inspect Video Properties
```bash
# Detailed JSON output
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4
# Get resolution
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height \
-of csv=s=x:p=0 input.mp4
```
### Image Information
```bash
# Basic info
identify image.jpg
# Detailed format
identify -verbose image.jpg
# Custom format
identify -format "%f: %wx%h %b\n" image.jpg
```
## Performance Tips
1. **Use CRF for quality control** - Better than bitrate for video
2. **Copy streams when possible** - Avoid re-encoding with `-c copy`
3. **Hardware acceleration** - GPU encoding 5-10x faster
4. **Appropriate presets** - Balance speed vs compression
5. **Batch with mogrify** - In-place image processing
6. **Strip metadata** - Reduce file size with `-strip`
7. **Progressive JPEG** - Better web loading with `-interlace Plane`
8. **Limit memory** - Prevent crashes on large batches
9. **Test on samples** - Verify settings before batch
10. **Parallel processing** - Use GNU Parallel for multiple files
## Reference Documentation
Detailed guides in `references/`:
- **ffmpeg-encoding.md** - Video/audio codecs, quality optimization, hardware acceleration
- **ffmpeg-streaming.md** - HLS/DASH, live streaming, adaptive bitrate
- **ffmpeg-filters.md** - Video/audio filters, complex filtergraphs
- **imagemagick-editing.md** - Format conversion, effects, transformations
- **imagemagick-batch.md** - Batch processing, mogrify, parallel operations
- **format-compatibility.md** - Format support, codec recommendations
## Common Parameters
### FFmpeg Video
- `-c:v` - Video codec (libx264, libx265, libvpx-vp9)
- `-crf` - Quality (0-51, lower=better, 23=default)
- `-preset` - Speed/compression (ultrafast to veryslow)
- `-b:v` - Video bitrate (e.g., 2M, 2500k)
- `-vf` - Video filters
### FFmpeg Audio
- `-c:a` - Audio codec (aac, mp3, opus)
- `-b:a` - Audio bitrate (e.g., 128k, 192k)
- `-ar` - Sample rate (44100, 48000)
### ImageMagick Geometry
- `800x600` - Fit within (maintains aspect)
- `800x600!` - Force exact size
- `800x600^` - Fill (may crop)
- `800x` - Width only
- `x600` - Height only
- `50%` - Scale percentage
## Troubleshooting
**FFmpeg "Unknown encoder"**
```bash
# Check available encoders
ffmpeg -encoders | grep h264
# Install codec libraries
sudo apt-get install libx264-dev libx265-dev
```
**ImageMagick "not authorized"**
```bash
# Edit policy file
sudo nano /etc/ImageMagick-7/policy.xml
# Change <policy domain="coder" rights="none" pattern="PDF" />
# to <policy domain="coder" rights="read|write" pattern="PDF" />
```
**Memory errors**
```bash
# Limit memory usage
ffmpeg -threads 4 input.mp4 output.mp4
magick -limit memory 2GB -limit map 4GB input.jpg output.jpg
```
## Resources
- FFmpeg: https://ffmpeg.org/documentation.html
- FFmpeg Wiki: https://trac.ffmpeg.org/
- ImageMagick: https://imagemagick.org/
- ImageMagick Usage: https://imagemagick.org/Usage/

View File

@@ -0,0 +1,358 @@
# FFmpeg Video & Audio Encoding
Complete guide to codec selection, quality optimization, and hardware acceleration.
## Video Codecs
### H.264 (libx264)
Most widely supported codec, excellent compression/quality balance.
**Best for:** Universal compatibility, streaming, web video
**Quality range:** CRF 17-28 (lower = better)
```bash
# High quality
ffmpeg -i input.mkv -c:v libx264 -preset slow -crf 18 -c:a copy output.mp4
# Standard quality (recommended)
ffmpeg -i input.mkv -c:v libx264 -preset medium -crf 23 -c:a copy output.mp4
# Fast encoding
ffmpeg -i input.mkv -c:v libx264 -preset fast -crf 23 -c:a copy output.mp4
```
### H.265/HEVC (libx265)
25-50% better compression than H.264, slower encoding.
**Best for:** 4K video, file size reduction, archival
```bash
# High quality 4K
ffmpeg -i input.mkv -c:v libx265 -preset medium -crf 24 -c:a copy output.mp4
# Balanced quality
ffmpeg -i input.mkv -c:v libx265 -preset fast -crf 26 -c:a copy output.mp4
```
### VP9 (libvpx-vp9)
Royalty-free, WebM format, good for YouTube and open-source projects.
**Best for:** YouTube, Chrome/Firefox, open platforms
```bash
# Quality-based (recommended)
ffmpeg -i input.mkv -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus output.webm
# Two-pass for better quality
ffmpeg -i input.mkv -c:v libvpx-vp9 -b:v 2M -pass 1 -an -f null /dev/null
ffmpeg -i input.mkv -c:v libvpx-vp9 -b:v 2M -pass 2 -c:a libopus output.webm
```
### AV1 (libaom-av1, libsvtav1)
Next-generation codec, best compression, very slow encoding.
**Best for:** Future-proofing, maximum compression, low bandwidth
```bash
# Using libaom (slow, highest quality)
ffmpeg -i input.mkv -c:v libaom-av1 -crf 30 -b:v 0 -strict experimental output.mp4
# Using SVT-AV1 (faster)
ffmpeg -i input.mkv -c:v libsvtav1 -crf 30 -preset 5 output.mp4
```
## Audio Codecs
### AAC (Industry Standard)
Best quality for streaming, universal support.
```bash
# High quality
ffmpeg -i input.mp4 -c:a aac -b:a 192k output.mp4
# Standard quality
ffmpeg -i input.mp4 -c:a aac -b:a 128k output.mp4
# Low bitrate
ffmpeg -i input.mp4 -c:a aac -b:a 96k output.mp4
```
### MP3 (libmp3lame)
Universal compatibility, good quality.
```bash
# Variable bitrate (best quality)
ffmpeg -i input.wav -c:a libmp3lame -q:a 0 output.mp3
# Constant bitrate
ffmpeg -i input.wav -c:a libmp3lame -b:a 192k output.mp3
```
### Opus (libopus)
Best quality at low bitrates, ideal for voice and streaming.
```bash
# Voice (mono)
ffmpeg -i input.mp4 -c:a libopus -b:a 32k -ac 1 output.webm
# Music (stereo)
ffmpeg -i input.mp4 -c:a libopus -b:a 128k output.webm
```
### FLAC (Lossless)
No quality loss, archival quality, larger files.
```bash
# Lossless audio
ffmpeg -i input.wav -c:a flac output.flac
# Extract audio losslessly
ffmpeg -i video.mp4 -c:a flac audio.flac
```
## Quality Optimization
### CRF (Constant Rate Factor)
Best for quality-focused encoding. Single-pass, adjusts bitrate for complexity.
**CRF Scale:**
- 0 = Lossless (huge files)
- 17-18 = Visually lossless
- 20-23 = High quality (recommended)
- 24-28 = Medium quality
- 30+ = Low quality
- 51 = Worst quality
```bash
# Visually lossless
ffmpeg -i input.mp4 -c:v libx264 -crf 18 -preset slow output.mp4
# High quality (recommended)
ffmpeg -i input.mp4 -c:v libx264 -crf 22 -preset medium output.mp4
# Balanced quality/size
ffmpeg -i input.mp4 -c:v libx264 -crf 25 -preset fast output.mp4
```
### Bitrate-Based Encoding
Target specific file size or quality. Two-pass recommended.
```bash
# Calculate target bitrate
# bitrate = (target_size_MB * 8192) / duration_seconds - audio_bitrate
# Two-pass encoding (2600k video, 128k audio)
ffmpeg -y -i input.mkv -c:v libx264 -b:v 2600k -pass 1 -an -f null /dev/null
ffmpeg -i input.mkv -c:v libx264 -b:v 2600k -pass 2 -c:a aac -b:a 128k output.mp4
```
### Presets (Speed vs Compression)
Trade-off between encoding speed and file size.
**Available presets:**
- `ultrafast` - Fastest, largest files
- `superfast`
- `veryfast`
- `faster`
- `fast`
- `medium` - Default balance
- `slow` - Better compression
- `slower`
- `veryslow` - Best compression
- `placebo` - Not recommended (minimal gains)
```bash
# Fast encoding (real-time)
ffmpeg -i input.mp4 -c:v libx264 -preset ultrafast -crf 23 output.mp4
# Balanced
ffmpeg -i input.mp4 -c:v libx264 -preset medium -crf 22 output.mp4
# Best compression (slow)
ffmpeg -i input.mp4 -c:v libx264 -preset veryslow -crf 20 output.mp4
```
## Hardware Acceleration
### NVIDIA NVENC
5-10x faster encoding, slightly larger files than software encoding.
**Requirements:** NVIDIA GPU (GTX 10xx or newer)
```bash
# H.264 with NVENC
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset fast -crf 22 output.mp4
# H.265 with NVENC
ffmpeg -hwaccel cuda -i input.mp4 -c:v hevc_nvenc -preset slow -crf 24 output.mp4
# Quality levels (instead of CRF)
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset slow -rc vbr -cq 22 output.mp4
```
**NVENC Presets:**
- `default` - Balanced
- `slow` - Better quality
- `medium`
- `fast`
- `hp` - High performance
- `hq` - High quality
- `bd` - Bluray disk
- `ll` - Low latency
- `llhq` - Low latency high quality
- `llhp` - Low latency high performance
### Intel QuickSync (QSV)
Fast hardware encoding on Intel CPUs with integrated graphics.
**Requirements:** Intel CPU with Quick Sync Video support
```bash
# H.264 with QSV
ffmpeg -hwaccel qsv -c:v h264_qsv -i input.mp4 \
-c:v h264_qsv -preset fast -global_quality 22 output.mp4
# H.265 with QSV
ffmpeg -hwaccel qsv -c:v hevc_qsv -i input.mp4 \
-c:v hevc_qsv -preset medium -global_quality 24 output.mp4
# Quality levels
ffmpeg -hwaccel qsv -i input.mp4 -c:v h264_qsv -global_quality 20 output.mp4
```
### AMD VCE/VCN
Hardware encoding on AMD GPUs.
**Requirements:** AMD GPU with VCE/VCN support
```bash
# H.264 with AMF
ffmpeg -hwaccel auto -i input.mp4 \
-c:v h264_amf -quality balanced -rc cqp -qp 22 output.mp4
# H.265 with AMF
ffmpeg -hwaccel auto -i input.mp4 \
-c:v hevc_amf -quality quality -rc cqp -qp 24 output.mp4
```
### Apple VideoToolbox (macOS)
Hardware encoding on macOS devices.
```bash
# H.264 with VideoToolbox
ffmpeg -i input.mp4 -c:v h264_videotoolbox -b:v 2M output.mp4
# H.265 with VideoToolbox
ffmpeg -i input.mp4 -c:v hevc_videotoolbox -b:v 1.5M output.mp4
```
## Performance Tuning
### Multi-Threading
FFmpeg automatically uses multiple cores. Override if needed:
```bash
# Limit threads
ffmpeg -threads 4 -i input.mp4 -c:v libx264 output.mp4
# Auto (default)
ffmpeg -threads 0 -i input.mp4 -c:v libx264 output.mp4
```
### Tune Options
Optimize encoder for specific content types:
```bash
# Film content
ffmpeg -i input.mp4 -c:v libx264 -tune film -crf 22 output.mp4
# Animation
ffmpeg -i input.mp4 -c:v libx264 -tune animation -crf 22 output.mp4
# Grain (film with noise)
ffmpeg -i input.mp4 -c:v libx264 -tune grain -crf 22 output.mp4
# Low latency streaming
ffmpeg -i input.mp4 -c:v libx264 -tune zerolatency -crf 22 output.mp4
# Screen content (sharp edges)
ffmpeg -i input.mp4 -c:v libx264 -tune stillimage -crf 22 output.mp4
```
## Codec Selection Guide
### Use Cases
| Use Case | Codec | Settings |
|----------|-------|----------|
| Web video | H.264 | CRF 23, preset medium |
| 4K streaming | H.265 | CRF 24, preset fast |
| YouTube upload | VP9 or H.264 | CRF 23 |
| Archive | H.265 or H.264 | CRF 18, preset slow |
| Low bandwidth | AV1 or H.265 | CRF 30 |
| Fast encoding | H.264 NVENC | preset fast |
| Maximum compatibility | H.264 | profile main, level 4.0 |
### Platform Compatibility
| Platform | Recommended | Supported |
|----------|------------|-----------|
| Web browsers | H.264 | H.264, VP9, AV1 |
| Mobile devices | H.264 | H.264, H.265 |
| Smart TVs | H.264 | H.264, H.265 |
| YouTube | VP9, H.264 | All |
| Social media | H.264 | H.264 |
## Best Practices
1. **Use CRF for most tasks** - Better than bitrate for variable content
2. **Start with CRF 23** - Good balance, adjust based on results
3. **Use slow preset** - For archival and final delivery
4. **Use fast preset** - For previews and testing
5. **Hardware acceleration** - When speed is critical
6. **Two-pass encoding** - When file size is fixed
7. **Match source frame rate** - Don't increase FPS
8. **Don't upscale resolution** - Keep original or downscale
9. **Test on short clips** - Verify settings before full encode
10. **Keep source files** - Original quality for re-encoding
## Troubleshooting
### Poor Quality Output
```bash
# Lower CRF value
ffmpeg -i input.mp4 -c:v libx264 -crf 18 -preset slow output.mp4
# Use slower preset
ffmpeg -i input.mp4 -c:v libx264 -crf 22 -preset veryslow output.mp4
# Increase bitrate (two-pass)
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 5M -pass 1 -an -f null /dev/null
ffmpeg -i input.mp4 -c:v libx264 -b:v 5M -pass 2 -c:a aac output.mp4
```
### Slow Encoding
```bash
# Use faster preset
ffmpeg -i input.mp4 -c:v libx264 -preset ultrafast output.mp4
# Use hardware acceleration
ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc output.mp4
# Reduce resolution
ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libx264 output.mp4
```
### Large File Size
```bash
# Increase CRF
ffmpeg -i input.mp4 -c:v libx264 -crf 26 output.mp4
# Use better codec
ffmpeg -i input.mp4 -c:v libx265 -crf 26 output.mp4
# Two-pass with target bitrate
ffmpeg -y -i input.mp4 -c:v libx264 -b:v 1M -pass 1 -an -f null /dev/null
ffmpeg -i input.mp4 -c:v libx264 -b:v 1M -pass 2 -c:a aac output.mp4
```

View File

@@ -0,0 +1,503 @@
# FFmpeg Filters & Effects
Complete guide to video and audio filters, complex filtergraphs, and effect chains.
## Filter Basics
### Filter Syntax
Filters are applied with `-vf` (video) or `-af` (audio).
```bash
# Single filter
ffmpeg -i input.mp4 -vf scale=1280:720 output.mp4
# Chain filters with comma
ffmpeg -i input.mp4 -vf "scale=1280:720,hqdn3d" output.mp4
# Complex filtergraph with -filter_complex
ffmpeg -i input.mp4 -i logo.png \
-filter_complex "[0:v][1:v]overlay=10:10" \
output.mp4
```
## Video Filters
### Scale (Resize)
Change video dimensions.
```bash
# Specific dimensions
ffmpeg -i input.mp4 -vf scale=1280:720 output.mp4
# Maintain aspect ratio (auto height)
ffmpeg -i input.mp4 -vf scale=1280:-1 output.mp4
# Maintain aspect ratio (auto width)
ffmpeg -i input.mp4 -vf scale=-1:720 output.mp4
# Scale to half
ffmpeg -i input.mp4 -vf scale=iw/2:ih/2 output.mp4
# Scale with algorithm
ffmpeg -i input.mp4 -vf scale=1280:-1:flags=lanczos output.mp4
```
**Scaling algorithms:**
- `bilinear` - Fast, default
- `bicubic` - Better quality
- `lanczos` - Best quality, slower
### Crop
Extract portion of video.
```bash
# Crop width:height:x:y
ffmpeg -i input.mp4 -vf crop=1280:720:0:0 output.mp4
# Crop from center
ffmpeg -i input.mp4 -vf crop=1280:720:(iw-1280)/2:(ih-720)/2 output.mp4
# Auto-detect black borders
ffmpeg -i input.mp4 -vf cropdetect -f null -
# Apply detected crop
ffmpeg -i input.mp4 -vf crop=1920:800:0:140 output.mp4
```
### Rotate & Flip
Change video orientation.
```bash
# Rotate 90° clockwise
ffmpeg -i input.mp4 -vf transpose=1 output.mp4
# Rotate 90° counter-clockwise
ffmpeg -i input.mp4 -vf transpose=2 output.mp4
# Rotate 180°
ffmpeg -i input.mp4 -vf transpose=1,transpose=1 output.mp4
# Flip horizontal
ffmpeg -i input.mp4 -vf hflip output.mp4
# Flip vertical
ffmpeg -i input.mp4 -vf vflip output.mp4
# Rotate arbitrary angle
ffmpeg -i input.mp4 -vf rotate=45*PI/180 output.mp4
```
### Overlay (Watermark)
Composite images over video.
```bash
# Top-left corner
ffmpeg -i video.mp4 -i logo.png \
-filter_complex overlay=10:10 output.mp4
# Top-right corner
ffmpeg -i video.mp4 -i logo.png \
-filter_complex "overlay=W-w-10:10" output.mp4
# Bottom-right corner
ffmpeg -i video.mp4 -i logo.png \
-filter_complex "overlay=W-w-10:H-h-10" output.mp4
# Center
ffmpeg -i video.mp4 -i logo.png \
-filter_complex "overlay=(W-w)/2:(H-h)/2" output.mp4
# With transparency
ffmpeg -i video.mp4 -i logo.png \
-filter_complex "[1:v]format=rgba,colorchannelmixer=aa=0.5[logo];[0:v][logo]overlay=10:10" \
output.mp4
```
### Denoise
Reduce video noise.
```bash
# High-quality denoise (hqdn3d)
ffmpeg -i input.mp4 -vf hqdn3d output.mp4
# Stronger denoise
ffmpeg -i input.mp4 -vf hqdn3d=4:3:6:4.5 output.mp4
# Temporal denoise (nlmeans - slow but best)
ffmpeg -i input.mp4 -vf nlmeans output.mp4
# Fast denoise
ffmpeg -i input.mp4 -vf dctdnoiz output.mp4
```
### Deinterlace
Remove interlacing artifacts.
```bash
# YADIF (fast, good quality)
ffmpeg -i input.mp4 -vf yadif output.mp4
# YADIF with frame doubling
ffmpeg -i input.mp4 -vf yadif=1 output.mp4
# Bwdif (better quality)
ffmpeg -i input.mp4 -vf bwdif output.mp4
```
### Speed & Slow Motion
Change playback speed.
```bash
# 2x speed (video + audio)
ffmpeg -i input.mp4 -vf setpts=0.5*PTS -af atempo=2.0 output.mp4
# 0.5x speed (slow motion)
ffmpeg -i input.mp4 -vf setpts=2.0*PTS -af atempo=0.5 output.mp4
# 4x speed (chain atempo)
ffmpeg -i input.mp4 -vf setpts=0.25*PTS -af atempo=2.0,atempo=2.0 output.mp4
```
### Pad (Add Borders)
Add borders or letterbox.
```bash
# Add black borders to make 16:9
ffmpeg -i input.mp4 -vf "pad=1920:1080:(ow-iw)/2:(oh-ih)/2" output.mp4
# Add colored borders
ffmpeg -i input.mp4 -vf "pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=white" output.mp4
# Letterbox for Instagram (1:1)
ffmpeg -i input.mp4 -vf "scale=1080:-1,pad=1080:1080:(ow-iw)/2:(oh-ih)/2:color=black" output.mp4
```
### Sharpen & Blur
Adjust image sharpness.
```bash
# Sharpen (unsharp mask)
ffmpeg -i input.mp4 -vf unsharp=5:5:1.0 output.mp4
# Stronger sharpen
ffmpeg -i input.mp4 -vf unsharp=7:7:2.5 output.mp4
# Gaussian blur
ffmpeg -i input.mp4 -vf gblur=sigma=8 output.mp4
# Box blur
ffmpeg -i input.mp4 -vf boxblur=5:1 output.mp4
```
### Color Adjustments
Modify colors and exposure.
```bash
# Brightness (+/- 1.0)
ffmpeg -i input.mp4 -vf eq=brightness=0.1 output.mp4
# Contrast (+/- 2.0)
ffmpeg -i input.mp4 -vf eq=contrast=1.2 output.mp4
# Saturation (0-3)
ffmpeg -i input.mp4 -vf eq=saturation=1.5 output.mp4
# Gamma (0.1-10)
ffmpeg -i input.mp4 -vf eq=gamma=1.2 output.mp4
# Combined adjustments
ffmpeg -i input.mp4 -vf eq=brightness=0.05:contrast=1.1:saturation=1.2 output.mp4
# Curves (color grading)
ffmpeg -i input.mp4 -vf curves=vintage output.mp4
# Hue shift
ffmpeg -i input.mp4 -vf hue=h=90 output.mp4
```
### Grayscale & Effects
Convert to monochrome or apply effects.
```bash
# Grayscale
ffmpeg -i input.mp4 -vf hue=s=0 output.mp4
# Sepia tone
ffmpeg -i input.mp4 -vf colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131 output.mp4
# Negative
ffmpeg -i input.mp4 -vf negate output.mp4
# Edge detection
ffmpeg -i input.mp4 -vf edgedetect output.mp4
# Vignette
ffmpeg -i input.mp4 -vf vignette output.mp4
```
### Fade In/Out
Smooth transitions.
```bash
# Fade in from black (2 seconds)
ffmpeg -i input.mp4 -vf fade=in:0:60 output.mp4
# Fade out to black (last 2 seconds)
ffmpeg -i input.mp4 -vf fade=out:st=28:d=2 output.mp4
# Both fade in and out
ffmpeg -i input.mp4 -vf "fade=in:0:30,fade=out:st=28:d=2" output.mp4
```
### Stabilization
Reduce camera shake.
```bash
# Two-pass stabilization
# Pass 1: detect motion
ffmpeg -i input.mp4 -vf vidstabdetect=shakiness=10:accuracy=15 -f null -
# Pass 2: stabilize
ffmpeg -i input.mp4 -vf vidstabtransform=smoothing=30:input="transforms.trf" output.mp4
```
### Text Overlay
Add text to video.
```bash
# Simple text
ffmpeg -i input.mp4 -vf "drawtext=text='Hello World':fontsize=24:x=10:y=10" output.mp4
# With styling
ffmpeg -i input.mp4 -vf "drawtext=text='Title':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=50:box=1:boxcolor=black@0.5:boxborderw=5" output.mp4
# Timestamp
ffmpeg -i input.mp4 -vf "drawtext=text='%{pts\:hms}':fontsize=20:x=10:y=10:fontcolor=white" output.mp4
```
## Audio Filters
### Volume
Adjust audio level.
```bash
# Increase by 10dB
ffmpeg -i input.mp4 -af volume=10dB output.mp4
# Decrease to 50%
ffmpeg -i input.mp4 -af volume=0.5 output.mp4
# Double volume
ffmpeg -i input.mp4 -af volume=2.0 output.mp4
```
### Normalize
Balance audio levels.
```bash
# Loudness normalization (EBU R128)
ffmpeg -i input.mp4 -af loudnorm output.mp4
# With specific target
ffmpeg -i input.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 output.mp4
# Two-pass normalization (better quality)
# Pass 1: analyze
ffmpeg -i input.mp4 -af loudnorm=print_format=json -f null -
# Pass 2: normalize with measured values
ffmpeg -i input.mp4 -af loudnorm=measured_I=-23:measured_LRA=7:measured_TP=-2:measured_thresh=-33 output.mp4
```
### Equalizer
Adjust frequency bands.
```bash
# Bass boost
ffmpeg -i input.mp4 -af equalizer=f=100:width_type=h:width=200:g=10 output.mp4
# Treble boost
ffmpeg -i input.mp4 -af equalizer=f=10000:width_type=h:width=2000:g=5 output.mp4
# Multiple bands
ffmpeg -i input.mp4 -af "equalizer=f=100:g=5,equalizer=f=1000:g=-3" output.mp4
```
### Compressor
Dynamic range compression.
```bash
# Basic compression
ffmpeg -i input.mp4 -af acompressor output.mp4
# Custom settings
ffmpeg -i input.mp4 -af acompressor=threshold=-20dB:ratio=4:attack=200:release=1000 output.mp4
```
### Noise Reduction
Remove background noise.
```bash
# High-pass filter (remove low frequency noise)
ffmpeg -i input.mp4 -af highpass=f=200 output.mp4
# Low-pass filter (remove high frequency noise)
ffmpeg -i input.mp4 -af lowpass=f=3000 output.mp4
# Band-pass filter
ffmpeg -i input.mp4 -af "highpass=f=200,lowpass=f=3000" output.mp4
```
### Fade Audio
Smooth audio transitions.
```bash
# Fade in (2 seconds)
ffmpeg -i input.mp4 -af afade=t=in:st=0:d=2 output.mp4
# Fade out (last 3 seconds)
ffmpeg -i input.mp4 -af afade=t=out:st=27:d=3 output.mp4
# Both
ffmpeg -i input.mp4 -af "afade=t=in:st=0:d=2,afade=t=out:st=27:d=3" output.mp4
```
### Audio Mixing
Combine multiple audio tracks.
```bash
# Mix two audio files
ffmpeg -i audio1.mp3 -i audio2.mp3 \
-filter_complex amix=inputs=2:duration=longest output.mp3
# Mix with volume adjustment
ffmpeg -i audio1.mp3 -i audio2.mp3 \
-filter_complex "[0:a]volume=0.8[a1];[1:a]volume=0.5[a2];[a1][a2]amix=inputs=2" \
output.mp3
```
## Complex Filtergraphs
### Multiple Outputs
Create multiple versions simultaneously.
```bash
# Generate 3 resolutions at once
ffmpeg -i input.mp4 \
-filter_complex "[0:v]split=3[v1][v2][v3]; \
[v1]scale=1920:1080[out1]; \
[v2]scale=1280:720[out2]; \
[v3]scale=640:360[out3]" \
-map "[out1]" -c:v libx264 -crf 22 output_1080p.mp4 \
-map "[out2]" -c:v libx264 -crf 23 output_720p.mp4 \
-map "[out3]" -c:v libx264 -crf 24 output_360p.mp4 \
-map 0:a -c:a copy
```
### Picture-in-Picture
Overlay small video on main video.
```bash
ffmpeg -i main.mp4 -i small.mp4 \
-filter_complex "[1:v]scale=320:180[pip]; \
[0:v][pip]overlay=W-w-10:H-h-10" \
output.mp4
```
### Side-by-Side Comparison
Compare two videos.
```bash
# Horizontal
ffmpeg -i left.mp4 -i right.mp4 \
-filter_complex "[0:v][1:v]hstack=inputs=2" \
output.mp4
# Vertical
ffmpeg -i top.mp4 -i bottom.mp4 \
-filter_complex "[0:v][1:v]vstack=inputs=2" \
output.mp4
```
### Crossfade Transition
Smooth transition between videos.
```bash
ffmpeg -i video1.mp4 -i video2.mp4 \
-filter_complex "[0:v][1:v]xfade=transition=fade:duration=2:offset=8" \
output.mp4
```
**Transition types:** fade, wipeleft, wiperight, wipeup, wipedown, slideleft, slideright, slideup, slidedown, circlecrop, rectcrop, distance, fadeblack, fadewhite, radial, smoothleft, smoothright, smoothup, smoothdown
### Color Correction Pipeline
Professional color grading.
```bash
ffmpeg -i input.mp4 \
-filter_complex "[0:v]eq=contrast=1.1:brightness=0.05:saturation=1.2[v1]; \
[v1]curves=vintage[v2]; \
[v2]vignette[v3]; \
[v3]unsharp=5:5:1.0[out]" \
-map "[out]" -c:v libx264 -crf 18 output.mp4
```
## Filter Performance
### GPU Acceleration
Use hardware filters when available.
```bash
# NVIDIA CUDA scale
ffmpeg -hwaccel cuda -i input.mp4 \
-vf scale_cuda=1280:720 \
-c:v h264_nvenc output.mp4
# Multiple GPU filters
ffmpeg -hwaccel cuda -i input.mp4 \
-vf "scale_cuda=1280:720,hwdownload,format=nv12" \
-c:v h264_nvenc output.mp4
```
### Optimize Filter Order
More efficient filter chains.
```bash
# Bad: scale after complex operations
ffmpeg -i input.mp4 -vf "hqdn3d,unsharp=5:5:1.0,scale=1280:720" output.mp4
# Good: scale first (fewer pixels to process)
ffmpeg -i input.mp4 -vf "scale=1280:720,hqdn3d,unsharp=5:5:1.0" output.mp4
```
## Common Filter Recipes
### YouTube Optimized
```bash
ffmpeg -i input.mp4 \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
-c:v libx264 -preset slow -crf 18 -c:a aac -b:a 192k \
output.mp4
```
### Instagram Portrait
```bash
ffmpeg -i input.mp4 \
-vf "scale=1080:1350:force_original_aspect_ratio=decrease,pad=1080:1350:(ow-iw)/2:(oh-ih)/2:color=white" \
-c:v libx264 -preset fast -crf 23 -c:a aac \
output.mp4
```
### Vintage Film Look
```bash
ffmpeg -i input.mp4 \
-vf "curves=vintage,vignette=angle=PI/4,eq=saturation=0.8,noise=alls=10:allf=t" \
-c:v libx264 -crf 20 output.mp4
```
### Clean & Enhance
```bash
ffmpeg -i input.mp4 \
-vf "hqdn3d=4:3:6:4.5,unsharp=5:5:1.0,eq=contrast=1.05:saturation=1.1" \
-c:v libx264 -crf 20 output.mp4
```

View File

@@ -0,0 +1,403 @@
# FFmpeg Streaming & Live Video
Complete guide to HLS/DASH streaming, live streaming platforms, and adaptive bitrate encoding.
## HLS (HTTP Live Streaming)
### Basic HLS Stream
Generate playlist for on-demand streaming.
```bash
# Simple HLS with default settings
ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_segment_filename "segment_%03d.ts" \
playlist.m3u8
```
**Key parameters:**
- `-hls_time` - Segment duration (seconds, default 2)
- `-hls_playlist_type` - `vod` (on-demand) or `event` (live)
- `-hls_segment_filename` - Naming pattern for segments
### Optimized HLS
Better quality and compatibility.
```bash
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 22 \
-g 48 -sc_threshold 0 \
-c:a aac -b:a 128k \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_segment_filename "segment_%03d.ts" \
playlist.m3u8
```
**Parameters explained:**
- `-g 48` - Keyframe every 48 frames (2s @ 24fps)
- `-sc_threshold 0` - Disable scene detection (consistent segments)
### Multi-Bitrate HLS (Adaptive)
Create multiple quality levels for adaptive streaming.
```bash
ffmpeg -i input.mp4 \
-map 0:v -map 0:a -map 0:v -map 0:a -map 0:v -map 0:a \
-c:v libx264 -crf 22 -c:a aac -b:a 128k \
-b:v:0 800k -s:v:0 640x360 -maxrate:v:0 856k -bufsize:v:0 1200k \
-b:v:1 1400k -s:v:1 842x480 -maxrate:v:1 1498k -bufsize:v:1 2100k \
-b:v:2 2800k -s:v:2 1280x720 -maxrate:v:2 2996k -bufsize:v:2 4200k \
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
-master_pl_name master.m3u8 \
-f hls -hls_time 6 -hls_list_size 0 \
-hls_segment_filename "stream_%v/segment_%03d.ts" \
stream_%v/playlist.m3u8
```
**Creates:**
- `master.m3u8` - Master playlist (entry point)
- `stream_0/playlist.m3u8` - 360p stream
- `stream_1/playlist.m3u8` - 480p stream
- `stream_2/playlist.m3u8` - 720p stream
### HLS with Encryption
Protect content with AES-128 encryption.
```bash
# Generate encryption key
openssl rand 16 > enc.key
echo "enc.key" > enc.keyinfo
echo "enc.key" >> enc.keyinfo
openssl rand -hex 16 >> enc.keyinfo
# Encode with encryption
ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_time 6 \
-hls_key_info_file enc.keyinfo \
-hls_segment_filename "segment_%03d.ts" \
playlist.m3u8
```
## DASH (Dynamic Adaptive Streaming)
### Basic DASH
MPEG-DASH format for adaptive streaming.
```bash
ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-f dash -seg_duration 6 \
-use_template 1 -use_timeline 1 \
manifest.mpd
```
### Multi-Bitrate DASH
Multiple quality levels.
```bash
ffmpeg -i input.mp4 \
-map 0:v -map 0:a -map 0:v -map 0:a \
-c:v libx264 -c:a aac \
-b:v:0 800k -s:v:0 640x360 \
-b:v:1 1400k -s:v:1 1280x720 \
-b:a:0 128k -b:a:1 128k \
-f dash -seg_duration 6 \
-use_template 1 -use_timeline 1 \
manifest.mpd
```
## RTMP Live Streaming
### Stream to Twitch
```bash
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset veryfast -maxrate 3000k -bufsize 6000k \
-pix_fmt yuv420p -g 50 -c:a aac -b:a 128k -ar 44100 \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
### Stream to YouTube
```bash
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset veryfast -maxrate 2500k -bufsize 5000k \
-pix_fmt yuv420p -g 60 -c:a aac -b:a 128k \
-f flv rtmp://a.rtmp.youtube.com/live2/STREAM_KEY
```
### Stream to Facebook
```bash
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset veryfast -maxrate 4000k -bufsize 8000k \
-pix_fmt yuv420p -g 60 -c:a aac -b:a 128k \
-f flv rtmps://live-api-s.facebook.com:443/rtmp/STREAM_KEY
```
### Custom RTMP Server
```bash
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset veryfast -tune zerolatency \
-maxrate 2500k -bufsize 5000k \
-pix_fmt yuv420p -g 50 \
-c:a aac -b:a 128k -ar 44100 \
-f flv rtmp://your-server.com/live/stream-key
```
## Screen Capture + Stream
### Linux (X11)
```bash
ffmpeg -f x11grab -s 1920x1080 -framerate 30 -i :0.0 \
-f pulse -ac 2 -i default \
-c:v libx264 -preset veryfast -tune zerolatency \
-maxrate 2500k -bufsize 5000k -pix_fmt yuv420p \
-c:a aac -b:a 128k -ar 44100 \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
### macOS (AVFoundation)
```bash
# List devices
ffmpeg -f avfoundation -list_devices true -i ""
# Capture and stream
ffmpeg -f avfoundation -framerate 30 -i "1:0" \
-c:v libx264 -preset veryfast -tune zerolatency \
-maxrate 2500k -bufsize 5000k -pix_fmt yuv420p \
-c:a aac -b:a 128k \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
### Windows (DirectShow)
```bash
ffmpeg -f dshow -i video="screen-capture-recorder":audio="Stereo Mix" \
-c:v libx264 -preset ultrafast -tune zerolatency \
-maxrate 750k -bufsize 3000k \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
## Thumbnail Generation
### Single Thumbnail
Extract frame at specific time.
```bash
# At 5 seconds
ffmpeg -ss 00:00:05 -i input.mp4 -vframes 1 -vf scale=320:-1 thumb.jpg
# At 10% duration
ffmpeg -ss $(ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 input.mp4 | \
awk '{print $1*0.1}') -i input.mp4 -vframes 1 thumb.jpg
```
### Multiple Thumbnails
Generate thumbnails at intervals.
```bash
# One per minute
ffmpeg -i input.mp4 -vf fps=1/60,scale=320:-1 thumb_%03d.jpg
# One per 10 seconds
ffmpeg -i input.mp4 -vf fps=1/10,scale=320:-1 thumb_%03d.jpg
# First 10 frames
ffmpeg -i input.mp4 -vframes 10 -vf scale=320:-1 thumb_%02d.jpg
```
### Thumbnail Sprite Sheet
Create single image with multiple thumbnails.
```bash
# Generate frames
ffmpeg -i input.mp4 -vf fps=1/10,scale=160:90 frames/thumb_%03d.jpg
# Combine into sprite (requires ImageMagick)
montage frames/thumb_*.jpg -tile 5x -geometry +0+0 sprite.jpg
```
## Preview Generation
### Video Preview (Trailer)
Extract multiple short clips.
```bash
# Extract 3 segments
ffmpeg -i input.mp4 \
-ss 00:00:30 -t 00:00:10 -c copy segment1.mp4
ffmpeg -i input.mp4 \
-ss 00:05:00 -t 00:00:10 -c copy segment2.mp4
ffmpeg -i input.mp4 \
-ss 00:10:00 -t 00:00:10 -c copy segment3.mp4
# Concatenate segments
echo "file 'segment1.mp4'" > concat.txt
echo "file 'segment2.mp4'" >> concat.txt
echo "file 'segment3.mp4'" >> concat.txt
ffmpeg -f concat -safe 0 -i concat.txt -c copy preview.mp4
```
### Fast Preview (Low Quality)
Quick preview for review.
```bash
ffmpeg -i input.mp4 \
-vf scale=640:-1 \
-c:v libx264 -preset ultrafast -crf 28 \
-c:a aac -b:a 64k \
preview.mp4
```
## Streaming Parameters
### Important RTMP Parameters
**Real-time reading:**
- `-re` - Read input at native frame rate
**Low latency:**
- `-tune zerolatency` - Optimize for minimal latency
- `-preset ultrafast` or `veryfast` - Fast encoding
**Keyframes:**
- `-g 50` - Keyframe interval (GOP size)
- Recommended: 2 seconds (fps * 2)
**Rate control:**
- `-maxrate` - Maximum bitrate (e.g., 3000k)
- `-bufsize` - Buffer size (typically 2x maxrate)
**Compatibility:**
- `-pix_fmt yuv420p` - Compatible pixel format
### Bitrate Recommendations
**1080p 60fps:**
- 4500-6000 kbps video
- 160 kbps audio
**1080p 30fps:**
- 3000-4500 kbps video
- 128 kbps audio
**720p 60fps:**
- 2500-4000 kbps video
- 128 kbps audio
**720p 30fps:**
- 1500-2500 kbps video
- 128 kbps audio
**480p:**
- 500-1000 kbps video
- 128 kbps audio
## UDP/RTP Streaming
### UDP Stream
Simple network streaming.
```bash
# Sender
ffmpeg -re -i input.mp4 -c copy -f mpegts udp://192.168.1.100:1234
# Receiver
ffplay udp://192.168.1.100:1234
```
### RTP Stream
Real-Time Protocol for low latency.
```bash
# Audio only
ffmpeg -re -i audio.mp3 -c:a libopus -f rtp rtp://192.168.1.100:5004
# Video + audio
ffmpeg -re -i input.mp4 \
-c:v libx264 -preset ultrafast \
-c:a aac -f rtp rtp://192.168.1.100:5004
```
### Multicast Stream
Stream to multiple receivers.
```bash
# Sender (multicast address)
ffmpeg -re -i input.mp4 -c copy -f mpegts udp://239.255.0.1:1234
# Receiver
ffplay udp://239.255.0.1:1234
```
## Advanced Streaming
### Hardware-Accelerated Streaming
Use GPU for faster encoding.
```bash
# NVIDIA NVENC
ffmpeg -re -i input.mp4 \
-c:v h264_nvenc -preset fast -maxrate 3000k -bufsize 6000k \
-c:a aac -b:a 128k \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
# Intel QSV
ffmpeg -re -hwaccel qsv -i input.mp4 \
-c:v h264_qsv -preset fast -maxrate 3000k -bufsize 6000k \
-c:a aac -b:a 128k \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
### Stream with Overlay
Add graphics during stream.
```bash
ffmpeg -re -i input.mp4 -i logo.png \
-filter_complex "[0:v][1:v]overlay=10:10" \
-c:v libx264 -preset veryfast -maxrate 3000k \
-c:a copy \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
### Loop Stream
Continuously loop video for 24/7 stream.
```bash
ffmpeg -stream_loop -1 -re -i input.mp4 \
-c:v libx264 -preset veryfast -maxrate 2500k \
-c:a aac -b:a 128k \
-f flv rtmp://live.twitch.tv/app/STREAM_KEY
```
## Troubleshooting
### Buffering Issues
```bash
# Reduce buffer size
ffmpeg -re -i input.mp4 -maxrate 2000k -bufsize 2000k -c:v libx264 -f flv rtmp://...
# Use faster preset
ffmpeg -re -i input.mp4 -preset ultrafast -c:v libx264 -f flv rtmp://...
```
### Audio/Video Desync
```bash
# Force constant frame rate
ffmpeg -re -i input.mp4 -r 30 -c:v libx264 -f flv rtmp://...
# Use -vsync 1
ffmpeg -re -i input.mp4 -vsync 1 -c:v libx264 -f flv rtmp://...
```
### Connection Drops
```bash
# Increase timeout
ffmpeg -timeout 5000000 -re -i input.mp4 -c:v libx264 -f flv rtmp://...
# Reconnect on failure (use wrapper script)
while true; do
ffmpeg -re -i input.mp4 -c:v libx264 -f flv rtmp://...
sleep 5
done
```

View File

@@ -0,0 +1,375 @@
# Format Compatibility & Conversion Guide
Complete guide to media format support, codec recommendations, and conversion best practices.
## Image Format Support
### ImageMagick Formats
**Raster Formats (Full Support):**
- JPEG (.jpg, .jpeg) - Lossy, universal
- PNG (.png) - Lossless, transparency
- WebP (.webp) - Modern, lossy/lossless
- GIF (.gif) - Animation, limited colors
- TIFF (.tif, .tiff) - Professional, lossless
- BMP (.bmp) - Uncompressed, legacy
- ICO (.ico) - Icons, multi-size
**Raw Formats (Read Support):**
- CR2, NEF, ARW, DNG (Canon, Nikon, Sony, Adobe RAW)
- Requires dcraw or ufraw-batch
**Vector Formats (Limited):**
- SVG (.svg) - Read only, converts to raster
- PDF (.pdf) - Read/write, may have policy restrictions
**Other Formats:**
- HEIC (.heic) - Apple format, requires libheif
- AVIF (.avif) - Next-gen, requires libavif
- PSD (.psd) - Photoshop, basic support
### FFmpeg Image Support
**Input Formats:**
- JPEG, PNG, BMP, TIFF, WebP, GIF
- Image sequences (frame_%04d.png)
**Output Formats:**
- JPEG, PNG, BMP, TIFF
- Video from images
## Video Format Support
### Container Formats
**Universal Containers:**
- MP4 (.mp4) - Most compatible, streaming
- MKV (.mkv) - Feature-rich, flexible
- WebM (.webm) - Web-optimized, open
- AVI (.avi) - Legacy, broad support
- MOV (.mov) - Apple, professional
**Streaming Containers:**
- TS (.ts) - Transport stream, HLS segments
- M3U8 (.m3u8) - HLS playlist
- MPD (.mpd) - DASH manifest
- FLV (.flv) - Flash (legacy)
**Professional Formats:**
- ProRes (.mov) - Apple professional
- DNxHD/DNxHR (.mxf, .mov) - Avid professional
- MXF (.mxf) - Broadcast
### Video Codecs
**Modern Codecs:**
- H.264/AVC (libx264) - Universal, excellent balance
- H.265/HEVC (libx265) - Better compression, 4K
- VP9 (libvpx-vp9) - Open, YouTube
- AV1 (libaom-av1, libsvtav1) - Next-gen, best compression
**Legacy Codecs:**
- MPEG-4 (mpeg4) - Older devices
- MPEG-2 (mpeg2video) - DVD, broadcast
- VP8 (libvpx) - WebM predecessor
**Professional Codecs:**
- ProRes (prores) - Apple post-production
- DNxHD (dnxhd) - Avid editing
- Uncompressed (rawvideo) - Maximum quality
### Audio Codecs
**Modern Codecs:**
- AAC (aac) - Universal, streaming
- Opus (libopus) - Best low-bitrate
- MP3 (libmp3lame) - Universal compatibility
**Lossless Codecs:**
- FLAC (flac) - Open, archival
- ALAC (alac) - Apple lossless
- WAV (pcm_s16le) - Uncompressed
**Other Codecs:**
- Vorbis (libvorbis) - Open, WebM
- AC-3 (ac3) - Dolby Digital, surround
- DTS (dts) - Cinema surround
## Format Recommendations
### Use Case Matrix
| Use Case | Image Format | Video Container | Video Codec | Audio Codec |
|----------|--------------|-----------------|-------------|-------------|
| Web general | JPEG 85% | MP4 | H.264 | AAC 128k |
| Web transparency | PNG | - | - | - |
| Web modern | WebP | WebM | VP9 | Opus |
| Social media | JPEG 85% | MP4 | H.264 | AAC 128k |
| 4K streaming | - | MP4 | H.265 | AAC 192k |
| Archive | PNG/TIFF | MKV | H.265 CRF 18 | FLAC |
| Email | JPEG 75% | - | - | - |
| Print | TIFF/PNG | - | - | - |
| YouTube | - | MP4/WebM | H.264/VP9 | AAC/Opus |
| Live stream | - | FLV | H.264 | AAC |
| Editing | - | MOV/MXF | ProRes/DNxHD | PCM |
### Platform Compatibility
**Web Browsers (2025):**
- Images: JPEG, PNG, WebP, GIF, SVG
- Video: MP4 (H.264), WebM (VP9), MP4 (AV1)
- Audio: AAC, MP3, Opus, Vorbis
**Mobile Devices:**
- iOS: JPEG, PNG, HEIC, MP4 (H.264/H.265), AAC
- Android: JPEG, PNG, WebP, MP4 (H.264/H.265), AAC
**Smart TVs:**
- Most: MP4 (H.264), AAC
- Modern: MP4 (H.265), AC-3
**Social Media:**
- All platforms: JPEG, MP4 (H.264), AAC
## Quality vs Size Trade-offs
### Image Quality Comparison
**JPEG Quality Levels:**
- 95-100: ~5-10 MB (large image), minimal artifacts
- 85-94: ~1-3 MB, imperceptible loss
- 75-84: ~500 KB-1 MB, slight artifacts
- 60-74: ~200-500 KB, visible artifacts
- Below 60: <200 KB, poor quality
**Format Comparison (Same quality):**
- WebP: 25-35% smaller than JPEG
- HEIC: 40-50% smaller than JPEG
- AVIF: 50-60% smaller than JPEG
- PNG: 2-5x larger than JPEG (lossless)
### Video Quality Comparison
**H.264 CRF Values:**
- CRF 18: Visually lossless, ~8-15 Mbps (1080p)
- CRF 23: High quality, ~4-8 Mbps (1080p)
- CRF 28: Medium quality, ~2-4 Mbps (1080p)
**Codec Comparison (Same quality):**
- H.265: 40-50% smaller than H.264
- VP9: 30-40% smaller than H.264
- AV1: 50-60% smaller than H.264
### Audio Quality Comparison
**AAC Bitrates:**
- 320 kbps: Transparent, archival
- 192 kbps: High quality, music
- 128 kbps: Good quality, streaming
- 96 kbps: Acceptable, low bandwidth
- 64 kbps: Poor, voice only
**Codec Efficiency (Same quality):**
- Opus: Best at low bitrates (<128k)
- AAC: Best overall balance
- MP3: Less efficient but universal
## Conversion Best Practices
### Image Conversions
**PNG to JPEG:**
```bash
# Standard conversion
magick input.png -quality 85 -strip output.jpg
# With transparency handling
magick input.png -background white -flatten -quality 85 output.jpg
```
**JPEG to WebP:**
```bash
# FFmpeg
ffmpeg -i input.jpg -quality 80 output.webp
# ImageMagick
magick input.jpg -quality 80 output.webp
```
**RAW to JPEG:**
```bash
# Requires dcraw
magick input.CR2 -quality 90 output.jpg
```
**HEIC to JPEG:**
```bash
# Requires libheif
magick input.heic -quality 85 output.jpg
```
### Video Conversions
**MKV to MP4:**
```bash
# Copy streams (fast)
ffmpeg -i input.mkv -c copy output.mp4
# Re-encode if needed
ffmpeg -i input.mkv -c:v libx264 -crf 23 -c:a aac output.mp4
```
**AVI to MP4:**
```bash
# Modern codecs
ffmpeg -i input.avi -c:v libx264 -crf 23 -c:a aac output.mp4
```
**MOV to MP4:**
```bash
# Copy if H.264 already
ffmpeg -i input.mov -c copy output.mp4
# Convert ProRes to H.264
ffmpeg -i input.mov -c:v libx264 -crf 18 -c:a aac output.mp4
```
**Any to WebM:**
```bash
# VP9 encoding
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus output.webm
```
### Audio Conversions
**Extract Audio from Video:**
```bash
# Keep original codec
ffmpeg -i video.mp4 -vn -c:a copy audio.m4a
# Convert to MP3
ffmpeg -i video.mp4 -vn -q:a 0 audio.mp3
# Convert to FLAC (lossless)
ffmpeg -i video.mp4 -vn -c:a flac audio.flac
```
**Audio Format Conversion:**
```bash
# WAV to MP3
ffmpeg -i input.wav -c:a libmp3lame -b:a 192k output.mp3
# MP3 to AAC
ffmpeg -i input.mp3 -c:a aac -b:a 192k output.m4a
# Any to Opus
ffmpeg -i input.wav -c:a libopus -b:a 128k output.opus
```
## Codec Selection Guide
### Choose H.264 When:
- Maximum compatibility needed
- Targeting older devices
- Streaming to unknown devices
- Social media upload
- Fast encoding required
### Choose H.265 When:
- 4K video encoding
- Storage space limited
- Modern device targets
- Archival quality needed
- Bandwidth constrained
### Choose VP9 When:
- YouTube upload
- Open-source requirement
- Chrome/Firefox primary
- Royalty-free needed
### Choose AV1 When:
- Future-proofing content
- Maximum compression needed
- Encoding time not critical
- Modern platform targets
## Format Migration Strategies
### Archive to Web
```bash
# High-res archive -> Web-optimized
for img in archive/*.tif; do
base=$(basename "$img" .tif)
magick "$img" -resize 2000x2000\> -quality 85 -strip "web/${base}.jpg"
magick "$img" -resize 2000x2000\> -quality 85 "web/${base}.webp"
done
```
### Legacy to Modern
```bash
# Convert old formats to modern codecs
for video in legacy/*.avi; do
base=$(basename "$video" .avi)
ffmpeg -i "$video" \
-c:v libx264 -crf 23 -preset slow \
-c:a aac -b:a 128k \
"modern/${base}.mp4"
done
```
### Multi-Format Publishing
```bash
# Create multiple formats for compatibility
input="source.mp4"
# Modern browsers
ffmpeg -i "$input" -c:v libx264 -crf 23 -c:a aac output.mp4
ffmpeg -i "$input" -c:v libvpx-vp9 -crf 30 -c:a libopus output.webm
# Images
ffmpeg -ss 5 -i "$input" -vframes 1 poster.jpg
magick poster.jpg -quality 80 poster.webp
```
## Troubleshooting
### Unsupported Format
```bash
# Check FFmpeg formats
ffmpeg -formats
# Check ImageMagick formats
magick identify -list format
# Install missing codec support
sudo apt-get install libx264-dev libx265-dev libvpx-dev
```
### Compatibility Issues
```bash
# Force compatible encoding
ffmpeg -i input.mp4 \
-c:v libx264 -profile:v high -level 4.0 \
-pix_fmt yuv420p \
-c:a aac -b:a 128k \
output.mp4
```
### Quality Loss
```bash
# Avoid multiple conversions
# Bad: source -> edit -> web -> social
# Good: source -> final (single conversion)
# Use lossless intermediate
ffmpeg -i source.mp4 -c:v ffv1 intermediate.mkv
# Edit intermediate
ffmpeg -i intermediate.mkv -c:v libx264 final.mp4
```

View File

@@ -0,0 +1,612 @@
# ImageMagick Batch Processing
Complete guide to batch operations, mogrify command, parallel processing, and automation.
## Mogrify Command
### Basic Mogrify
Modify files in-place (overwrites originals).
```bash
# Resize all JPEGs
mogrify -resize 800x600 *.jpg
# Convert format (creates new files)
mogrify -format png *.jpg
# Apply effect to all images
mogrify -quality 85 -strip *.jpg
```
**Warning:** mogrify modifies files in-place. Always backup originals or use `-path` to output to different directory.
### Output to Different Directory
Preserve originals.
```bash
# Create output directory first
mkdir output
# Process to output directory
mogrify -path ./output -resize 800x600 *.jpg
# With format conversion
mogrify -path ./optimized -format webp -quality 80 *.png
```
## Common Batch Operations
### Resize All Images
```bash
# Resize to width 800
mogrify -resize 800x *.jpg
# Resize to height 600
mogrify -resize x600 *.jpg
# Fit within 800×600
mogrify -resize 800x600 *.jpg
# Resize to exact dimensions
mogrify -resize 800x600! *.jpg
# Only shrink, never enlarge
mogrify -resize 800x600\> *.jpg
```
### Format Conversion
```bash
# PNG to JPEG
mogrify -path ./jpg -format jpg -quality 85 *.png
# JPEG to WebP
mogrify -path ./webp -format webp -quality 80 *.jpg
# Any format to PNG
mogrify -path ./png -format png *.{jpg,gif,bmp}
```
### Optimize Images
```bash
# Strip metadata from all JPEGs
mogrify -strip *.jpg
# Optimize JPEGs for web
mogrify -quality 85 -strip -interlace Plane *.jpg
# Compress PNGs
mogrify -quality 95 *.png
# Combined optimization
mogrify -quality 85 -strip -interlace Plane -sampling-factor 4:2:0 *.jpg
```
### Apply Effects
```bash
# Add watermark to all images
mogrify -gravity southeast -draw "image over 10,10 0,0 'watermark.png'" *.jpg
# Convert all to grayscale
mogrify -colorspace Gray *.jpg
# Apply sepia tone
mogrify -sepia-tone 80% *.jpg
# Sharpen all images
mogrify -sharpen 0x1 *.jpg
```
### Thumbnail Generation
```bash
# Create square thumbnails
mogrify -path ./thumbnails -resize 200x200^ -gravity center -extent 200x200 *.jpg
# Create thumbnails with max dimension
mogrify -path ./thumbs -thumbnail 300x300 *.jpg
# Thumbnails with quality control
mogrify -path ./thumbs -thumbnail 200x200 -quality 80 -strip *.jpg
```
## Shell Loops
### Basic For Loop
More control than mogrify.
```bash
# Resize with custom naming
for img in *.jpg; do
magick "$img" -resize 800x600 "resized_$img"
done
# Process to subdirectory
mkdir processed
for img in *.jpg; do
magick "$img" -resize 1920x1080 "processed/$img"
done
```
### Multiple Operations
```bash
# Complex processing pipeline
for img in *.jpg; do
magick "$img" \
-resize 1920x1080^ \
-gravity center \
-crop 1920x1080+0+0 +repage \
-unsharp 0x1 \
-quality 85 -strip \
"processed_$img"
done
```
### Format Conversion with Rename
```bash
# Convert PNG to JPEG with new names
for img in *.png; do
magick "$img" -quality 90 "${img%.png}.jpg"
done
# Add prefix during conversion
for img in *.jpg; do
magick "$img" -resize 800x "web_${img}"
done
```
### Conditional Processing
```bash
# Only process large images
for img in *.jpg; do
width=$(identify -format "%w" "$img")
if [ $width -gt 2000 ]; then
magick "$img" -resize 2000x "resized_$img"
fi
done
# Skip existing output files
for img in *.jpg; do
output="output_$img"
if [ ! -f "$output" ]; then
magick "$img" -resize 800x "$output"
fi
done
```
## Parallel Processing
### GNU Parallel
Process multiple images simultaneously.
```bash
# Install GNU Parallel
# Ubuntu/Debian: sudo apt-get install parallel
# macOS: brew install parallel
# Basic parallel resize
parallel magick {} -resize 800x600 resized_{} ::: *.jpg
# Parallel with function
resize_image() {
magick "$1" -resize 1920x1080 -quality 85 "processed_$1"
}
export -f resize_image
parallel resize_image ::: *.jpg
# Limit concurrent jobs
parallel -j 4 magick {} -resize 800x {} ::: *.jpg
# Progress indicator
parallel --progress magick {} -resize 800x {} ::: *.jpg
```
### Xargs Parallel
```bash
# Using xargs for parallel processing
ls *.jpg | xargs -I {} -P 4 magick {} -resize 800x processed_{}
# With find
find . -name "*.jpg" -print0 | \
xargs -0 -I {} -P 4 magick {} -resize 800x {}
```
## Advanced Batch Patterns
### Recursive Processing
```bash
# Process all JPEGs in subdirectories
find . -name "*.jpg" -exec magick {} -resize 800x {} \;
# With output directory structure
find . -name "*.jpg" -type f | while read img; do
outdir="output/$(dirname "$img")"
mkdir -p "$outdir"
magick "$img" -resize 800x "$outdir/$(basename "$img")"
done
```
### Batch with Different Sizes
```bash
# Generate multiple sizes
for size in 320 640 1024 1920; do
mkdir -p "output/${size}w"
for img in *.jpg; do
magick "$img" -resize ${size}x -quality 85 "output/${size}w/$img"
done
done
# Parallel version
for size in 320 640 1024 1920; do
mkdir -p "output/${size}w"
parallel magick {} -resize ${size}x -quality 85 "output/${size}w/{}" ::: *.jpg
done
```
### Responsive Image Set
```bash
# Create responsive image set with srcset
mkdir -p responsive
for img in *.jpg; do
base="${img%.jpg}"
for width in 320 640 1024 1920; do
magick "$img" -resize ${width}x -quality 85 \
"responsive/${base}-${width}w.jpg"
done
done
```
### Watermark Batch
```bash
# Add watermark to all images
for img in *.jpg; do
magick "$img" watermark.png \
-gravity southeast -geometry +10+10 \
-composite "watermarked_$img"
done
# Different watermark positions for portrait vs landscape
for img in *.jpg; do
width=$(identify -format "%w" "$img")
height=$(identify -format "%h" "$img")
if [ $width -gt $height ]; then
# Landscape
magick "$img" watermark.png -gravity southeast -composite "marked_$img"
else
# Portrait
magick "$img" watermark.png -gravity south -composite "marked_$img"
fi
done
```
## Error Handling
### Check Before Processing
```bash
# Verify image before processing
for img in *.jpg; do
if identify "$img" > /dev/null 2>&1; then
magick "$img" -resize 800x "processed_$img"
else
echo "Skipping corrupt image: $img"
fi
done
```
### Log Processing
```bash
# Log successful and failed operations
log_file="batch_process.log"
error_log="errors.log"
for img in *.jpg; do
if magick "$img" -resize 800x "output/$img" 2>> "$error_log"; then
echo "$(date): Processed $img" >> "$log_file"
else
echo "$(date): Failed $img" >> "$error_log"
fi
done
```
### Dry Run Mode
```bash
# Test without modifying files
dry_run=true
for img in *.jpg; do
cmd="magick $img -resize 800x processed_$img"
if [ "$dry_run" = true ]; then
echo "Would run: $cmd"
else
eval $cmd
fi
done
```
## Optimization Workflows
### Web Publishing Pipeline
```bash
# Complete web optimization workflow
mkdir -p web/{original,optimized,thumbnails}
# Copy originals
cp *.jpg web/original/
# Create optimized versions
mogrify -path web/optimized \
-resize 1920x1080\> \
-quality 85 \
-strip \
-interlace Plane \
web/original/*.jpg
# Create thumbnails
mogrify -path web/thumbnails \
-thumbnail 300x300 \
-quality 80 \
-strip \
web/original/*.jpg
```
### Archive to Web Conversion
```bash
# Convert high-res archives to web formats
for img in archives/*.jpg; do
base=$(basename "$img" .jpg)
# Full size web version
magick "$img" -resize 2048x2048\> -quality 90 -strip "web/${base}.jpg"
# Thumbnail
magick "$img" -thumbnail 400x400 -quality 85 "web/${base}_thumb.jpg"
# WebP version
magick "$img" -resize 2048x2048\> -quality 85 "web/${base}.webp"
done
```
### Print to Web Workflow
```bash
# Convert print-ready images to web
for img in print/*.tif; do
base=$(basename "$img" .tif)
# Convert colorspace and optimize
magick "$img" \
-colorspace sRGB \
-resize 2000x2000\> \
-quality 90 \
-strip \
-interlace Plane \
"web/${base}.jpg"
done
```
## Batch Reporting
### Generate Report
```bash
# Create processing report
report="batch_report.txt"
echo "Batch Processing Report - $(date)" > "$report"
echo "================================" >> "$report"
total=0
success=0
failed=0
for img in *.jpg; do
((total++))
if magick "$img" -resize 800x "output/$img" 2>/dev/null; then
((success++))
echo "$img" >> "$report"
else
((failed++))
echo "$img" >> "$report"
fi
done
echo "" >> "$report"
echo "Total: $total, Success: $success, Failed: $failed" >> "$report"
```
### Image Inventory
```bash
# Create inventory of images
inventory="image_inventory.csv"
echo "Filename,Width,Height,Format,Size,ColorSpace" > "$inventory"
for img in *.{jpg,png,gif}; do
[ -f "$img" ] || continue
info=$(identify -format "%f,%w,%h,%m,%b,%[colorspace]" "$img")
echo "$info" >> "$inventory"
done
```
## Performance Tips
### Optimize Loop Performance
```bash
# Bad: Launch mogrify for each file
for img in *.jpg; do
mogrify -resize 800x "$img"
done
# Good: Process all files in one mogrify call
mogrify -resize 800x *.jpg
# Best: Use parallel processing for complex operations
parallel magick {} -resize 800x -quality 85 processed_{} ::: *.jpg
```
### Memory Management
```bash
# Limit memory for batch processing
for img in *.jpg; do
magick -limit memory 2GB -limit map 4GB \
"$img" -resize 50% "output/$img"
done
```
### Progress Tracking
```bash
# Show progress for long batch operations
total=$(ls *.jpg | wc -l)
current=0
for img in *.jpg; do
((current++))
echo "Processing $current/$total: $img"
magick "$img" -resize 800x "output/$img"
done
```
## Automation Scripts
### Complete Bash Script
```bash
#!/bin/bash
# Configuration
INPUT_DIR="./input"
OUTPUT_DIR="./output"
QUALITY=85
MAX_WIDTH=1920
THUMBNAIL_SIZE=300
# Create output directories
mkdir -p "$OUTPUT_DIR"/{full,thumbnails}
# Process images
echo "Processing images..."
for img in "$INPUT_DIR"/*.{jpg,jpeg,png}; do
[ -f "$img" ] || continue
filename=$(basename "$img")
base="${filename%.*}"
# Full size
magick "$img" \
-resize ${MAX_WIDTH}x\> \
-quality $QUALITY \
-strip \
"$OUTPUT_DIR/full/${base}.jpg"
# Thumbnail
magick "$img" \
-thumbnail ${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE} \
-quality 80 \
-strip \
"$OUTPUT_DIR/thumbnails/${base}_thumb.jpg"
echo "$filename"
done
echo "Done!"
```
### Python Batch Script
```python
#!/usr/bin/env python3
import os
import subprocess
from pathlib import Path
INPUT_DIR = Path("./input")
OUTPUT_DIR = Path("./output")
SIZES = [320, 640, 1024, 1920]
# Create output directories
for size in SIZES:
(OUTPUT_DIR / f"{size}w").mkdir(parents=True, exist_ok=True)
# Process images
for img in INPUT_DIR.glob("*.jpg"):
for size in SIZES:
output = OUTPUT_DIR / f"{size}w" / img.name
subprocess.run([
"magick", str(img),
"-resize", f"{size}x",
"-quality", "85",
"-strip",
str(output)
])
print(f"{img.name} -> {size}w")
```
## Common Batch Recipes
### Social Media Sizes
```bash
# Generate social media image sizes
for img in *.jpg; do
base="${img%.jpg}"
# Instagram square (1080×1080)
magick "$img" -resize 1080x1080^ -gravity center -extent 1080x1080 "${base}_ig_square.jpg"
# Instagram portrait (1080×1350)
magick "$img" -resize 1080x1350^ -gravity center -extent 1080x1350 "${base}_ig_portrait.jpg"
# Facebook post (1200×630)
magick "$img" -resize 1200x630^ -gravity center -extent 1200x630 "${base}_fb_post.jpg"
# Twitter post (1200×675)
magick "$img" -resize 1200x675^ -gravity center -extent 1200x675 "${base}_tw_post.jpg"
done
```
### Email Newsletter Images
```bash
# Optimize images for email
mogrify -path ./email \
-resize 600x\> \
-quality 75 \
-strip \
-interlace Plane \
*.jpg
```
### Backup and Archive
```bash
# Create web versions and keep originals
mkdir -p {originals,web}
# Move originals
mv *.jpg originals/
# Create optimized copies
for img in originals/*.jpg; do
base=$(basename "$img")
magick "$img" -resize 2000x2000\> -quality 85 -strip "web/$base"
done
```

View File

@@ -0,0 +1,623 @@
# ImageMagick Image Editing
Complete guide to format conversion, resizing, effects, transformations, and composition.
## Format Conversion
### Basic Conversion
Convert between image formats.
```bash
# PNG to JPEG
magick input.png output.jpg
# JPEG to WebP
magick input.jpg output.webp
# Multiple outputs simultaneously
magick input.png output.jpg output.webp output.gif
# Convert with quality setting
magick input.png -quality 85 output.jpg
```
### Quality Settings
**JPEG Quality (0-100):**
- 95-100: Archival, minimal compression
- 85-94: High quality, web publishing
- 75-84: Medium quality, web optimized
- 60-74: Lower quality, smaller files
- Below 60: Visible artifacts
```bash
# High quality
magick input.png -quality 95 output.jpg
# Web optimized (recommended)
magick input.png -quality 85 -strip output.jpg
# Smaller file size
magick input.png -quality 75 -sampling-factor 4:2:0 -strip output.jpg
```
**PNG Quality (0-9 = compression level):**
```bash
# Maximum compression (slower)
magick input.jpg -quality 95 output.png
# Faster compression
magick input.jpg -quality 75 output.png
```
**WebP Quality:**
```bash
# Lossy with quality
magick input.jpg -quality 80 output.webp
# Lossless
magick input.png -define webp:lossless=true output.webp
```
### Progressive & Optimization
```bash
# Progressive JPEG (better web loading)
magick input.png -quality 85 -interlace Plane output.jpg
# Strip metadata (reduce file size)
magick input.jpg -strip output.jpg
# Combined optimization
magick input.png -quality 85 -interlace Plane -strip output.jpg
```
## Resizing Operations
### Basic Resize
Maintain aspect ratio.
```bash
# Fit within 800×600
magick input.jpg -resize 800x600 output.jpg
# Resize to specific width (auto height)
magick input.jpg -resize 800x output.jpg
# Resize to specific height (auto width)
magick input.jpg -resize x600 output.jpg
# Scale by percentage
magick input.jpg -resize 50% output.jpg
```
### Advanced Resize
```bash
# Resize only if larger (shrink only)
magick input.jpg -resize 800x600\> output.jpg
# Resize only if smaller (enlarge only)
magick input.jpg -resize 800x600\< output.jpg
# Force exact dimensions (ignore aspect ratio)
magick input.jpg -resize 800x600! output.jpg
# Fill dimensions (may crop)
magick input.jpg -resize 800x600^ output.jpg
# Minimum dimensions
magick input.jpg -resize 800x600^ output.jpg
```
### Resize Algorithms
```bash
# High quality (Lanczos)
magick input.jpg -filter Lanczos -resize 50% output.jpg
# Fast resize (Box)
magick input.jpg -filter Box -resize 50% output.jpg
# Mitchel filter (good balance)
magick input.jpg -filter Mitchell -resize 50% output.jpg
```
**Filter comparison:**
- `Lanczos` - Highest quality, slower
- `Mitchell` - Good quality, fast
- `Catrom` - Sharp, good for downscaling
- `Box` - Fastest, acceptable quality
- `Cubic` - Smooth results
## Cropping
### Basic Crop
Extract region from image.
```bash
# Crop width×height+x+y
magick input.jpg -crop 400x400+100+100 output.jpg
# Remove virtual canvas after crop
magick input.jpg -crop 400x400+100+100 +repage output.jpg
# Crop from center
magick input.jpg -gravity center -crop 400x400+0+0 output.jpg
# Crop to aspect ratio
magick input.jpg -gravity center -crop 16:9 +repage output.jpg
```
### Smart Crop
Content-aware cropping.
```bash
# Trim transparent/same-color borders
magick input.png -trim +repage output.png
# Trim with fuzz tolerance
magick input.jpg -fuzz 10% -trim +repage output.jpg
```
### Thumbnail Generation
Create square thumbnails from any aspect ratio.
```bash
# Resize and crop to square
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 thumb.jpg
# Alternative method
magick input.jpg -thumbnail 200x200^ -gravity center -crop 200x200+0+0 +repage thumb.jpg
# With background (no crop)
magick input.jpg -resize 200x200 -background white -gravity center -extent 200x200 thumb.jpg
```
## Effects & Filters
### Blur Effects
```bash
# Standard blur (radius 0 = auto)
magick input.jpg -blur 0x8 output.jpg
# Gaussian blur (radius×sigma)
magick input.jpg -gaussian-blur 5x3 output.jpg
# Motion blur (angle)
magick input.jpg -motion-blur 0x20+45 output.jpg
# Radial blur
magick input.jpg -radial-blur 10 output.jpg
```
### Sharpen
```bash
# Basic sharpen
magick input.jpg -sharpen 0x1 output.jpg
# Stronger sharpen
magick input.jpg -sharpen 0x3 output.jpg
# Unsharp mask (advanced)
magick input.jpg -unsharp 0x1 output.jpg
```
### Color Effects
```bash
# Grayscale
magick input.jpg -colorspace Gray output.jpg
# Sepia tone
magick input.jpg -sepia-tone 80% output.jpg
# Negate (invert colors)
magick input.jpg -negate output.jpg
# Posterize (reduce colors)
magick input.jpg -posterize 8 output.jpg
# Solarize
magick input.jpg -solarize 50% output.jpg
```
### Artistic Effects
```bash
# Edge detection
magick input.jpg -edge 3 output.jpg
# Emboss
magick input.jpg -emboss 2 output.jpg
# Oil painting
magick input.jpg -paint 4 output.jpg
# Charcoal drawing
magick input.jpg -charcoal 2 output.jpg
# Sketch
magick input.jpg -sketch 0x20+120 output.jpg
# Swirl
magick input.jpg -swirl 90 output.jpg
```
## Adjustments
### Brightness & Contrast
```bash
# Increase brightness
magick input.jpg -brightness-contrast 10x0 output.jpg
# Increase contrast
magick input.jpg -brightness-contrast 0x20 output.jpg
# Both
magick input.jpg -brightness-contrast 10x20 output.jpg
# Negative values to decrease
magick input.jpg -brightness-contrast -10x-10 output.jpg
```
### Color Adjustments
```bash
# Adjust saturation (HSL modulation)
# Format: brightness,saturation,hue
magick input.jpg -modulate 100,150,100 output.jpg
# Adjust hue
magick input.jpg -modulate 100,100,120 output.jpg
# Combined adjustments
magick input.jpg -modulate 105,120,100 output.jpg
# Adjust specific color channels
magick input.jpg -channel Red -evaluate multiply 1.2 output.jpg
```
### Auto Corrections
```bash
# Auto level (normalize contrast)
magick input.jpg -auto-level output.jpg
# Auto gamma correction
magick input.jpg -auto-gamma output.jpg
# Normalize (stretch histogram)
magick input.jpg -normalize output.jpg
# Enhance (digital enhancement)
magick input.jpg -enhance output.jpg
# Equalize (histogram equalization)
magick input.jpg -equalize output.jpg
```
## Transformations
### Rotation
```bash
# Rotate 90° clockwise
magick input.jpg -rotate 90 output.jpg
# Rotate 180°
magick input.jpg -rotate 180 output.jpg
# Rotate counter-clockwise
magick input.jpg -rotate -90 output.jpg
# Rotate with background
magick input.jpg -background white -rotate 45 output.jpg
# Auto-orient based on EXIF
magick input.jpg -auto-orient output.jpg
```
### Flip & Mirror
```bash
# Flip vertically
magick input.jpg -flip output.jpg
# Flip horizontally (mirror)
magick input.jpg -flop output.jpg
# Both
magick input.jpg -flip -flop output.jpg
```
## Borders & Frames
### Simple Borders
```bash
# Add 10px black border
magick input.jpg -border 10x10 output.jpg
# Colored border
magick input.jpg -bordercolor red -border 10x10 output.jpg
# Different width/height
magick input.jpg -bordercolor blue -border 20x10 output.jpg
```
### Advanced Frames
```bash
# Raised frame
magick input.jpg -mattecolor gray -frame 10x10+5+5 output.jpg
# Shadow effect
magick input.jpg \
\( +clone -background black -shadow 80x3+5+5 \) \
+swap -background white -layers merge +repage \
output.jpg
# Rounded corners
magick input.jpg \
\( +clone -threshold -1 -draw "fill black polygon 0,0 0,15 15,0 fill white circle 15,15 15,0" \
\( +clone -flip \) -compose multiply -composite \
\( +clone -flop \) -compose multiply -composite \
\) -alpha off -compose copy_opacity -composite \
output.png
```
## Text & Annotations
### Basic Text
```bash
# Simple text overlay
magick input.jpg -pointsize 30 -fill white -annotate +10+30 "Hello" output.jpg
# Positioned text
magick input.jpg -gravity south -pointsize 20 -fill white \
-annotate +0+10 "Copyright 2025" output.jpg
# Text with background
magick input.jpg -gravity center -pointsize 40 -fill white \
-undercolor black -annotate +0+0 "Watermark" output.jpg
```
### Advanced Text
```bash
# Semi-transparent watermark
magick input.jpg \
\( -background none -fill "rgba(255,255,255,0.5)" \
-pointsize 50 label:"DRAFT" \) \
-gravity center -compose over -composite \
output.jpg
# Text with stroke
magick input.jpg -gravity center \
-stroke black -strokewidth 2 -fill white \
-pointsize 60 -annotate +0+0 "Title" \
output.jpg
# Custom font
magick input.jpg -font Arial-Bold -pointsize 40 \
-gravity center -fill white -annotate +0+0 "Text" \
output.jpg
```
## Image Composition
### Overlay Images
```bash
# Basic overlay (top-left)
magick input.jpg overlay.png -composite output.jpg
# Position with gravity
magick input.jpg watermark.png -gravity southeast -composite output.jpg
# Position with offset
magick input.jpg watermark.png -gravity southeast \
-geometry +10+10 -composite output.jpg
# Center overlay
magick input.jpg logo.png -gravity center -composite output.jpg
```
### Composite Modes
```bash
# Over (default)
magick input.jpg overlay.png -compose over -composite output.jpg
# Multiply
magick input.jpg texture.png -compose multiply -composite output.jpg
# Screen
magick input.jpg light.png -compose screen -composite output.jpg
# Overlay blend mode
magick input.jpg pattern.png -compose overlay -composite output.jpg
```
### Side-by-Side
```bash
# Horizontal append
magick image1.jpg image2.jpg +append output.jpg
# Vertical append
magick image1.jpg image2.jpg -append output.jpg
# With spacing
magick image1.jpg image2.jpg -gravity center \
-background white -splice 10x0 +append output.jpg
```
## Transparency
### Create Transparency
```bash
# Make color transparent
magick input.jpg -transparent white output.png
# Make similar colors transparent (with fuzz)
magick input.jpg -fuzz 10% -transparent white output.png
# Alpha channel operations
magick input.png -alpha set -channel A -evaluate multiply 0.5 +channel output.png
```
### Remove Transparency
```bash
# Flatten with white background
magick input.png -background white -flatten output.jpg
# Flatten with custom color
magick input.png -background "#ff0000" -flatten output.jpg
```
## Advanced Techniques
### Vignette Effect
```bash
# Default vignette
magick input.jpg -vignette 0x20 output.jpg
# Custom vignette
magick input.jpg -background black -vignette 0x25+10+10 output.jpg
```
### Depth of Field Blur
```bash
# Radial blur from center
magick input.jpg \
\( +clone -blur 0x8 \) \
\( +clone -fill white -colorize 100 \
-fill black -draw "circle %[fx:w/2],%[fx:h/2] %[fx:w/2],%[fx:h/4]" \
-blur 0x20 \) \
-composite output.jpg
```
### HDR Effect
```bash
magick input.jpg \
\( +clone -colorspace gray \) \
\( -clone 0 -auto-level -modulate 100,150,100 \) \
-delete 0 -compose overlay -composite \
output.jpg
```
### Tilt-Shift Effect
```bash
magick input.jpg \
\( +clone -sparse-color Barycentric '0,%[fx:h*0.3] gray0 0,%[fx:h*0.5] white 0,%[fx:h*0.7] gray0' \) \
\( +clone -blur 0x20 \) \
-compose blend -define compose:args=100 -composite \
output.jpg
```
## Color Management
### Color Profiles
```bash
# Strip color profile
magick input.jpg -strip output.jpg
# Assign color profile
magick input.jpg -profile sRGB.icc output.jpg
# Convert between profiles
magick input.jpg -profile AdobeRGB.icc -profile sRGB.icc output.jpg
```
### Color Space Conversion
```bash
# Convert to sRGB
magick input.jpg -colorspace sRGB output.jpg
# Convert to CMYK (print)
magick input.jpg -colorspace CMYK output.tif
# Convert to LAB
magick input.jpg -colorspace LAB output.jpg
```
## Performance Optimization
### Memory Management
```bash
# Limit memory usage
magick -limit memory 2GB -limit map 4GB input.jpg -resize 50% output.jpg
# Set thread count
magick -limit thread 4 input.jpg -resize 50% output.jpg
# Streaming for large files
magick -define stream:buffer-size=0 huge.jpg -resize 50% output.jpg
```
### Quality vs Size
```bash
# Maximum quality (large file)
magick input.jpg -quality 95 output.jpg
# Balanced (recommended)
magick input.jpg -quality 85 -strip output.jpg
# Smaller file (acceptable quality)
magick input.jpg -quality 70 -sampling-factor 4:2:0 -strip output.jpg
# Progressive JPEG
magick input.jpg -quality 85 -interlace Plane -strip output.jpg
```
## Common Recipes
### Avatar/Profile Picture
```bash
# Square thumbnail
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 avatar.jpg
# Circular avatar (PNG)
magick input.jpg -resize 200x200^ -gravity center -extent 200x200 \
\( +clone -threshold -1 -negate -fill white -draw "circle 100,100 100,0" \) \
-alpha off -compose copy_opacity -composite avatar.png
```
### Responsive Images
```bash
# Generate multiple sizes
for size in 320 640 1024 1920; do
magick input.jpg -resize ${size}x -quality 85 -strip "output-${size}w.jpg"
done
```
### Photo Enhancement
```bash
# Auto-enhance workflow
magick input.jpg \
-auto-level \
-unsharp 0x1 \
-brightness-contrast 5x10 \
-modulate 100,110,100 \
-quality 90 -strip \
output.jpg
```

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Batch image resizing with multiple strategies.
Supports aspect ratio maintenance, smart cropping, thumbnail generation,
watermarks, format conversion, and parallel processing.
"""
import argparse
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import List, Optional, Tuple
class ImageResizer:
"""Handle image resizing operations using ImageMagick."""
def __init__(self, verbose: bool = False, dry_run: bool = False):
self.verbose = verbose
self.dry_run = dry_run
def check_imagemagick(self) -> bool:
"""Check if ImageMagick is available."""
try:
subprocess.run(
['magick', '-version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def build_resize_command(
self,
input_path: Path,
output_path: Path,
width: Optional[int],
height: Optional[int],
strategy: str,
quality: int,
watermark: Optional[Path] = None
) -> List[str]:
"""Build ImageMagick resize command based on strategy."""
cmd = ['magick', str(input_path)]
# Apply resize strategy
if strategy == 'fit':
# Fit within dimensions, maintain aspect ratio
geometry = f"{width or ''}x{height or ''}"
cmd.extend(['-resize', geometry])
elif strategy == 'fill':
# Fill dimensions, crop excess
if not width or not height:
raise ValueError("Both width and height required for 'fill' strategy")
cmd.extend([
'-resize', f'{width}x{height}^',
'-gravity', 'center',
'-extent', f'{width}x{height}'
])
elif strategy == 'cover':
# Cover dimensions, may exceed
if not width or not height:
raise ValueError("Both width and height required for 'cover' strategy")
cmd.extend(['-resize', f'{width}x{height}^'])
elif strategy == 'exact':
# Force exact dimensions, ignore aspect ratio
if not width or not height:
raise ValueError("Both width and height required for 'exact' strategy")
cmd.extend(['-resize', f'{width}x{height}!'])
elif strategy == 'thumbnail':
# Create square thumbnail
size = width or height or 200
cmd.extend([
'-resize', f'{size}x{size}^',
'-gravity', 'center',
'-extent', f'{size}x{size}'
])
# Add watermark if specified
if watermark:
cmd.extend([
str(watermark),
'-gravity', 'southeast',
'-geometry', '+10+10',
'-composite'
])
# Output settings
cmd.extend([
'-quality', str(quality),
'-strip',
str(output_path)
])
return cmd
def resize_image(
self,
input_path: Path,
output_path: Path,
width: Optional[int],
height: Optional[int],
strategy: str = 'fit',
quality: int = 85,
watermark: Optional[Path] = None
) -> bool:
"""Resize a single image."""
try:
# Ensure output directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
cmd = self.build_resize_command(
input_path, output_path, width, height,
strategy, quality, watermark
)
if self.verbose or self.dry_run:
print(f"Command: {' '.join(cmd)}")
if self.dry_run:
return True
subprocess.run(
cmd,
stdout=subprocess.PIPE if not self.verbose else None,
stderr=subprocess.PIPE if not self.verbose else None,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"Error resizing {input_path}: {e}", file=sys.stderr)
if not self.verbose and e.stderr:
print(e.stderr.decode(), file=sys.stderr)
return False
except Exception as e:
print(f"Error processing {input_path}: {e}", file=sys.stderr)
return False
def batch_resize(
self,
input_paths: List[Path],
output_dir: Path,
width: Optional[int],
height: Optional[int],
strategy: str = 'fit',
quality: int = 85,
format_ext: Optional[str] = None,
watermark: Optional[Path] = None,
parallel: int = 1
) -> Tuple[int, int]:
"""Resize multiple images."""
success_count = 0
fail_count = 0
def process_image(input_path: Path) -> Tuple[Path, bool]:
"""Process single image for parallel execution."""
if not input_path.exists() or not input_path.is_file():
return input_path, False
# Determine output path
output_name = input_path.stem
if format_ext:
output_path = output_dir / f"{output_name}.{format_ext.lstrip('.')}"
else:
output_path = output_dir / input_path.name
if not self.dry_run:
print(f"Processing {input_path.name} -> {output_path.name}")
success = self.resize_image(
input_path, output_path, width, height,
strategy, quality, watermark
)
return input_path, success
# Process images
if parallel > 1:
with ThreadPoolExecutor(max_workers=parallel) as executor:
futures = [executor.submit(process_image, path) for path in input_paths]
for future in as_completed(futures):
_, success = future.result()
if success:
success_count += 1
else:
fail_count += 1
else:
for input_path in input_paths:
_, success = process_image(input_path)
if success:
success_count += 1
else:
fail_count += 1
return success_count, fail_count
def collect_images(paths: List[Path], recursive: bool = False) -> List[Path]:
"""Collect image files from paths."""
image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'}
images = []
for path in paths:
if path.is_file() and path.suffix.lower() in image_exts:
images.append(path)
elif path.is_dir():
pattern = '**/*' if recursive else '*'
for img_path in path.glob(pattern):
if img_path.is_file() and img_path.suffix.lower() in image_exts:
images.append(img_path)
return images
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Batch image resizing with multiple strategies.'
)
parser.add_argument(
'inputs',
nargs='+',
type=Path,
help='Input image(s) or directory'
)
parser.add_argument(
'-o', '--output',
type=Path,
required=True,
help='Output directory'
)
parser.add_argument(
'-w', '--width',
type=int,
help='Target width in pixels'
)
parser.add_argument(
'-h', '--height',
type=int,
dest='img_height',
help='Target height in pixels'
)
parser.add_argument(
'-s', '--strategy',
choices=['fit', 'fill', 'cover', 'exact', 'thumbnail'],
default='fit',
help='Resize strategy (default: fit)'
)
parser.add_argument(
'-q', '--quality',
type=int,
default=85,
help='Output quality 0-100 (default: 85)'
)
parser.add_argument(
'-f', '--format',
help='Output format (e.g., jpg, png, webp)'
)
parser.add_argument(
'-wm', '--watermark',
type=Path,
help='Watermark image to overlay'
)
parser.add_argument(
'-p', '--parallel',
type=int,
default=1,
help='Number of parallel processes (default: 1)'
)
parser.add_argument(
'-r', '--recursive',
action='store_true',
help='Process directories recursively'
)
parser.add_argument(
'-n', '--dry-run',
action='store_true',
help='Show commands without executing'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Verbose output'
)
args = parser.parse_args()
# Validate dimensions
if not args.width and not args.img_height:
print("Error: At least one of --width or --height required", file=sys.stderr)
sys.exit(1)
# Initialize resizer
resizer = ImageResizer(verbose=args.verbose, dry_run=args.dry_run)
# Check dependencies
if not resizer.check_imagemagick():
print("Error: ImageMagick not found", file=sys.stderr)
sys.exit(1)
# Collect input images
images = collect_images(args.inputs, args.recursive)
if not images:
print("Error: No images found", file=sys.stderr)
sys.exit(1)
print(f"Found {len(images)} image(s) to process")
# Create output directory
if not args.dry_run:
args.output.mkdir(parents=True, exist_ok=True)
# Process images
success, fail = resizer.batch_resize(
images,
args.output,
args.width,
args.img_height,
args.strategy,
args.quality,
args.format,
args.watermark,
args.parallel
)
print(f"\nResults: {success} succeeded, {fail} failed")
sys.exit(0 if fail == 0 else 1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""
Unified media conversion tool for video, audio, and images.
Auto-detects format and applies appropriate tool (FFmpeg or ImageMagick).
Supports quality presets, batch processing, and dry-run mode.
"""
import argparse
import subprocess
import sys
from pathlib import Path
from typing import List, Optional, Tuple
# Format mappings
VIDEO_FORMATS = {'.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.m4v'}
AUDIO_FORMATS = {'.mp3', '.aac', '.m4a', '.opus', '.flac', '.wav', '.ogg'}
IMAGE_FORMATS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif'}
# Quality presets
QUALITY_PRESETS = {
'web': {
'video_crf': 23,
'video_preset': 'medium',
'audio_bitrate': '128k',
'image_quality': 85
},
'archive': {
'video_crf': 18,
'video_preset': 'slow',
'audio_bitrate': '192k',
'image_quality': 95
},
'mobile': {
'video_crf': 26,
'video_preset': 'fast',
'audio_bitrate': '96k',
'image_quality': 80
}
}
def check_dependencies() -> Tuple[bool, bool]:
"""Check if ffmpeg and imagemagick are available."""
ffmpeg_available = subprocess.run(
['ffmpeg', '-version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
).returncode == 0
magick_available = subprocess.run(
['magick', '-version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
).returncode == 0
return ffmpeg_available, magick_available
def detect_media_type(file_path: Path) -> str:
"""Detect media type from file extension."""
ext = file_path.suffix.lower()
if ext in VIDEO_FORMATS:
return 'video'
elif ext in AUDIO_FORMATS:
return 'audio'
elif ext in IMAGE_FORMATS:
return 'image'
else:
return 'unknown'
def build_video_command(
input_path: Path,
output_path: Path,
preset: str = 'web'
) -> List[str]:
"""Build FFmpeg command for video conversion."""
quality = QUALITY_PRESETS[preset]
return [
'ffmpeg', '-i', str(input_path),
'-c:v', 'libx264',
'-preset', quality['video_preset'],
'-crf', str(quality['video_crf']),
'-c:a', 'aac',
'-b:a', quality['audio_bitrate'],
'-movflags', '+faststart',
'-y',
str(output_path)
]
def build_audio_command(
input_path: Path,
output_path: Path,
preset: str = 'web'
) -> List[str]:
"""Build FFmpeg command for audio conversion."""
quality = QUALITY_PRESETS[preset]
output_ext = output_path.suffix.lower()
codec_map = {
'.mp3': 'libmp3lame',
'.aac': 'aac',
'.m4a': 'aac',
'.opus': 'libopus',
'.flac': 'flac',
'.wav': 'pcm_s16le',
'.ogg': 'libvorbis'
}
codec = codec_map.get(output_ext, 'aac')
cmd = ['ffmpeg', '-i', str(input_path), '-c:a', codec]
# Add bitrate for lossy codecs
if codec not in ['flac', 'pcm_s16le']:
cmd.extend(['-b:a', quality['audio_bitrate']])
cmd.extend(['-y', str(output_path)])
return cmd
def build_image_command(
input_path: Path,
output_path: Path,
preset: str = 'web'
) -> List[str]:
"""Build ImageMagick command for image conversion."""
quality = QUALITY_PRESETS[preset]
return [
'magick', str(input_path),
'-quality', str(quality['image_quality']),
'-strip',
str(output_path)
]
def convert_file(
input_path: Path,
output_path: Path,
preset: str = 'web',
dry_run: bool = False,
verbose: bool = False
) -> bool:
"""Convert a single media file."""
media_type = detect_media_type(input_path)
if media_type == 'unknown':
print(f"Error: Unsupported format for {input_path}", file=sys.stderr)
return False
# Ensure output directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
# Build command based on media type
if media_type == 'video':
cmd = build_video_command(input_path, output_path, preset)
elif media_type == 'audio':
cmd = build_audio_command(input_path, output_path, preset)
else: # image
cmd = build_image_command(input_path, output_path, preset)
if verbose or dry_run:
print(f"Command: {' '.join(cmd)}")
if dry_run:
return True
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE if not verbose else None,
stderr=subprocess.PIPE if not verbose else None,
check=True
)
return True
except subprocess.CalledProcessError as e:
print(f"Error converting {input_path}: {e}", file=sys.stderr)
if not verbose and e.stderr:
print(e.stderr.decode(), file=sys.stderr)
return False
except Exception as e:
print(f"Error converting {input_path}: {e}", file=sys.stderr)
return False
def batch_convert(
input_paths: List[Path],
output_dir: Optional[Path] = None,
output_format: Optional[str] = None,
preset: str = 'web',
dry_run: bool = False,
verbose: bool = False
) -> Tuple[int, int]:
"""Convert multiple files."""
success_count = 0
fail_count = 0
for input_path in input_paths:
if not input_path.exists():
print(f"Error: {input_path} not found", file=sys.stderr)
fail_count += 1
continue
# Determine output path
if output_dir:
output_name = input_path.stem
if output_format:
output_path = output_dir / f"{output_name}.{output_format.lstrip('.')}"
else:
output_path = output_dir / input_path.name
else:
if output_format:
output_path = input_path.with_suffix(f".{output_format.lstrip('.')}")
else:
print(f"Error: No output format specified for {input_path}", file=sys.stderr)
fail_count += 1
continue
print(f"Converting {input_path.name} -> {output_path.name}")
if convert_file(input_path, output_path, preset, dry_run, verbose):
success_count += 1
else:
fail_count += 1
return success_count, fail_count
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Unified media conversion tool for video, audio, and images.'
)
parser.add_argument(
'inputs',
nargs='+',
type=Path,
help='Input file(s) to convert'
)
parser.add_argument(
'-o', '--output',
type=Path,
help='Output file or directory for batch conversion'
)
parser.add_argument(
'-f', '--format',
help='Output format (e.g., mp4, jpg, mp3)'
)
parser.add_argument(
'-p', '--preset',
choices=['web', 'archive', 'mobile'],
default='web',
help='Quality preset (default: web)'
)
parser.add_argument(
'-n', '--dry-run',
action='store_true',
help='Show commands without executing'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Verbose output'
)
args = parser.parse_args()
# Check dependencies
ffmpeg_ok, magick_ok = check_dependencies()
if not ffmpeg_ok and not magick_ok:
print("Error: Neither ffmpeg nor imagemagick found", file=sys.stderr)
sys.exit(1)
# Handle single file vs batch conversion
if len(args.inputs) == 1 and args.output and not args.output.is_dir():
# Single file conversion
success = convert_file(
args.inputs[0],
args.output,
args.preset,
args.dry_run,
args.verbose
)
sys.exit(0 if success else 1)
else:
# Batch conversion
output_dir = args.output if args.output else Path.cwd()
if not args.output:
output_dir = None # Will convert in place with new format
success, fail = batch_convert(
args.inputs,
output_dir,
args.format,
args.preset,
args.dry_run,
args.verbose
)
print(f"\nResults: {success} succeeded, {fail} failed")
sys.exit(0 if fail == 0 else 1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,24 @@
# Media Processing Skill Dependencies
# Python 3.10+ required
# No Python package dependencies - uses system binaries
# Required system tools (install separately):
# - FFmpeg (video/audio processing)
# - ImageMagick (image processing)
# Testing dependencies (dev)
pytest>=8.0.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0
# Installation instructions:
#
# Ubuntu/Debian:
# sudo apt-get install ffmpeg imagemagick
#
# macOS (Homebrew):
# brew install ffmpeg imagemagick
#
# Windows:
# choco install ffmpeg imagemagick
# or download from official websites

View File

@@ -0,0 +1,2 @@
pytest>=7.4.0
pytest-cov>=4.1.0

View File

@@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""Tests for batch_resize.py"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, call, patch
import pytest
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from batch_resize import ImageResizer, collect_images
class TestImageResizer:
"""Test ImageResizer class."""
def setup_method(self):
"""Set up test fixtures."""
self.resizer = ImageResizer(verbose=False, dry_run=False)
@patch("subprocess.run")
def test_check_imagemagick_available(self, mock_run):
"""Test ImageMagick availability check."""
mock_run.return_value = MagicMock(returncode=0)
assert self.resizer.check_imagemagick() is True
@patch("subprocess.run")
def test_check_imagemagick_unavailable(self, mock_run):
"""Test when ImageMagick is not available."""
mock_run.side_effect = FileNotFoundError()
assert self.resizer.check_imagemagick() is False
def test_build_resize_command_fit_strategy(self):
"""Test command building for 'fit' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="fit",
quality=85
)
assert "magick" in cmd
assert str(Path("input.jpg")) in cmd
assert "-resize" in cmd
assert "800x600" in cmd
assert "-quality" in cmd
assert "85" in cmd
assert "-strip" in cmd
def test_build_resize_command_fill_strategy(self):
"""Test command building for 'fill' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="fill",
quality=85
)
assert "-resize" in cmd
assert "800x600^" in cmd
assert "-gravity" in cmd
assert "center" in cmd
assert "-extent" in cmd
def test_build_resize_command_thumbnail_strategy(self):
"""Test command building for 'thumbnail' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=200,
height=None,
strategy="thumbnail",
quality=85
)
assert "200x200^" in cmd
assert "-gravity" in cmd
assert "center" in cmd
def test_build_resize_command_with_watermark(self):
"""Test command building with watermark."""
watermark = Path("watermark.png")
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None,
strategy="fit",
quality=85,
watermark=watermark
)
assert str(watermark) in cmd
assert "-gravity" in cmd
assert "southeast" in cmd
assert "-composite" in cmd
def test_build_resize_command_exact_strategy(self):
"""Test command building for 'exact' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="exact",
quality=85
)
assert "800x600!" in cmd
def test_build_resize_command_fill_requires_dimensions(self):
"""Test that 'fill' strategy requires both dimensions."""
with pytest.raises(ValueError):
self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None,
strategy="fill",
quality=85
)
@patch("subprocess.run")
def test_resize_image_success(self, mock_run):
"""Test successful image resize."""
mock_run.return_value = MagicMock(returncode=0)
result = self.resizer.resize_image(
Path("input.jpg"),
Path("output/output.jpg"),
width=800,
height=None,
strategy="fit",
quality=85
)
assert result is True
mock_run.assert_called_once()
@patch("subprocess.run")
def test_resize_image_dry_run(self, mock_run):
"""Test resize in dry-run mode."""
resizer = ImageResizer(dry_run=True)
result = resizer.resize_image(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None
)
assert result is True
mock_run.assert_not_called()
@patch("subprocess.run")
def test_resize_image_failure(self, mock_run):
"""Test resize failure handling."""
mock_run.side_effect = Exception("Resize failed")
result = self.resizer.resize_image(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None
)
assert result is False
class TestCollectImages:
"""Test image collection functionality."""
def test_collect_images_from_file(self, tmp_path):
"""Test collecting a single image file."""
img_file = tmp_path / "test.jpg"
img_file.touch()
images = collect_images([img_file])
assert len(images) == 1
assert images[0] == img_file
def test_collect_images_from_directory(self, tmp_path):
"""Test collecting images from directory."""
(tmp_path / "image1.jpg").touch()
(tmp_path / "image2.png").touch()
(tmp_path / "text.txt").touch()
images = collect_images([tmp_path])
assert len(images) == 2
assert all(img.suffix.lower() in {'.jpg', '.png'} for img in images)
def test_collect_images_recursive(self, tmp_path):
"""Test recursive image collection."""
subdir = tmp_path / "subdir"
subdir.mkdir()
(tmp_path / "image1.jpg").touch()
(subdir / "image2.jpg").touch()
images = collect_images([tmp_path], recursive=True)
assert len(images) == 2
images_non_recursive = collect_images([tmp_path], recursive=False)
assert len(images_non_recursive) == 1
def test_collect_images_filters_extensions(self, tmp_path):
"""Test that only image files are collected."""
(tmp_path / "image.jpg").touch()
(tmp_path / "doc.pdf").touch()
(tmp_path / "text.txt").touch()
images = collect_images([tmp_path])
assert len(images) == 1
assert images[0].suffix.lower() == '.jpg'
def test_collect_images_multiple_paths(self, tmp_path):
"""Test collecting from multiple paths."""
dir1 = tmp_path / "dir1"
dir2 = tmp_path / "dir2"
dir1.mkdir()
dir2.mkdir()
(dir1 / "image1.jpg").touch()
(dir2 / "image2.png").touch()
images = collect_images([dir1, dir2])
assert len(images) == 2
class TestBatchResize:
"""Test batch resize functionality."""
def setup_method(self):
"""Set up test fixtures."""
self.resizer = ImageResizer(verbose=False, dry_run=False)
@patch.object(ImageResizer, "resize_image")
def test_batch_resize_success(self, mock_resize, tmp_path):
"""Test successful batch resize."""
mock_resize.return_value = True
input_images = [
tmp_path / "image1.jpg",
tmp_path / "image2.jpg"
]
for img in input_images:
img.touch()
output_dir = tmp_path / "output"
success, fail = self.resizer.batch_resize(
input_images,
output_dir,
width=800,
height=None,
strategy="fit"
)
assert success == 2
assert fail == 0
assert mock_resize.call_count == 2
@patch.object(ImageResizer, "resize_image")
def test_batch_resize_with_failures(self, mock_resize, tmp_path):
"""Test batch resize with some failures."""
mock_resize.side_effect = [True, False, True]
input_images = [
tmp_path / "image1.jpg",
tmp_path / "image2.jpg",
tmp_path / "image3.jpg"
]
for img in input_images:
img.touch()
output_dir = tmp_path / "output"
success, fail = self.resizer.batch_resize(
input_images,
output_dir,
width=800,
height=None
)
assert success == 2
assert fail == 1
@patch.object(ImageResizer, "resize_image")
def test_batch_resize_format_conversion(self, mock_resize, tmp_path):
"""Test batch resize with format conversion."""
mock_resize.return_value = True
input_images = [tmp_path / "image.png"]
input_images[0].touch()
output_dir = tmp_path / "output"
self.resizer.batch_resize(
input_images,
output_dir,
width=800,
height=None,
format_ext="jpg"
)
# Check that resize_image was called with .jpg extension
call_args = mock_resize.call_args[0]
assert call_args[1].suffix == ".jpg"
class TestResizeStrategies:
"""Test different resize strategies."""
def setup_method(self):
"""Set up test fixtures."""
self.resizer = ImageResizer()
def test_fit_strategy_maintains_aspect(self):
"""Test that 'fit' strategy maintains aspect ratio."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="fit",
quality=85
)
# Should have resize without ^ or !
resize_idx = cmd.index("-resize")
geometry = cmd[resize_idx + 1]
assert "^" not in geometry
assert "!" not in geometry
def test_cover_strategy_fills_dimensions(self):
"""Test that 'cover' strategy fills dimensions."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="cover",
quality=85
)
resize_idx = cmd.index("-resize")
geometry = cmd[resize_idx + 1]
assert "^" in geometry
def test_exact_strategy_ignores_aspect(self):
"""Test that 'exact' strategy ignores aspect ratio."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="exact",
quality=85
)
resize_idx = cmd.index("-resize")
geometry = cmd[resize_idx + 1]
assert "!" in geometry
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""Tests for media_convert.py"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from media_convert import (
build_audio_command,
build_image_command,
build_video_command,
check_dependencies,
convert_file,
detect_media_type,
)
class TestMediaTypeDetection:
"""Test media type detection."""
def test_detect_video_formats(self):
"""Test video format detection."""
assert detect_media_type(Path("test.mp4")) == "video"
assert detect_media_type(Path("test.mkv")) == "video"
assert detect_media_type(Path("test.avi")) == "video"
assert detect_media_type(Path("test.mov")) == "video"
def test_detect_audio_formats(self):
"""Test audio format detection."""
assert detect_media_type(Path("test.mp3")) == "audio"
assert detect_media_type(Path("test.aac")) == "audio"
assert detect_media_type(Path("test.flac")) == "audio"
assert detect_media_type(Path("test.wav")) == "audio"
def test_detect_image_formats(self):
"""Test image format detection."""
assert detect_media_type(Path("test.jpg")) == "image"
assert detect_media_type(Path("test.png")) == "image"
assert detect_media_type(Path("test.gif")) == "image"
assert detect_media_type(Path("test.webp")) == "image"
def test_detect_unknown_format(self):
"""Test unknown format detection."""
assert detect_media_type(Path("test.txt")) == "unknown"
assert detect_media_type(Path("test.doc")) == "unknown"
def test_case_insensitive(self):
"""Test case-insensitive detection."""
assert detect_media_type(Path("TEST.MP4")) == "video"
assert detect_media_type(Path("TEST.JPG")) == "image"
class TestCommandBuilding:
"""Test command building functions."""
def test_build_video_command_web_preset(self):
"""Test video command with web preset."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="web"
)
assert "ffmpeg" in cmd
assert "-i" in cmd
assert str(Path("input.mp4")) in cmd
assert "-c:v" in cmd
assert "libx264" in cmd
assert "-crf" in cmd
assert "23" in cmd
assert "-preset" in cmd
assert "medium" in cmd
assert str(Path("output.mp4")) in cmd
def test_build_video_command_archive_preset(self):
"""Test video command with archive preset."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="archive"
)
assert "18" in cmd # CRF for archive
assert "slow" in cmd # Preset for archive
def test_build_audio_command_mp3(self):
"""Test audio command for MP3 output."""
cmd = build_audio_command(
Path("input.wav"),
Path("output.mp3"),
preset="web"
)
assert "ffmpeg" in cmd
assert "-c:a" in cmd
assert "libmp3lame" in cmd
assert "-b:a" in cmd
def test_build_audio_command_flac(self):
"""Test audio command for FLAC (lossless)."""
cmd = build_audio_command(
Path("input.wav"),
Path("output.flac"),
preset="web"
)
assert "flac" in cmd
assert "-b:a" not in cmd # No bitrate for lossless
def test_build_image_command(self):
"""Test image command building."""
cmd = build_image_command(
Path("input.png"),
Path("output.jpg"),
preset="web"
)
assert "magick" in cmd
assert str(Path("input.png")) in cmd
assert "-quality" in cmd
assert "85" in cmd
assert "-strip" in cmd
assert str(Path("output.jpg")) in cmd
class TestDependencyCheck:
"""Test dependency checking."""
@patch("subprocess.run")
def test_check_dependencies_both_available(self, mock_run):
"""Test when both tools are available."""
mock_run.return_value = MagicMock(returncode=0)
ffmpeg_ok, magick_ok = check_dependencies()
assert ffmpeg_ok is True
assert magick_ok is True
@patch("subprocess.run")
def test_check_dependencies_ffmpeg_only(self, mock_run):
"""Test when only FFmpeg is available."""
def side_effect(*args, **kwargs):
if "ffmpeg" in args[0]:
return MagicMock(returncode=0)
return MagicMock(returncode=1)
mock_run.side_effect = side_effect
ffmpeg_ok, magick_ok = check_dependencies()
assert ffmpeg_ok is True
assert magick_ok is False
class TestFileConversion:
"""Test file conversion functionality."""
@patch("subprocess.run")
@patch("media_convert.detect_media_type")
def test_convert_video_file_dry_run(self, mock_detect, mock_run):
"""Test video conversion in dry-run mode."""
mock_detect.return_value = "video"
result = convert_file(
Path("input.mp4"),
Path("output.mp4"),
preset="web",
dry_run=True
)
assert result is True
mock_run.assert_not_called()
@patch("subprocess.run")
@patch("media_convert.detect_media_type")
def test_convert_image_file_success(self, mock_detect, mock_run):
"""Test successful image conversion."""
mock_detect.return_value = "image"
mock_run.return_value = MagicMock(returncode=0)
result = convert_file(
Path("input.png"),
Path("output.jpg"),
preset="web"
)
assert result is True
mock_run.assert_called_once()
@patch("subprocess.run")
@patch("media_convert.detect_media_type")
def test_convert_file_error(self, mock_detect, mock_run):
"""Test conversion error handling."""
mock_detect.return_value = "video"
mock_run.side_effect = Exception("Conversion failed")
result = convert_file(
Path("input.mp4"),
Path("output.mp4")
)
assert result is False
@patch("media_convert.detect_media_type")
def test_convert_unknown_format(self, mock_detect):
"""Test conversion with unknown format."""
mock_detect.return_value = "unknown"
result = convert_file(
Path("input.txt"),
Path("output.txt")
)
assert result is False
class TestQualityPresets:
"""Test quality preset functionality."""
def test_web_preset_settings(self):
"""Test web preset values."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="web"
)
cmd_str = " ".join(cmd)
assert "23" in cmd_str # CRF
assert "128k" in cmd_str # Audio bitrate
def test_archive_preset_settings(self):
"""Test archive preset values."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="archive"
)
cmd_str = " ".join(cmd)
assert "18" in cmd_str # Higher quality CRF
assert "192k" in cmd_str # Higher audio bitrate
def test_mobile_preset_settings(self):
"""Test mobile preset values."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="mobile"
)
cmd_str = " ".join(cmd)
assert "26" in cmd_str # Lower quality CRF
assert "96k" in cmd_str # Lower audio bitrate
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,397 @@
#!/usr/bin/env python3
"""Tests for video_optimize.py"""
import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from video_optimize import VideoInfo, VideoOptimizer
class TestVideoOptimizer:
"""Test VideoOptimizer class."""
def setup_method(self):
"""Set up test fixtures."""
self.optimizer = VideoOptimizer(verbose=False, dry_run=False)
@patch("subprocess.run")
def test_check_ffmpeg_available(self, mock_run):
"""Test FFmpeg availability check."""
mock_run.return_value = MagicMock(returncode=0)
assert self.optimizer.check_ffmpeg() is True
@patch("subprocess.run")
def test_check_ffmpeg_unavailable(self, mock_run):
"""Test when FFmpeg is not available."""
mock_run.side_effect = FileNotFoundError()
assert self.optimizer.check_ffmpeg() is False
@patch("subprocess.run")
def test_get_video_info_success(self, mock_run):
"""Test successful video info extraction."""
mock_data = {
"streams": [
{
"codec_type": "video",
"codec_name": "h264",
"width": 1920,
"height": 1080,
"r_frame_rate": "30/1"
},
{
"codec_type": "audio",
"codec_name": "aac",
"bit_rate": "128000"
}
],
"format": {
"duration": "120.5",
"bit_rate": "5000000",
"size": "75000000"
}
}
mock_run.return_value = MagicMock(
stdout=json.dumps(mock_data).encode(),
returncode=0
)
info = self.optimizer.get_video_info(Path("test.mp4"))
assert info is not None
assert info.width == 1920
assert info.height == 1080
assert info.fps == 30.0
assert info.codec == "h264"
assert info.audio_codec == "aac"
@patch("subprocess.run")
def test_get_video_info_failure(self, mock_run):
"""Test video info extraction failure."""
mock_run.side_effect = Exception("ffprobe failed")
info = self.optimizer.get_video_info(Path("test.mp4"))
assert info is None
def test_calculate_target_resolution_no_constraints(self):
"""Test resolution calculation without constraints."""
width, height = self.optimizer.calculate_target_resolution(
1920, 1080, None, None
)
assert width == 1920
assert height == 1080
def test_calculate_target_resolution_width_constraint(self):
"""Test resolution calculation with width constraint."""
width, height = self.optimizer.calculate_target_resolution(
1920, 1080, 1280, None
)
assert width == 1280
assert height == 720
def test_calculate_target_resolution_height_constraint(self):
"""Test resolution calculation with height constraint."""
width, height = self.optimizer.calculate_target_resolution(
1920, 1080, None, 720
)
assert width == 1280
assert height == 720
def test_calculate_target_resolution_both_constraints(self):
"""Test resolution calculation with both constraints."""
width, height = self.optimizer.calculate_target_resolution(
1920, 1080, 1280, 720
)
assert width == 1280
assert height == 720
def test_calculate_target_resolution_even_dimensions(self):
"""Test that dimensions are always even."""
width, height = self.optimizer.calculate_target_resolution(
1920, 1080, 1279, None # Odd width
)
assert width % 2 == 0
assert height % 2 == 0
def test_calculate_target_resolution_no_upscale(self):
"""Test that small videos are not upscaled."""
width, height = self.optimizer.calculate_target_resolution(
640, 480, 1920, 1080
)
assert width == 640
assert height == 480
@patch("subprocess.run")
@patch.object(VideoOptimizer, "get_video_info")
def test_optimize_video_dry_run(self, mock_get_info, mock_run):
"""Test video optimization in dry-run mode."""
mock_info = VideoInfo(
path=Path("input.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.return_value = mock_info
optimizer = VideoOptimizer(dry_run=True)
result = optimizer.optimize_video(
Path("input.mp4"),
Path("output.mp4"),
max_width=1280
)
assert result is True
mock_run.assert_not_called()
@patch("subprocess.run")
@patch.object(VideoOptimizer, "get_video_info")
def test_optimize_video_resolution_reduction(self, mock_get_info, mock_run):
"""Test video optimization with resolution reduction."""
mock_info = VideoInfo(
path=Path("input.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.return_value = mock_info
mock_run.return_value = MagicMock(returncode=0)
result = self.optimizer.optimize_video(
Path("input.mp4"),
Path("output.mp4"),
max_width=1280,
max_height=720
)
assert result is True
mock_run.assert_called_once()
# Check that scale filter is applied
cmd = mock_run.call_args[0][0]
assert "-vf" in cmd
filter_idx = cmd.index("-vf")
assert "scale=1280:720" in cmd[filter_idx + 1]
@patch("subprocess.run")
@patch.object(VideoOptimizer, "get_video_info")
def test_optimize_video_fps_reduction(self, mock_get_info, mock_run):
"""Test video optimization with FPS reduction."""
mock_info = VideoInfo(
path=Path("input.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=60.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.return_value = mock_info
mock_run.return_value = MagicMock(returncode=0)
result = self.optimizer.optimize_video(
Path("input.mp4"),
Path("output.mp4"),
target_fps=30.0
)
assert result is True
# Check that FPS filter is applied
cmd = mock_run.call_args[0][0]
assert "-r" in cmd
fps_idx = cmd.index("-r")
assert "30.0" in cmd[fps_idx + 1]
@patch("subprocess.run")
@patch.object(VideoOptimizer, "get_video_info")
def test_optimize_video_two_pass(self, mock_get_info, mock_run):
"""Test two-pass encoding."""
mock_info = VideoInfo(
path=Path("input.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.return_value = mock_info
mock_run.return_value = MagicMock(returncode=0)
result = self.optimizer.optimize_video(
Path("input.mp4"),
Path("output.mp4"),
two_pass=True
)
assert result is True
# Should be called twice (pass 1 and pass 2)
assert mock_run.call_count == 2
# Check pass 1 command
pass1_cmd = mock_run.call_args_list[0][0][0]
assert "-pass" in pass1_cmd
assert "1" in pass1_cmd
# Check pass 2 command
pass2_cmd = mock_run.call_args_list[1][0][0]
assert "-pass" in pass2_cmd
assert "2" in pass2_cmd
@patch("subprocess.run")
@patch.object(VideoOptimizer, "get_video_info")
def test_optimize_video_crf_encoding(self, mock_get_info, mock_run):
"""Test CRF-based encoding (single pass)."""
mock_info = VideoInfo(
path=Path("input.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.return_value = mock_info
mock_run.return_value = MagicMock(returncode=0)
result = self.optimizer.optimize_video(
Path("input.mp4"),
Path("output.mp4"),
crf=23,
two_pass=False
)
assert result is True
mock_run.assert_called_once()
# Check CRF parameter
cmd = mock_run.call_args[0][0]
assert "-crf" in cmd
crf_idx = cmd.index("-crf")
assert "23" in cmd[crf_idx + 1]
@patch("subprocess.run")
@patch.object(VideoOptimizer, "get_video_info")
def test_optimize_video_failure(self, mock_get_info, mock_run):
"""Test optimization failure handling."""
mock_info = VideoInfo(
path=Path("input.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.return_value = mock_info
mock_run.side_effect = Exception("FFmpeg failed")
result = self.optimizer.optimize_video(
Path("input.mp4"),
Path("output.mp4")
)
assert result is False
class TestVideoInfo:
"""Test VideoInfo dataclass."""
def test_video_info_creation(self):
"""Test creating VideoInfo object."""
info = VideoInfo(
path=Path("test.mp4"),
duration=120.5,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
assert info.width == 1920
assert info.height == 1080
assert info.fps == 30.0
assert info.codec == "h264"
class TestCompareVideos:
"""Test video comparison functionality."""
@patch.object(VideoOptimizer, "get_video_info")
def test_compare_videos_success(self, mock_get_info, capsys):
"""Test video comparison output."""
orig_info = VideoInfo(
path=Path("original.mp4"),
duration=120.0,
width=1920,
height=1080,
bitrate=5000000,
fps=30.0,
size=75000000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
opt_info = VideoInfo(
path=Path("optimized.mp4"),
duration=120.0,
width=1280,
height=720,
bitrate=2500000,
fps=30.0,
size=37500000,
codec="h264",
audio_codec="aac",
audio_bitrate=128000
)
mock_get_info.side_effect = [orig_info, opt_info]
optimizer = VideoOptimizer()
optimizer.compare_videos(Path("original.mp4"), Path("optimized.mp4"))
captured = capsys.readouterr()
assert "Resolution" in captured.out
assert "1920x1080" in captured.out
assert "1280x720" in captured.out
assert "50.0%" in captured.out # Size reduction
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,414 @@
#!/usr/bin/env python3
"""
Video size optimization with quality/size balance.
Supports resolution reduction, frame rate adjustment, audio bitrate optimization,
multi-pass encoding, and comparison metrics.
"""
import argparse
import json
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple
@dataclass
class VideoInfo:
"""Video file information."""
path: Path
duration: float
width: int
height: int
bitrate: int
fps: float
size: int
codec: str
audio_codec: str
audio_bitrate: int
class VideoOptimizer:
"""Handle video optimization operations using FFmpeg."""
def __init__(self, verbose: bool = False, dry_run: bool = False):
self.verbose = verbose
self.dry_run = dry_run
def check_ffmpeg(self) -> bool:
"""Check if FFmpeg is available."""
try:
subprocess.run(
['ffmpeg', '-version'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def get_video_info(self, input_path: Path) -> Optional[VideoInfo]:
"""Extract video information using ffprobe."""
try:
cmd = [
'ffprobe',
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
str(input_path)
]
result = subprocess.run(cmd, capture_output=True, check=True)
data = json.loads(result.stdout)
# Find video and audio streams
video_stream = None
audio_stream = None
for stream in data['streams']:
if stream['codec_type'] == 'video' and not video_stream:
video_stream = stream
elif stream['codec_type'] == 'audio' and not audio_stream:
audio_stream = stream
if not video_stream:
return None
# Parse frame rate
fps_parts = video_stream.get('r_frame_rate', '0/1').split('/')
fps = float(fps_parts[0]) / float(fps_parts[1]) if len(fps_parts) == 2 else 0
return VideoInfo(
path=input_path,
duration=float(data['format'].get('duration', 0)),
width=int(video_stream.get('width', 0)),
height=int(video_stream.get('height', 0)),
bitrate=int(data['format'].get('bit_rate', 0)),
fps=fps,
size=int(data['format'].get('size', 0)),
codec=video_stream.get('codec_name', 'unknown'),
audio_codec=audio_stream.get('codec_name', 'none') if audio_stream else 'none',
audio_bitrate=int(audio_stream.get('bit_rate', 0)) if audio_stream else 0
)
except Exception as e:
print(f"Error getting video info: {e}", file=sys.stderr)
return None
def calculate_target_resolution(
self,
width: int,
height: int,
max_width: Optional[int],
max_height: Optional[int]
) -> Tuple[int, int]:
"""Calculate target resolution maintaining aspect ratio."""
if not max_width and not max_height:
return width, height
aspect_ratio = width / height
if max_width and max_height:
# Fit within both constraints
if width > max_width or height > max_height:
if width / max_width > height / max_height:
new_width = max_width
new_height = int(max_width / aspect_ratio)
else:
new_height = max_height
new_width = int(max_height * aspect_ratio)
else:
new_width, new_height = width, height
elif max_width:
new_width = min(width, max_width)
new_height = int(new_width / aspect_ratio)
else:
new_height = min(height, max_height)
new_width = int(new_height * aspect_ratio)
# Ensure dimensions are even (required by some codecs)
new_width = new_width - (new_width % 2)
new_height = new_height - (new_height % 2)
return new_width, new_height
def optimize_video(
self,
input_path: Path,
output_path: Path,
max_width: Optional[int] = None,
max_height: Optional[int] = None,
target_fps: Optional[float] = None,
crf: int = 23,
audio_bitrate: str = '128k',
preset: str = 'medium',
two_pass: bool = False
) -> bool:
"""Optimize a video file."""
# Get input video info
info = self.get_video_info(input_path)
if not info:
print(f"Error: Could not read video info for {input_path}", file=sys.stderr)
return False
if self.verbose:
print(f"\nInput video info:")
print(f" Resolution: {info.width}x{info.height}")
print(f" FPS: {info.fps:.2f}")
print(f" Bitrate: {info.bitrate // 1000} kbps")
print(f" Size: {info.size / (1024*1024):.2f} MB")
# Calculate target resolution
target_width, target_height = self.calculate_target_resolution(
info.width, info.height, max_width, max_height
)
# Build FFmpeg command
cmd = ['ffmpeg', '-i', str(input_path)]
# Video filters
filters = []
if target_width != info.width or target_height != info.height:
filters.append(f'scale={target_width}:{target_height}')
if filters:
cmd.extend(['-vf', ','.join(filters)])
# Frame rate adjustment
if target_fps and target_fps < info.fps:
cmd.extend(['-r', str(target_fps)])
# Video encoding
if two_pass:
# Two-pass encoding for better quality
target_bitrate = int(info.bitrate * 0.7) # 30% reduction
# Pass 1
pass1_cmd = cmd + [
'-c:v', 'libx264',
'-preset', preset,
'-b:v', str(target_bitrate),
'-pass', '1',
'-an',
'-f', 'null',
'/dev/null' if sys.platform != 'win32' else 'NUL'
]
if self.verbose or self.dry_run:
print(f"Pass 1: {' '.join(pass1_cmd)}")
if not self.dry_run:
try:
subprocess.run(pass1_cmd, check=True, capture_output=not self.verbose)
except subprocess.CalledProcessError as e:
print(f"Error in pass 1: {e}", file=sys.stderr)
return False
# Pass 2
cmd.extend([
'-c:v', 'libx264',
'-preset', preset,
'-b:v', str(target_bitrate),
'-pass', '2'
])
else:
# Single-pass CRF encoding
cmd.extend([
'-c:v', 'libx264',
'-preset', preset,
'-crf', str(crf)
])
# Audio encoding
cmd.extend([
'-c:a', 'aac',
'-b:a', audio_bitrate
])
# Output
cmd.extend(['-movflags', '+faststart', '-y', str(output_path)])
if self.verbose or self.dry_run:
print(f"Command: {' '.join(cmd)}")
if self.dry_run:
return True
# Execute
try:
subprocess.run(cmd, check=True, capture_output=not self.verbose)
# Get output info
output_info = self.get_video_info(output_path)
if output_info and self.verbose:
print(f"\nOutput video info:")
print(f" Resolution: {output_info.width}x{output_info.height}")
print(f" FPS: {output_info.fps:.2f}")
print(f" Bitrate: {output_info.bitrate // 1000} kbps")
print(f" Size: {output_info.size / (1024*1024):.2f} MB")
reduction = (1 - output_info.size / info.size) * 100
print(f" Size reduction: {reduction:.1f}%")
return True
except subprocess.CalledProcessError as e:
print(f"Error optimizing video: {e}", file=sys.stderr)
return False
except Exception as e:
print(f"Error optimizing video: {e}", file=sys.stderr)
return False
finally:
# Clean up two-pass log files
if two_pass and not self.dry_run:
for log_file in Path('.').glob('ffmpeg2pass-*.log*'):
log_file.unlink(missing_ok=True)
def compare_videos(self, original: Path, optimized: Path) -> None:
"""Compare original and optimized videos."""
orig_info = self.get_video_info(original)
opt_info = self.get_video_info(optimized)
if not orig_info or not opt_info:
print("Error: Could not compare videos", file=sys.stderr)
return
print(f"\n{'Metric':<20} {'Original':<20} {'Optimized':<20} {'Change':<15}")
print("-" * 75)
# Resolution
orig_res = f"{orig_info.width}x{orig_info.height}"
opt_res = f"{opt_info.width}x{opt_info.height}"
print(f"{'Resolution':<20} {orig_res:<20} {opt_res:<20}")
# FPS
fps_change = opt_info.fps - orig_info.fps
print(f"{'FPS':<20} {orig_info.fps:<20.2f} {opt_info.fps:<20.2f} {fps_change:+.2f}")
# Bitrate
orig_br = f"{orig_info.bitrate // 1000} kbps"
opt_br = f"{opt_info.bitrate // 1000} kbps"
br_change = ((opt_info.bitrate / orig_info.bitrate) - 1) * 100
print(f"{'Bitrate':<20} {orig_br:<20} {opt_br:<20} {br_change:+.1f}%")
# Size
orig_size = f"{orig_info.size / (1024*1024):.2f} MB"
opt_size = f"{opt_info.size / (1024*1024):.2f} MB"
size_reduction = (1 - opt_info.size / orig_info.size) * 100
print(f"{'Size':<20} {orig_size:<20} {opt_size:<20} {-size_reduction:.1f}%")
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description='Video size optimization with quality/size balance.'
)
parser.add_argument(
'input',
type=Path,
help='Input video file'
)
parser.add_argument(
'-o', '--output',
type=Path,
required=True,
help='Output video file'
)
parser.add_argument(
'-w', '--max-width',
type=int,
help='Maximum width in pixels'
)
parser.add_argument(
'-H', '--max-height',
type=int,
help='Maximum height in pixels'
)
parser.add_argument(
'--fps',
type=float,
help='Target frame rate'
)
parser.add_argument(
'--crf',
type=int,
default=23,
help='CRF quality (18-28, lower=better, default: 23)'
)
parser.add_argument(
'--audio-bitrate',
default='128k',
help='Audio bitrate (default: 128k)'
)
parser.add_argument(
'--preset',
choices=['ultrafast', 'superfast', 'veryfast', 'faster', 'fast',
'medium', 'slow', 'slower', 'veryslow'],
default='medium',
help='Encoding preset (default: medium)'
)
parser.add_argument(
'--two-pass',
action='store_true',
help='Use two-pass encoding (better quality)'
)
parser.add_argument(
'--compare',
action='store_true',
help='Compare original and optimized videos'
)
parser.add_argument(
'-n', '--dry-run',
action='store_true',
help='Show command without executing'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Verbose output'
)
args = parser.parse_args()
# Validate input
if not args.input.exists():
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
sys.exit(1)
# Initialize optimizer
optimizer = VideoOptimizer(verbose=args.verbose, dry_run=args.dry_run)
# Check dependencies
if not optimizer.check_ffmpeg():
print("Error: FFmpeg not found", file=sys.stderr)
sys.exit(1)
# Optimize video
print(f"Optimizing {args.input.name}...")
success = optimizer.optimize_video(
args.input,
args.output,
args.max_width,
args.max_height,
args.fps,
args.crf,
args.audio_bitrate,
args.preset,
args.two_pass
)
if not success:
sys.exit(1)
# Compare if requested
if args.compare and not args.dry_run:
optimizer.compare_videos(args.input, args.output)
print(f"\nOptimized video saved to: {args.output}")
if __name__ == '__main__':
main()