How to Batch Process and Compress Video Files Using Python and FFmpeg

How to Batch Process and Compress Video Files Using Python and FFmpeg

Compressing one video with FFmpeg is easy. Compressing two hundred of them — without typing two hundred commands, mixing up filenames, or overwriting originals — is where Python comes in. This guide walks you through a complete script that scans a folder for video files, compresses each one with FFmpeg to reduce file size while preserving visual quality, and moves the finished files to an output directory automatically.

No manual file handling, no babysitting a terminal. Set it up once, point it at a folder, and let it run.

The Challenge of Managing Large Video Libraries

Video files accumulate fast: screen recordings, meeting captures, drone footage, camera dumps, training content. Left uncompressed, they eat storage at an alarming rate — and manually compressing them has all the classic problems of repetitive file work:

  • It’s slow and attention-hungry. Each file means another command typed, another filename copied, another wait.
  • It’s error-prone. One mistyped output name and you have overwritten an original.
  • It never ends. New files keep arriving, so the job is never actually “done.”

The batch approach flips this: a script owns the whole loop — find, compress, verify, move — and you own none of it. This is the same philosophy behind all Python batch video editing: make the computer do the repetitive part, every time, the same way.

Installing FFmpeg and Accessing it via Python

FFmpeg is the industry-standard command-line tool for video processing. Install it first:

  • Windows: download a build from ffmpeg.org (or install with winget install ffmpeg), and ensure the ffmpeg executable is on your PATH.
  • macOS: brew install ffmpeg
  • Linux (Debian/Ubuntu): sudo apt install ffmpeg

Verify the installation from any terminal:

ffmpeg -version

Python talks to FFmpeg through the built-in subprocess module — no extra packages required. A minimal example that compresses a single file:

import subprocess

subprocess.run([
    "ffmpeg",
    "-i", "input.mp4",          # input file
    "-vcodec", "libx264",       # H.264 video codec
    "-crf", "28",               # quality level (lower = better quality)
    "-preset", "medium",        # speed/efficiency tradeoff
    "-acodec", "aac",           # audio codec
    "output.mp4",
], check=True)

The key setting is CRF (Constant Rate Factor): it controls the quality-versus-size tradeoff. Values around 23 are near-transparent quality; values around 28 produce substantially smaller files with a modest quality cost. Test a value on one representative file before committing to the whole library. For a deeper look at codec and quality options, see our guide to FFmpeg video compression techniques.

How to Use Python to Automate Video Compression

Now let’s scale from one file to a whole folder.

Scanning a Folder for Video Files

Use pathlib to find every video file in an input directory, matching by extension:

from pathlib import Path

INPUT_DIR = Path("videos/incoming")
VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".avi"}

def find_videos(folder: Path) -> list[Path]:
    return sorted(
        f for f in folder.iterdir()
        if f.is_file() and f.suffix.lower() in VIDEO_EXTENSIONS
    )

Matching on a defined set of extensions keeps the script from accidentally trying to “compress” a stray text file or thumbnail sitting in the same folder.

Running FFmpeg Commands Using Python’s subprocess Module

Wrap the FFmpeg call in a function that takes an input path and an output path, and loop it over the file list:

import subprocess
from pathlib import Path

