A lightweight macOS desktop app that batch-compresses video files using ffmpeg. Scan a folder, review the detected settings, and let it encode — all with a clean progress UI.
Built for people who want to compress a library of movies or TV shows quickly without manually adding files to an encoder one by one. Point it at a folder and walk away.
Made with 💛 by Paul Davies
Like this project? Help keep the lights on at weluvbeer.com by buying me a Ko-fi!
- Recursively scans a source folder for video files across any number of subdirectories
- Handles files with or without a dedicated container folder
- Auto-detects resolution, FPS, audio tracks and subtitles per file
- Review all files before encoding — see exactly what will be processed
- Encoding presets — separate presets for 1080p and 4K content:
- Fast H.264 (Hardware) — VideoToolbox h264, quality 50
- Fast H.265 (Hardware) — VideoToolbox HEVC, quality 50 (default)
- Fast H.264 — x264, fast preset, RF 22
- Balanced H.265 — x265, medium preset, RF 20
- Quality H.265 12-bit — x265 12-bit, medium preset, RF 20
- Quality AV1 — SVT-AV1, preset 4, RF 30
- Audio bitexact stream copy — the original audio is copied byte-for-byte, preserving codec, channels, channel layout and bitrate
- Smart audio track selection — configurable language preference with automatic fallback; prefers DTS/TrueHD within the chosen language, then falls back to the fallback language, then the first available track
- Language Preferences — independent dropdowns for audio track language, subtitle language, and a fallback language used when the preferred language is not found:
- Original Language (default) — uses the language of the first audio track in each file, so a mixed batch of English, Korean, French films each picks its own native language automatically
- Non-English — always picks the first non-English track, regardless of language; useful for batches of foreign-language content
- Specific language — force a particular language (English, Korean, Japanese, Thai, Vietnamese, French, German, Italian, Spanish, Dutch, Portuguese, Chinese, Polish, Danish, Swedish, Finnish, Norwegian) for the whole batch
- Prioritise DTS — when checked, DTS and TrueHD tracks are preferred within the selected language
- Subtitle language — independently selectable; always defaults to English; falls back to the fallback language, then English if the chosen language is not present
- Subtitles passed through at most 1 Forced + 1 Regular + 1 SDH per file, never burned in
- Mirrors source folder structure in the output directory
- Copies non-video files (subtitles, artwork, NFO etc.) per-directory as each directory is encoded
- Post-encode verification — two-stage check using ffprobe:
- Duration check — output must match source within 2 seconds
- Audio packet scan — streams every audio packet timestamp looking for gaps (>1s) and backwards jumps that cause loud-bang / desync issues
- Reverse compression detection — if the output is larger than the source, the original is copied to the output path instead
- Post-processing — automatically rename and clean up after encoding:
- Successful encode: output folder (or file) renamed with a custom suffix, e.g.
Movie.Done - Failed encode or zero compression (output no smaller than source): source folder renamed with the problem suffix, e.g.
Movie.Check; source file is copied to the output so nothing is lost - Orphaned extras (subtitles, artwork etc.) are cleaned up if an encode fails partway through
- Optionally delete the source file/folder after a verified successful encode
- Successful encode: output folder (or file) renamed with a custom suffix, e.g.
- Scrollable per-file progress panel with FPS and ETA
- Cancel individual files or stop everything immediately
- Dismiss completed rows individually or clear all finished files
- Add more files to the queue while encoding is already running
- Thermal throttle safeguards — monitors encoding FPS and automatically pauses to let the machine cool down when performance degrades, plus proactive scheduled cooldowns to prevent overheating during long batches
- Process safety — graceful SIGTERM shutdown for ffmpeg (preserves VideoToolbox GPU encoder sessions), orphan process cleanup on startup, bounded memory buffers, explicit pipe closure
Don't want to use the terminal? Grab the latest built app from the Releases page.
- Download
BulkVideoCompressor.app.zip - Unzip and drag to your Applications folder
- Right-click → Open the first time to get past macOS Gatekeeper
- Grant Full Disk Access in System Settings → Privacy & Security → Full Disk Access
ffmpeg is still required:
brew install ffmpeg
- Python 3.9+
- ffmpeg and ffprobe — install via Homebrew:
brew install ffmpeg - MediaInfo (optional, falls back to ffprobe) —
brew install mediainfo
git clone https://github.com/DavoInMelbourne/BulkVideoCompressor.git
cd BulkVideoCompressor
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python3 main.pypython3 main.pypip install pytest
python3 -m pytest tests/ -vpip install pyinstaller
pyinstaller --onefile --windowed --name "BulkVideoCompressor" main.pyThe app will be in dist/.
- Source Directory — folder to scan (recursive)
- Output Directory — where encoded files are written, mirroring the source structure
- 1080p Preset — encoding preset for content below 4K
- 4K Preset — encoding preset for 2160p/3840p+ content
- Audio Language — preferred audio track language for the batch (see Language Preferences above)
- Subtitles — preferred subtitle language (default: English)
- Fallback — language used for both audio and subtitles if the preferred language is not found in a file
- Prioritise DTS — when checked, DTS and TrueHD tracks are preferred within the selected language
- RF Quality — Constant Quality value (set automatically by preset; lower = better quality / larger file)
- Success suffix — text appended to the output folder/file name on success (e.g.
Done→Movie.Done); leave blank to skip - Problem suffix — text appended to the source folder/file name on failure (e.g.
Check→Movie.Check); leave blank to skip - Delete source files — if checked, deletes the source file and its folder after a verified successful encode
- Min FPS — minimum expected encoding FPS at 10% progress (see Thermal Safeguards below)
- Cool every / for — proactive cooldown interval (see Thermal Safeguards below)
- ffmpeg Path — auto-detected; override if needed
- Click Scan & Review to inspect detected settings
- Click Add to Queue to start encoding
| Scenario | has container folder | Result |
|---|---|---|
| Success | Yes | Output folder renamed, e.g. Movie → Movie.Done |
| Success | No (file in root) | Output file renamed, e.g. Movie.mkv → Movie.Done.mkv |
| Failed/error | Yes | Source folder renamed, e.g. Movie → Movie.Check |
| Failed/error | No (file in root) | Source file renamed, e.g. Movie.mkv → Movie.Check.mkv |
| Delete source | Yes | Source file deleted, then source folder deleted |
| Delete source | No (file in root) | Source file deleted only (root folder is never deleted) |
| Badge | Colour | Meaning |
|---|---|---|
| Waiting | Grey | Queued, not yet started |
| Encoding | Blue | Currently being encoded |
| Running large | Orange | Output is tracking larger than source at 25% |
| Scanning... | Orange | Post-encode audio packet scan in progress |
| Safe to delete | Green | Verified — original can be removed (shows final FPS) |
| Keep original | Red | Verification failed — do not delete source |
| Reverse compression | Purple | Output was larger — original copied to output |
| Problem file | Orange | File cannot reach minimum FPS threshold |
| Failed | Red | Encode failed |
| ERROR | Red | Unexpected crash (caught and logged) |
| Cancelled | Grey | Manually cancelled |
| Setting | Value |
|---|---|
| Video encoder | Selectable per resolution — x264, x265, AV1, or VideoToolbox |
| Quality mode | Constant Quality (RF) or VideoToolbox quality level |
| Framerate | Same as source; 50 fps is halved to 25 fps |
| Resolution / crop | No change (pass-through) |
| Audio | Bitexact stream copy — codec, channels and layout preserved |
| Subtitles | Forced, Regular, SDH — stream copy, never burned in |
| Container | MKV |
Long batch encodes are vulnerable to two things that silently kill FPS: thermal throttling and macOS power management. The app handles both automatically.
When you lock your screen or leave your Mac idle, macOS aggressively throttles background processes via App Nap — FPS can drop from 250 to single digits even though the machine is cold. The app automatically runs caffeinate -dims while encoding is active, which prevents display sleep, idle sleep, disk sleep, and system sleep. This keeps ffmpeg running at full speed even when you walk away. caffeinate is stopped automatically when encoding finishes or the app is closed.
-
Baseline capture — on the first file, at 10% progress, the app records the current FPS as the baseline and displays it on the form (e.g.
250 Base FPS). If the baseline is below your Min FPS setting, encoding stops with a prompt to lower the value. -
Problem file detection — if a file can't reach the Min FPS threshold at 10% progress, it's retried after a 10-second pause. If it still can't hit the threshold, it's marked as a problem file (orange) and skipped.
-
Proactive cooldown — a scheduled pause is inserted every N files (default: 10) for a configurable duration (default: 2 minutes). This prevents thermal buildup before it starts affecting FPS.
-
Batch summary — at the end of a run, a summary shows total files processed, problem files, and total cooldown time.
| Setting | Default | Notes |
|---|---|---|
| Min FPS | 200 | Suitable for most modern machines (less than ~2 years old). Lower this if your machine consistently runs below 200 FPS — the baseline will tell you where your machine sits. |
| Cool every | 10 files | How often to insert a proactive cooldown. Increase if your machine stays cool, reduce if it runs hot. |
| Cool for | 2.0 min | Duration of each proactive cooldown. The defaults are intentionally proactive to keep throughput high, but can be tailored per machine. |
The app is designed for unattended overnight batch encoding. Key safeguards:
- Graceful process shutdown — ffmpeg is sent SIGTERM first, with a 5-second window to release VideoToolbox hardware encoder sessions. SIGKILL is only used as a fallback. This prevents GPU encoder slot exhaustion that causes progressive FPS degradation.
- Orphan cleanup — on startup, any leftover ffmpeg/ffprobe processes from a previous crash are detected and killed.
- Stall detection — if ffmpeg produces no measurable encoding progress for 10 minutes, the process is terminated. Files with lossless audio (DTS-HD MA, TrueHD) that report
time=N/Aare handled correctly — activity is detected via FPS output so they are never incorrectly killed. - Bounded buffers — the progress-reading buffer is capped at 8KB to prevent unbounded memory growth.
- Pipe cleanup — stdout pipes are explicitly closed after every encode to prevent file descriptor leaks.
- Streaming verification — audio packet scanning streams ffprobe output line-by-line instead of loading it all into memory.
- Memory monitoring — each ffmpeg process is watched via psutil; if RSS exceeds 12GB the encode is killed with a clear error.
- App Nap prevention —
caffeinate -dimsruns during encoding to prevent macOS from throttling ffmpeg when the screen is locked or idle. - Large file support — the encode queue correctly handles files of any size; post-processing runs asynchronously so a slow verification scan on a large output file does not stall the queue.
.mkv .mp4 .avi .mov .ts .m2ts
BulkVideoCompressor/
main.py # App entry point
requirements.txt # Python dependencies
core/
handbrake.py # ffmpeg process management, verification, orphan cleanup
languages.py # Language enum (audio/subtitle language preferences)
mediainfo.py # Video file probing (metadata extraction)
queue_builder.py # Audio/subtitle track selection logic
scanner.py # Directory scanning and output path mapping
ui/
main_window.py # Main UI, queue management, state machine
workers.py # Background threads (ProbeWorker, EncodeWorker)
review_dialog.py # Pre-encoding review modal
tests/
test_process_cleanup.py # 33 tests covering process safety and thermal safeguards
When you're ready to publish a new version:
# 1. Build the app
pip install pyinstaller
pyinstaller --onefile --windowed --name "BulkVideoCompressor" main.py
# 2. Zip it (GitHub needs a zip — .app files are actually folders)
cd dist && zip -r BulkVideoCompressor.app.zip BulkVideoCompressor.app && cd ..
# 3. Tag the release in git
git tag v1.0.0
git push origin v1.0.0Then on GitHub:
- Go to Releases → Draft a new release
- Pick the tag you just pushed
- Attach
dist/BulkVideoCompressor.app.zip - Publish
Local use: drag
dist/BulkVideoCompressor.appstraight to your Applications folder — no need to zip.GitHub Release: attach the
.zip— when users download and unzip it they get the.app.
The download link in this README will point users straight to the Releases page.
MIT