In this tutorial, we’ll build VideoOptimizer — a desktop video converter and compressor using Python, FFmpeg, Tkinter, and ttkbootstrap.
You’ll learn:
How to detect FFmpeg automatically
How to calculate video duration & compression targets
How to run FFmpeg safely in background threads
How to build a modern GUI with progress tracking
How to package everything into a Windows EXE
🔗 Clone the source code here:
https://github.com/rogers-cyber/VideoOptimizer
📦 Download the ready-to-use Windows EXE:
https://github.com/rogers-cyber/VideoOptimizer/releases
Step 1: Import Required Modules
We start by importing standard libraries, GUI tools, and ttkbootstrap for styling.
import sys
import os
import re
import threading
import subprocess
from pathlib import Path
from queue import Queue, Empty
import tkinter as tk
from tkinter import filedialog, messagebox
import ttkbootstrap as tb
from ttkbootstrap.constants import *
Why this matters
subprocess → run FFmpeg
threading → keep UI responsive
Queue → safely update progress from background threads
ttkbootstrap → modern UI without extra CSS
Step 2: Detect FFmpeg Automatically
FFmpeg is required, but we don’t want users configuring paths manually.
def detect_ffmpeg():
import shutil
ff = shutil.which("ffmpeg")
if ff:
return ff
fallback = r"C:\ffmpeg\bin\ffmpeg.exe"
if os.path.exists(fallback):
return fallback
return None
FFMPEG_PATH = detect_ffmpeg()
✅ Checks system PATH
✅ Falls back to a common Windows location
✅ Prevents crashes if FFmpeg is missing
Step 3: App Configuration
Centralized configuration keeps the project clean and maintainable.
APP_NAME = "VideoOptimizer"
APP_VERSION = "2.0.1"
APP_AUTHOR = "Mate Technologies"
APP_WEBSITE = "https://matetools.gumroad.com"
SNAPS = [10, 20, 30, 40, 50]
Tip: SNAPS powers the preset compression buttons in the UI.
Step 4: Hide FFmpeg Console Windows (Windows Fix)
Without this, FFmpeg spawns annoying black console windows.
def win_no_window_flags():
return subprocess.CREATE_NO_WINDOW if sys.platform.startswith("win") else 0
def hidden_ffmpeg_startupinfo():
if sys.platform.startswith("win"):
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE
return si
return None
Step 5: Read Video Duration
We parse FFmpeg output to calculate accurate progress percentages.
def get_video_duration(input_file):
try:
result = subprocess.run(
[FFMPEG_PATH, "-i", input_file],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
errors="replace"
)
m = re.search(r"Duration: (\d+):(\d+):(\d+\.?\d*)", result.stderr)
if m:
h, m_, s = int(m.group(1)), int(m.group(2)), float(m.group(3))
return h * 3600 + m_ * 60 + s
except:
pass
return None
🎯 This enables real-time progress bars instead of fake timers.
Step 6: Smart Compression Logic
We dynamically calculate CRF based on the desired reduction.
def estimate_crf(reduction):
return int(23 + (reduction / 100) * 12)
Lower CRF → higher quality
Higher CRF → smaller file
Step 7: VideoConverter (Background Worker)
This class runs FFmpeg safely in a separate thread.
class VideoConverter:
def __init__(self, input_file, output_file, reduction, mode, codec="libx264", progress_queue=None):
self.input_file = input_file
self.output_file = output_file
self.reduction = reduction
self.mode = mode
self.codec = codec
self.progress_queue = progress_queue
self.stop_event = threading.Event()
Why this matters
UI never freezes
Conversion can be stopped safely
Progress updates are thread-safe
Step 8: Quality Mode vs Target Size Mode
Quality Priority (CRF-based)
cmd = [
FFMPEG_PATH, "-y", "-i", self.input_file,
"-c:v", self.codec,
"-preset", "fast",
"-crf", str(estimate_crf(self.reduction)),
"-c:a", "aac", "-b:a", "128k",
self.output_file
]
Target Size (2-Pass Encoding)
pass1_cmd = [
FFMPEG_PATH, "-y", "-i", self.input_file,
"-c:v", self.codec, "-preset", "fast",
"-b:v", f"{video_kbps}k", "-pass", "1",
"-an", "-f", "null", "-"
]
2-pass encoding gives much more accurate file sizes.
Step 9: Parsing FFmpeg Progress
We read FFmpeg’s time= output to calculate percent complete.
m = re.search(r'time=(\d+):(\d+):(\d+\.?\d*)', line)
if m:
cur_time = h*3600 + m_*60 + s
percent = (cur_time / self.duration) * 100
🔥 This is the secret behind real progress bars.
Step 10: Building the GUI
Create the main window with ttkbootstrap styling.
self.app = tk.Tk()
self.app.geometry("800x520")
self.app.title(f"{APP_NAME} {APP_VERSION}")
self.style = tb.Style(theme="superhero")
Add input/output selectors:
tb.Entry(input_frame, textvariable=self.video_path).pack(fill="x", expand=True)
tb.Button(input_frame, text="Browse", command=self._select_video)
Step 11: Progress Bar & Controls
self.progress_val = tk.DoubleVar(value=0)
tb.Progressbar(self.app, variable=self.progress_val, maximum=100)
Buttons are automatically enabled/disabled during conversion.
Step 12: Thread-Safe UI Updates
def _update_ui(self):
while True:
try:
msg_type, val = self.progress_queue.get_nowait()
if msg_type == "progress":
self.progress_val.set(val)
elif msg_type == "status":
self.status_var.set(val)
except Empty:
break
self.app.after(50, self._update_ui)
This keeps everything smooth and crash-free.
Step 13: Run the App
if __name__ == "__main__":
app = VIDConverterApp()
app.run()
Final Result
🎉 You now have:
A real desktop video compressor
Accurate progress tracking
Modern UI
Offline FFmpeg processing
Windows EXE support
🔗 Source Code:
https://github.com/rogers-cyber/VideoOptimizer
📦 Download EXE:
https://github.com/rogers-cyber/VideoOptimizer/releases

Top comments (0)