A constraint-driven video compression pipeline designed for the Discord 20MB file limit.
ffmpeg-shrinkwrap automatically compresses an entire directory worth of videos to fit Discord's 20MB limit (or any target size) using dynamic bitrate calculation and intelligent fallback strategies. Works on Linux, macOS, WSL, and native Windows.
Unlike static presets, it calculates exact bitrates and uses a heuristic to balance quality against size constraints, automating the "trial and error" process of encoding, retrying, downscaling, and splitting.
- Cross-Platform: Native Bash for Linux/macOS/WSL, PowerShell for Windows
- Clean UX: Start banner, per-file
[i/N]batch counter, live per-pass progress (Bash), and an end-of-run summary (status counts, total saved, elapsed). Graceful Ctrl+C cleanup (Linux/macOS and Windows), automatic cleanup - Target-Based Encoding: Calculates exact bitrates to fill the target size without wasting space
- Bidirectional Convergence: If a 2-pass result lands well under target, it re-encodes once at a higher bitrate to reclaim the unused headroom -- better quality, not just "under the limit"
- Multi-Format Input: Accepts
mp4,mkv,mov,avi,webm,m4v,flv(output is always.mp4) - Intelligent Fallback Chain:
- Phase 1: Adaptive retry with bitrate convergence (2-pass x265)
- Phase 2: 720p rescue mode if bitrate floor is hit
- Phase 3: Last-resort capped CRF 28 (
-crf 28 -maxrate -bufsize) -- CRF quality that still can't exceed the size cap - Phase 4: Keyframe-aware splitting (scans packet flags for the nearest keyframe before the midpoint)
- High Efficiency: Uses
libx265(HEVC) 2-pass encoding by default, with opt-in GPU encoders (AMF / NVENC / QSV / VideoToolbox via-c hw) that are functionally probed and fall back to software automatically - Broad Compatibility: Auto-detects FFmpeg version for correct flags, ensures
yuv420pfor universal playback - Post-Mortem Reports: Generates compression summary with before/after stats
- Download - Click
Code → Download ZIPand extract - Run - Double-click
drag_videos_here.bat- First time: Press
Yto auto-download FFmpeg (~100MB, one-time) - After setup: Drag videos onto the .bat file to compress or double-click to compress all videos in current directory
- First time: Press
That's all Folks! Compressed videos appear in the optimized folder.
- Option A: Drag video file(s) onto
drag_videos_here.bat - Option B: Drag an entire folder onto
drag_videos_here.bat - Option C: Double-click
drag_videos_here.batto process all videos in current folder
.\shrinkwrap.ps1 -Files "clip.mp4" # Process single file (19.8MB default)
.\shrinkwrap.ps1 -TargetSizeMB 49 -Preset medium -Files "clip.mp4" # Target 50MB Nitro Basic upload
.\shrinkwrap.ps1 -Encoder hw -Files "clip.mp4" # Use GPU hardware acceleration
.\shrinkwrap.ps1 -NormalizeAudio -Mono -Files "clip.mp4" # Normalize loudness + downmix mono
.\shrinkwrap.ps1 -NoAudio -Files "clip.mp4" # Strip audio completely (-an)
.\shrinkwrap.ps1 -Config # Interactive setup wizardParameters:
-Encoder <string> Video encoder: auto (default), hw, or a specific encoder name
(see "Hardware-accelerated encoding" below)
-Preset <string> FFmpeg preset (slow/medium/fast, default: slow)
-TargetSizeMB <float> Target file size in MB (default: 19.8)
-MinVideoBitrate <int> Video bitrate floor in kbps (default: 500)
-MinAudioBitrate <int> Audio bitrate floor in kbps (default: 64)
-MaxRetries <int> Max retry attempts (default: 3)
-NormalizeAudio Loudness-normalize audio (EBU R128, -16 LUFS, two-pass)
-Mono Downmix audio to mono (frees budget on voice-only clips)
-NoAudio Remove audio entirely (-an); frees the whole budget for video
-OutputDir <string> Output directory path (default: optimized)
-NoCleanup Keep logs and temp files for debugging
-Config Setup wizard: pick/persist a default encoder, then exit
(see "Saved preferences" below)
"FFmpeg download failed"
Download manually from gyan.dev/ffmpeg/builds (officially listed at ffmpeg.org), extract ffmpeg.exe and ffprobe.exe, place in shrinkwrap folder.
"Script won't run / security warning"
Windows may block scripts downloaded from the internet.
Right-click drag_videos_here.bat → Properties → Unblock → OK
# Clone repo
git clone https://github.com/nunogomes255/ffmpeg-shrinkwrap.git
cd ffmpeg-shrinkwrap
# Install globally and make executable
sudo cp shrinkwrap.sh /usr/local/bin/ffmpeg-shrinkwrap
sudo chmod +x /usr/local/bin/ffmpeg-shrinkwrapNow run from anywhere:
cd /path/to/your/videos
ffmpeg-shrinkwrapProcess all video files in current directory:
./shrinkwrap.shProcess specific files:
./shrinkwrap.sh clip1.mp4 clip2.mkv-c <string> Video encoder: auto (default), hw, or a specific encoder name
(see "Hardware-accelerated encoding" below)
-t <float> Target file size in MB (default: 19.8)
-p <string> FFmpeg x265 preset (slow/medium/fast, default: slow)
-v <int> Minimum video bitrate floor in kbps (default: 500)
-a <int> Minimum audio bitrate floor in kbps (default: 64)
-r <int> Max retries per resolution pass (default: 3)
-l Normalize audio loudness (EBU R128 / -16 LUFS, two-pass)
-m Downmix audio to mono (frees budget on voice-only clips)
-A Remove audio entirely (-an); frees the whole budget for video
-o <string> Output directory path (default: optimized)
-n No cleanup - preserve logs/temp files for debugging
-h Display help
--config Setup wizard: pick/persist a default encoder, then exit
(see "Saved preferences" below)
./shrinkwrap.sh clip.mp4 # Process single file (19.8MB default)
./shrinkwrap.sh -t 49 -p medium clip.mp4 # Target 50MB Discord Nitro Basic
./shrinkwrap.sh -c hw clip.mp4 # Auto-pick best working GPU encoder
./shrinkwrap.sh -c hevc_nvenc clip.mp4 # Force NVIDIA NVENC HEVC encoder
./shrinkwrap.sh -l -m clip.mp4 # Normalize loudness + downmix to mono
./shrinkwrap.sh -A clip.mp4 # Strip audio completely (-an)
./shrinkwrap.sh --config # Run interactive setup wizardBy default the tool uses software libx265 2-pass -- this is unchanged and remains the
recommended path for the best quality at a hard size cap. GPU encoders can be enabled
explicitly with -c (Bash) / -Encoder (PowerShell):
| Value | Behavior |
|---|---|
auto (default) |
Software detection (libx265, else libx264), 2-pass. No change to default runs. |
hw |
Probe the GPU hierarchy AV1 -> HEVC across AMF / NVENC / QSV / VideoToolbox; use the first that works, else fall back to software. |
| encoder name | Force a specific encoder, e.g. hevc_nvenc, av1_amf, h264_qsv, hevc_videotoolbox. |
Every hardware encoder is functionally validated with a tiny test encode before use
(being compiled into FFmpeg does not mean the machine can run it), so a missing or
non-functional GPU encoder is rejected and the tool falls back to software rather than
crashing mid-batch. Hardware encodes are single-pass capped VBR (NVENC adds
-multipass fullres); the CRF rescue maps to vendor constant-quality (-cq / -global_quality).
Presets are universal. Whatever you pass to -p/-Preset -- an x264/x265 name
(ultrafast...placebo), or another vendor's token (AMF quality/balanced/speed, NVENC
p1...p7) -- is normalized to a valid token for whichever encoder actually runs, via three
speed tiers (fast / medium / slow). So -p veryfast becomes AMF speed, -p quality becomes
NVENC p7 on an NVENC encoder, and so on; a token already native to the chosen encoder (and the
finer x264/QSV names) passes through unchanged. If you hand it a token from a different vendor
(or a typo), it still runs, but you get a one-line [Preset] warning plus a cheat-sheet of the
valid presets per encoder. The default slow is silent.
./shrinkwrap.sh -c hw clip.mp4 # auto-pick the best working GPU encoder
./shrinkwrap.sh -c hevc_nvenc clip.mp4 # force NVENC HEVC.\shrinkwrap.ps1 -Encoder av1_amf -Files clip.mp4 # force AMF AV1
⚠️ Why opt-in: at a hard ~19.8 MB cap, hardware single-pass generally compresses worse thanlibx2652-pass, and hardware AV1/HEVC may not play in Discord's inline player for all viewers. Use hardware for speed; keep the default for best quality/compat.
By default -- no config file, no -c/-Encoder flag -- the tool behaves exactly as it always
has: software libx265 2-pass, no prompt, ever. To make a different encoder the default
without retyping the flag, run the one-time setup wizard:
./shrinkwrap.sh --config # Linux / macOS / WSL / Git Bash.\shrinkwrap.ps1 -Config # WindowsThe wizard asks you to pick a default, writes a small shrinkwrap.conf, prints where it
saved, and exits without processing anything. Re-run it anytime to reconfigure, or edit
the file by hand. Delete the file to return to the default. There is no first-run prompt --
the wizard only runs when you explicitly ask for it.
Precedence: a per-run -c/-Encoder flag always wins over the config file, which in turn
wins over the built-in default (software x265).
The three modes:
mode |
Behavior |
|---|---|
software (default) |
Walk software_order (x265 first) -- the unchanged 2-pass path. |
hardware |
Walk hardware_order (GPU), probing each; fall back to software if none work. |
software_x264 |
Force libx264 -- maximum compatibility / legacy, plays everywhere, larger files. |
The file is plain key = value (no external dependencies) with sensible defaults. Any setting omitted from the file falls back to built-in defaults:
# ffmpeg-shrinkwrap preferences.
# Regenerate: ./shrinkwrap.sh --config | .\shrinkwrap.ps1 -Config (or edit by hand)
# Delete this file to return to defaults (software x265, 19.8MB target).
#
# mode: drives encoder choice when no -c/-Encoder flag is given.
# hardware - walk hardware_order (GPU); fall back to software_order
# software - walk software_order (x265 first)
# software_x264 - force libx264 (maximum compatibility / legacy)
mode = software
# Ordered candidates. Each entry is probed; the first that works wins. Reorder/trim freely.
hardware_order = av1_amf av1_nvenc av1_qsv hevc_amf hevc_nvenc hevc_qsv hevc_videotoolbox
software_order = libx265 libx264
# --- Compression & Speed Defaults ---
# target_size_mb: target file size in MB (default: 19.8 for Discord 20MB limit)
# preset: x265/universal preset (slow, medium, fast, faster, etc. default: slow)
target_size_mb = 19.8
preset = slow
# --- Audio Defaults ---
# normalize_audio: apply EBU R128 loudness normalization (true/false, default: false)
# mono: downmix audio to mono to save budget (true/false, default: false)
# no_audio: strip audio completely (true/false, default: false)
# audio_bitrate: initial audio bitrate in kbps (default: 192)
# min_audio_bitrate: audio bitrate floor in kbps (default: 64)
normalize_audio = false
mono = false
no_audio = false
audio_bitrate = 192
min_audio_bitrate = 64
# --- Output & Fallback Options ---
# output_dir: destination directory for compressed videos (default: optimized)
# min_video_bitrate: video bitrate floor in kbps before 720p rescue (default: 500)
# max_retries: max retry attempts per resolution pass (default: 3)
# crf_rescue_value: CRF quality value for Phase 3 rescue pass (default: 28)
# no_cleanup: preserve logs and intermediate pass files (true/false, default: false)
output_dir = optimized
min_video_bitrate = 500
max_retries = 3
crf_rescue_value = 28
no_cleanup = falseUnder mode = hardware each hardware_order entry is functionally probed at startup and the
first that works wins -- so adding e.g. h264_amf to the list makes it selectable.
Available Configuration Keys:
| Key | Type | Default | Description |
|---|---|---|---|
mode |
string | software |
Default encoder selection mode (software, hardware, software_x264). |
hardware_order |
string | (list) | Space-separated ordered list of hardware encoders to probe. |
software_order |
string | libx265 libx264 |
Space-separated fallback order for software encoders. |
target_size_mb |
float | 19.8 |
Target file size in MB (19.8MB fits within Discord 20MB cap). |
preset |
string | slow |
Compression speed preset (mapped automatically for GPU encoders). |
normalize_audio |
boolean | false |
Apply EBU R128 (-16 LUFS) two-pass loudness normalization. |
mono |
boolean | false |
Downmix audio to mono to conserve bitrate budget. |
no_audio |
boolean | false |
Strip audio completely (-an) to maximize video bitrate. |
audio_bitrate |
int | 192 |
Initial audio bitrate in kbps. |
min_audio_bitrate |
int | 64 |
Minimum audio bitrate floor in kbps before giving up audio budget. |
output_dir |
string | optimized |
Output folder path for compressed videos. |
min_video_bitrate |
int | 500 |
Minimum video bitrate floor in kbps before triggering 720p downscale. |
max_retries |
int | 3 |
Maximum retry attempts per resolution pass. |
crf_rescue_value |
int | 28 |
CRF quality target for Phase 3 rescue pass. |
no_cleanup |
boolean | false |
Preserve 2-pass log files and intermediate passes for inspection. |
Location: the script directory is used first; if it isn't writable (e.g. a system-wide
install), the file falls back to a per-user path -- $XDG_CONFIG_HOME/ffmpeg-shrinkwrap/ (or
~/.config/ffmpeg-shrinkwrap/) on Linux/macOS, %APPDATA%\ffmpeg-shrinkwrap\ on Windows.
Read order is script-dir -> per-user -> built-in defaults. Both the Bash and PowerShell ports
read and write the identical file, so a preference set in one is honored by the other.
--config/-Configneeds an interactive terminal; run non-interactively (piped/redirected) it prints an error and exits without hanging.
Instead of guessing a CRF value, the script calculates target video bitrate (
flowchart TD
A[Input Video] --> B{Calculate Bitrate}
B --> C[2-Pass Encode @ 1080p]
C --> D{Size ≤ Target?}
D -- Yes --> E[✓ Success]
D -- No --> F{Bitrate < Floor?}
F -- No --> G[Reduce Bitrate]
G --> B
F -- Yes --> H[Rescue: 720p Downscale]
H --> I{Size ≤ Target?}
I -- Yes --> E
I -- No --> J[Last Resort: Capped CRF 28]
J --> K{Size ≤ Target?}
K -- Yes --> E
K -- No --> L[Split at Keyframe]
L --> M[Optimize Part 1]
L --> N[Optimize Part 2]
M --> E
N --> E
Tested on 22 GTA V gameplay clips (27MB - 207MB):
| Metric | Result |
|---|---|
| Success Rate | 100% (all videos under 10MB) |
| Average Compression | 83% (1.6GB → 206MB total) |
| First-Pass Accuracy | 86% (19/22 worked on first try) |
| Rescue Triggers | 3 videos needed CRF fallback |
File Orig Size Final Size Reduction % Status
-------------------------------------------------------------------------------------------
Grand Theft Auto V 2025.03.10 - Clip 1 150.311 9.983 93.36 Optimized
Grand Theft Auto V 2025.03.10 - Clip 2 141.409 9.249 93.46 Optimized
Grand Theft Auto V 2025.12.09 - Clip 3 207.296 9.640 95.35 Optimized
Grand Theft Auto V 2025.12.09 - Clip 4 118.856 6.293 94.71 Rescued (CRF)
Grand Theft Auto V 2025.03.10 - Clip 5 68.718 9.499 86.18 Optimized
Key Takeaways:
- 207MB → 9.6MB (95% reduction)
- 150MB → 9.9MB (93% reduction)
- Even "impossible" cases get rescued with high quality
- FFmpeg (auto-installed on first run)
- PowerShell 5.1+ (built into Windows 10/11)
Required tools (must be in $PATH):
ffmpeg(recommend 5.0+, auto-adapts to older versions)ffprobebc(floating-point math)awk
Debian/Ubuntu:
sudo apt update && sudo apt install ffmpeg bc gawkmacOS (Homebrew):
brew install ffmpegWindows version downloads FFmpeg from:
- Primary: gyan.dev (official provider listed on ffmpeg.org)
- Fallback: GitHub mirror (github.com/GyanD/codexffmpeg)
Verify yourself:
- Visit ffmpeg.org/download.html
- See "gyan.dev" listed under official Windows builds "Windows builds from gyan.dev"
Prefer building from source?
git clone https://github.com/FFmpeg/FFmpeg
# Follow: https://trac.ffmpeg.org/wiki/CompilationGuideQ: Why x265 instead of x264?
A: Despite worse compatibility (doesn't run natively on windows 10 for free), HEVC (x265) is incredibly more optimized, produces 30-50% smaller files at the same quality, and it plays on Discord. This allows to achieve higher qualities with less file size. (HEVC-x265 vs AVC-x264)
Q: Can I use this for batch processing?
A: Yes! Both versions process entire directories by default.
Q: What if my video is under 20MB already?
A: Script detects this and just copies the file (no re-encoding).
Q: Does this work with non-MP4 files?
A: Yes -- mkv, mov, avi, webm, m4v, and flv inputs are accepted (output is always .mp4). Drag them onto the .bat, pass them as arguments, or drop them in the folder.
Q: Can I change the target to 8MB for Telegram?
A: Yes! Use -t 7.8 (Bash) or -TargetSizeMB 7.8 (PowerShell).
- HDR / 10-bit is flattened to SDR. Inputs are force-converted to 8-bit
yuv420pfor universal playback, so HDR/10-bit sources lose their wide gamut with no tone-mapping (highlights/colors can look washed out or shifted). SDR 8-bit sources are unaffected. - Audio is always re-encoded to AAC (in MP4, for Discord's inline player) -- there is no stream-copy passthrough.
-l/-Monoadjust loudness/channels but still transcode.
Issues and PRs welcome! Areas of interest:
-
Ensure support for other input formats (MKV, AVI, MOV)-- done (mp4/mkv/mov/avi/webm/m4v/flv) -
GPU-accelerated encoding as an option (NVENC, QSV, VideoToolbox)-- done (opt-in-c hw/-Encoder hw, plus AMF; see Hardware-accelerated encoding) -
Batch processing UI (progress for multiple files)-- done ([i/N]counter + end-of-run summary) - Better UI/UX
- HDR-to-SDR tone mapping for 10-bit HDR recordings
- SVT-AV1 (
libsvtav1) software encoding support - Audio stream-copy passthrough when source already meets budget
- Windows Context Menu integration (right-click -> Compress)
- Package manager distribution (Scoop, Winget, Homebrew)
- Quick platform presets (WhatsApp, Email, Telegram)
MIT License. Free to use, modify, and distribute.
Built for whoever is tired of manually tweaking replay settings, messing with Medal or using sketchy online converters/compressors. If this saved you time, give it a ⭐!