32blogby StudioMitsu
ffmpeg8 min read

Automate Video Processing with FFmpeg and Python

Learn how to call FFmpeg from Python to automate batch video processing. Covers subprocess, ffmpeg-python, progress bars, and error handling with retry logic.

FFmpegPythonautomationbatch processingvideo processing
On this page

If you're converting videos by hand, automating the process with Python is well worth the effort. Whether you need to batch-convert 100 MP4 files to WebM or resize a folder of recordings all at once, a single script can handle it.

This article walks through calling FFmpeg from Python — starting with the basics using subprocess, then moving to the ffmpeg-python library, a full folder batch converter, progress bars, and finally a robust error handling pattern with retries.

Why Use Python with FFmpeg

FFmpeg is a complete and powerful CLI tool on its own. But when you're dealing with large numbers of files or complex conditional logic, shell scripts start to show their limits.

Combining Python gives you several advantages.

  • File enumeration and filtering: target only files matching certain extensions, sizes, or naming patterns
  • Error handling and retries: log failures and reprocess them automatically
  • Progress display: know in real time how many files are done and how many remain
  • Structured logging: save conversion results in a format you can review later

Shell scripts can handle some of this, but Python keeps the logic readable as complexity grows.

The Basics with subprocess

The simplest way to call FFmpeg from Python is subprocess.run(). Since FFmpeg is a CLI tool, you pass the command-line arguments as a list and it just works.

python
import subprocess
from pathlib import Path