def compress_video(source: Path, destination: Path, crf: int = 28) -> bool:
    """Compress a single video. Returns True on success."""
    result = subprocess.run(
        [
            "ffmpeg",
            "-i", str(source),
            "-vcodec", "libx264",
            "-crf", str(crf),
            "-preset", "medium",
            "-acodec", "aac",
            "-y",                      # overwrite output if it exists
            str(destination),
        ],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        print(f"FAILED: {source.name}\n{result.stderr[-500:]}")
        return False
    return True

[IMAGE: Python subprocess module executing an FFmpeg video compression command]

Two deliberate choices here:

  • capture_output=True keeps FFmpeg’s very chatty output from flooding your terminal, while still letting you print the tail of the error log when something fails.
  • Returning a success flag instead of raising means one corrupt video won’t kill a 200-file batch — the loop logs the failure and moves on.

Moving Processed Videos to a New Directory

A reliable batch job keeps three folders: incoming (originals waiting to be processed), compressed (finished output), and originals (source files archived after successful compression). Never delete an original until its compressed version exists and is non-empty.

Here is the complete script tying everything together:

"""batch_compress.py — compress all videos in a folder with FFmpeg."""
import shutil
import subprocess
from pathlib import Path

INPUT_DIR = Path("videos/incoming")
OUTPUT_DIR = Path("videos/compressed")
ARCHIVE_DIR = Path("videos/originals")
VIDEO_EXTENSIONS = {".mp4", ".mov", ".mkv", ".avi"}
CRF = 28


def find_videos(folder: Path) -> list[Path]:
    return sorted(
        f for f in folder.iterdir()
        if f.is_file() and f.suffix.lower() in VIDEO_EXTENSIONS
    )


def compress_video(source: Path, destination: Path) -> bool:
    result = subprocess.run(
        ["ffmpeg", "-i", str(source), "-vcodec", "libx264",
         "-crf", str(CRF), "-preset", "medium", "-acodec", "aac",
         "-y", str(destination)],
        capture_output=True, text=True,
    )
    if result.returncode != 0:
        print(f"FAILED: {source.name}\n{result.stderr[-500:]}")
        return False
    return True


def main() -> None:
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)

    videos = find_videos(INPUT_DIR)
    if not videos:
        print("No videos found — nothing to do.")
        return

    done, failed = 0, 0
    for video in videos:
        output = OUTPUT_DIR / f"{video.stem}_compressed.mp4"
        print(f"Compressing {video.name} ...")

        if compress_video(video, output) and output.exists() \
                and output.stat().st_size > 0:
            before = video.stat().st_size / 1_048_576
            after = output.stat().st_size / 1_048_576
            print(f"  {before:.1f} MB -> {after:.1f} MB")
            shutil.move(str(video), ARCHIVE_DIR / video.name)
            done += 1
        else:
            failed += 1

    print(f"Finished: {done} compressed, {failed} failed.")


if __name__ == "__main__":
    main()

Run it with python batch_compress.py. The script reports the size reduction per file, archives each original only after verifying the output, and summarizes results at the end.

[IMAGE: System directory showing output from Python FFmpeg automate batch video processing]

Scheduling Your Batch Video Processing Job

Video compression is CPU-heavy, which makes it a perfect overnight job: drop new files into the incoming folder during the day, and let the script chew through them while you sleep.

Because the script only processes what it finds in videos/incoming and archives everything it completes, it is naturally safe to run repeatedly — an empty folder simply means “nothing to do.” That makes it ideal for scheduling your batch job with cron on Linux or Task Scheduler on Windows. A nightly 2:00 AM cron entry looks like this:

0 2 * * * /usr/bin/python3 /home/sam/scripts/batch_compress.py >> /home/sam/logs/compress.log 2>&1

With that single line in place, your video library maintains itself: new footage in, compressed and organized files out, originals safely archived — and your only remaining job is occasionally emptying the archive folder once you have verified the compressed versions.

FAQ

What CRF value should I use for video compression?

CRF controls the quality/size tradeoff in H.264 encoding: lower values mean higher quality and larger files. Values in the low 20s are visually near-lossless for most content, while values around 28 give significant size savings with modest quality loss. Test one representative file at a couple of settings and compare before running the full batch.

Will compressing videos with FFmpeg reduce quality?

Re-encoding is lossy, so some quality loss is inherent — but at sensible CRF settings it is difficult to notice for typical footage like screen recordings and meetings. The script above archives your originals rather than deleting them, so you can always re-encode at a higher quality later.

How do I compress only new files and skip already-processed ones?

The folder structure handles this automatically: originals are moved out of incoming after successful compression, so the input folder only ever contains unprocessed files. Each run picks up exactly what is new.

Can I run multiple FFmpeg compressions in parallel?

You can, using concurrent.futures, but FFmpeg already uses multiple CPU cores for a single encode. Running many encodes at once often just divides the same CPU among more processes. For most libraries, sequential processing overnight is simpler and effectively just as fast.

Why does subprocess say “ffmpeg not found”?

FFmpeg is not on the PATH of the environment running the script. Either add FFmpeg’s folder to your system PATH, or replace "ffmpeg" in the command list with the absolute path to the executable — especially important when the script runs from a scheduler, which uses a minimal environment.

Leave a Comment