def convert_to_mp4(input_path: Path, output_path: Path) -> bool:
    """Convert a file to MP4. Returns True on success, False on failure."""
    cmd = [
        "ffmpeg",
        "-i", str(input_path),
        "-c:v", "libx264",
        "-crf", "23",
        "-preset", "medium",
        "-c:a", "aac",
        "-b:a", "128k",
        "-y",               # overwrite without asking
        str(output_path),
    ]

    result = subprocess.run(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    return result.returncode == 0


if __name__ == "__main__":
    src = Path("input.mov")
    dst = Path("output.mp4")

    if convert_to_mp4(src, dst):
        print(f"Success: {dst}")
    else:
        print("Conversion failed")

A few things worth noting.

  • The "-y" flag overwrites the output file if it already exists — useful for batch processing
  • stdout=subprocess.PIPE and stderr=subprocess.PIPE capture FFmpeg's output instead of letting it flood the terminal
  • FFmpeg writes its logs to stderr even on success. Access error output via result.stderr.decode()

Using the ffmpeg-python Library

Calling subprocess directly is straightforward, but the command list gets unwieldy when you add many options. The ffmpeg-python library offers a cleaner approach.

Install it with pip.

python
# pip install ffmpeg-python

You build the FFmpeg pipeline using method chaining.

python
import ffmpeg


def transcode(input_path: str, output_path: str) -> None:
    """Transcode using ffmpeg-python."""
    (
        ffmpeg
        .input(input_path)
        .output(
            output_path,
            vcodec="libx264",
            crf=23,
            preset="medium",
            acodec="aac",
            audio_bitrate="128k",
        )
        .overwrite_output()
        .run(capture_stdout=True, capture_stderr=True)
    )


if __name__ == "__main__":
    transcode("input.mov", "output.mp4")

When an error occurs, ffmpeg.Error is raised. The e.stderr attribute contains FFmpeg's standard error output, which is useful for debugging.

python
import ffmpeg


def transcode_safe(input_path: str, output_path: str) -> None:
    try:
        (
            ffmpeg
            .input(input_path)
            .output(output_path, vcodec="libx264", crf=23)
            .overwrite_output()
            .run(capture_stdout=True, capture_stderr=True)
        )
    except ffmpeg.Error as e:
        print("FFmpeg error:")
        print(e.stderr.decode())
        raise

Batch Converting a Folder of Videos

Now for the main event. The script below converts all MOV files in a folder to MP4. It uses pathlib.glob() to enumerate files and processes them one by one.

python
import subprocess
from pathlib import Path


INPUT_DIR = Path("./originals")
OUTPUT_DIR = Path("./converted")
INPUT_EXT = ".mov"
OUTPUT_EXT = ".mp4"


def convert_file(src: Path, dst: Path) -> bool:
    """Convert a single file to MP4."""
    dst.parent.mkdir(parents=True, exist_ok=True)

    cmd = [
        "ffmpeg",
        "-i", str(src),
        "-c:v", "libx264",
        "-crf", "23",
        "-preset", "fast",
        "-c:a", "aac",
        "-b:a", "128k",
        "-movflags", "+faststart",  # optimize for web playback
        "-y",
        str(dst),
    ]

    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return result.returncode == 0


def batch_convert(input_dir: Path, output_dir: Path) -> None:
    """Convert all INPUT_EXT files in input_dir to OUTPUT_EXT."""
    files = list(input_dir.glob(f"**/*{INPUT_EXT}"))

    if not files:
        print(f"No {INPUT_EXT} files found in {input_dir}")
        return

    print(f"Found {len(files)} file(s) to convert")
    success_count = 0
    fail_list: list[Path] = []

    for i, src in enumerate(files, start=1):
        # preserve the relative folder structure in the output
        relative = src.relative_to(input_dir)
        dst = output_dir / relative.with_suffix(OUTPUT_EXT)

        print(f"[{i}/{len(files)}] {src.name} → {dst.name}", end=" ... ")

        if convert_file(src, dst):
            print("OK")
            success_count += 1
        else:
            print("FAILED")
            fail_list.append(src)

    print(f"\nDone: {success_count}/{len(files)} succeeded")

    if fail_list:
        print("Failed files:")
        for f in fail_list:
            print(f"  {f}")


if __name__ == "__main__":
    batch_convert(INPUT_DIR, OUTPUT_DIR)

The recursive glob pattern processes subfolders automatically. The output mirrors the original directory structure, so even deeply nested files stay organized.

Adding a Progress Bar

When processing a large number of files, a progress bar makes the wait much more manageable. tqdm adds one in a single line.

python
import subprocess
from pathlib import Path
from tqdm import tqdm


# pip install tqdm


def convert_file(src: Path, dst: Path) -> bool:
    dst.parent.mkdir(parents=True, exist_ok=True)
    cmd = [
        "ffmpeg", "-i", str(src),
        "-c:v", "libx264", "-crf", "23", "-preset", "fast",
        "-c:a", "aac", "-b:a", "128k",
        "-y", str(dst),
    ]
    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return result.returncode == 0


def batch_convert_with_progress(input_dir: Path, output_dir: Path) -> None:
    files = list(input_dir.glob("**/*.mov"))
    fail_list: list[Path] = []

    with tqdm(total=len(files), unit="file", ncols=80) as pbar:
        for src in files:
            relative = src.relative_to(input_dir)
            dst = output_dir / relative.with_suffix(".mp4")

            pbar.set_description(src.name[:30])  # show filename on the left

            if not convert_file(src, dst):
                fail_list.append(src)

            pbar.update(1)

    if fail_list:
        print(f"\nFailed: {len(fail_list)} file(s)")
        for f in fail_list:
            print(f"  {f}")


if __name__ == "__main__":
    batch_convert_with_progress(Path("./originals"), Path("./converted"))

set_description() shows the current filename to the left of the bar. Truncating with [:30] prevents long filenames from breaking the layout.

Error Handling and Retry Logic

One tricky aspect of FFmpeg error handling is that a successful exit code does not guarantee the output file is valid. If the input has data corruption partway through, FFmpeg may finish with exit code 0 while producing an unplayable file.

The script below combines retry logic with a basic output validation step using ffprobe.

python
import subprocess
import time
from pathlib import Path


MAX_RETRIES = 3
RETRY_DELAY = 2  # seconds


def verify_output(path: Path) -> bool:
    """Validate the output file using ffprobe."""
    result = subprocess.run(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration",
         "-of", "default=noprint_wrappers=1:nokey=1", str(path)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    if result.returncode != 0:
        return False
    output = result.stdout.decode().strip()
    return bool(output) and float(output) > 0


def convert_with_retry(src: Path, dst: Path, retries: int = MAX_RETRIES) -> bool:
    """Convert with up to `retries` attempts."""
    for attempt in range(1, retries + 1):
        dst.parent.mkdir(parents=True, exist_ok=True)

        cmd = [
            "ffmpeg", "-i", str(src),
            "-c:v", "libx264", "-crf", "23", "-preset", "fast",
            "-c:a", "aac", "-b:a", "128k",
            "-y", str(dst),
        ]

        result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        if result.returncode != 0:
            print(f"  Attempt {attempt}/{retries}: FFmpeg error (exit {result.returncode})")
            if attempt < retries:
                time.sleep(RETRY_DELAY)
            continue

        if not verify_output(dst):
            print(f"  Attempt {attempt}/{retries}: output validation failed")
            dst.unlink(missing_ok=True)  # remove the broken file
            if attempt < retries:
                time.sleep(RETRY_DELAY)
            continue

        return True

    return False


def batch_convert_robust(input_dir: Path, output_dir: Path) -> None:
    files = list(input_dir.glob("**/*.mov"))
    success, failed = 0, []

    for i, src in enumerate(files, start=1):
        relative = src.relative_to(input_dir)
        dst = output_dir / relative.with_suffix(".mp4")
        print(f"[{i}/{len(files)}] {src.name}")

        if convert_with_retry(src, dst):
            print(f"  Done: {dst}")
            success += 1
        else:
            print(f"  Skipped after {MAX_RETRIES} failed attempts: {src}")
            failed.append(src)

    print(f"\nResult: {success} succeeded / {len(failed)} failed / {len(files)} total")

    if failed:
        log_path = output_dir / "failed.txt"
        log_path.write_text("\n".join(str(f) for f in failed))
        print(f"Failure log saved to: {log_path}")


if __name__ == "__main__":
    batch_convert_robust(Path("./originals"), Path("./converted"))

Failed files are written to failed.txt in the output directory so you can review and reprocess them later.

For a solid grounding in FFmpeg itself, check out the FFmpeg usage tutorial.

Wrapping Up

Here's a summary of what this article covered.

  • subprocess.run() is the simplest way to call FFmpeg from Python, using the same options you'd pass on the command line
  • ffmpeg-python lets you build pipelines with method chaining, which is especially useful for complex filter graphs
  • Batch conversion uses pathlib.glob() to enumerate files and preserves the original folder structure in the output
  • tqdm adds a progress bar in one line — essential for long-running batch jobs
  • Robust error handling means not trusting exit code alone. Validate output with ffprobe and retry on failure

To take these scripts further, consider adding argparse for CLI arguments and logging to write structured logs to a file.