32blogby StudioMitsu

Automatiza el procesamiento de video con FFmpeg y Python

Aprende a llamar FFmpeg desde Python para automatizar el procesamiento de video por lotes. Cubre subprocess, ffmpeg-python, barras de progreso y manejo de errores con lógica de reintentos.

8 min read
Contenido

Si estás convirtiendo videos a mano, automatizar el proceso con Python bien vale el esfuerzo. Ya sea que necesites convertir por lotes 100 archivos MP4 a WebM o redimensionar una carpeta de grabaciones de una vez, un solo script puede encargarse de todo.

Este artículo recorre cómo llamar a FFmpeg desde Python — empezando con lo básico usando subprocess, luego pasando a la biblioteca ffmpeg-python, un convertidor por lotes de carpetas completo, barras de progreso, y finalmente un patrón robusto de manejo de errores con reintentos.

Por qué usar Python con FFmpeg

FFmpeg es una herramienta CLI completa y potente por sí sola. Pero cuando estás lidiando con grandes cantidades de archivos o lógica condicional compleja, los scripts de shell empiezan a mostrar sus límites.

Combinar Python te da varias ventajas.

  • Enumeración y filtrado de archivos: apunta solo a archivos que coincidan con ciertas extensiones, tamaños o patrones de nombres
  • Manejo de errores y reintentos: registra los fallos y reprocésalos automáticamente
  • Visualización de progreso: saber en tiempo real cuántos archivos están hechos y cuántos quedan
  • Registro estructurado: guardar resultados de conversión en un formato que puedas revisar después

Los scripts de shell pueden manejar algo de esto, pero Python mantiene la lógica legible a medida que crece la complejidad.

Lo básico con subprocess

La forma más simple de llamar a FFmpeg desde Python es subprocess.run(). Como FFmpeg es una herramienta CLI, pasas los argumentos de línea de comandos como una lista y simplemente funciona.

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")

Algunas cosas que vale la pena notar.

  • La bandera "-y" sobrescribe el archivo de salida si ya existe — útil para procesamiento por lotes
  • stdout=subprocess.PIPE y stderr=subprocess.PIPE capturan la salida de FFmpeg en lugar de dejar que inunde la terminal
  • FFmpeg escribe sus logs en stderr incluso en éxito. Accede a la salida de error con result.stderr.decode()

Usando la biblioteca ffmpeg-python

Llamar a subprocess directamente es sencillo, pero la lista de comandos se vuelve difícil de manejar cuando añades muchas opciones. La biblioteca ffmpeg-python ofrece un enfoque más limpio.

Instálala con pip.

python
# pip install ffmpeg-python

Construyes el pipeline de FFmpeg usando encadenamiento de métodos.

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")

Cuando ocurre un error, se lanza ffmpeg.Error. El atributo e.stderr contiene la salida de error estándar de FFmpeg, que es útil para depuración.

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

Conversión por lotes de una carpeta de videos

Ahora el evento principal. El script a continuación convierte todos los archivos MOV de una carpeta a MP4. Usa pathlib.glob() para enumerar archivos y los procesa uno por uno.

python
import subprocess
from pathlib import Path
from typing import List


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("FALLO")
            fail_list.append(src)

    print(f"\nHecho: {success_count}/{len(files)} exitosos")

    if fail_list:
        print("Archivos fallidos:")
        for f in fail_list:
            print(f"  {f}")


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

El patrón glob recursivo procesa subcarpetas automáticamente. La salida refleja la estructura de directorios original, así que incluso los archivos profundamente anidados se mantienen organizados.

Añadir una barra de progreso

Cuando procesas una gran cantidad de archivos, una barra de progreso hace la espera mucho más manejable. tqdm añade una en una sola línea.

python
import subprocess
from pathlib import Path
from typing import List
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"\nFallidos: {len(fail_list)} archivo(s)")
        for f in fail_list:
            print(f"  {f}")


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

set_description() muestra el nombre del archivo actual a la izquierda de la barra. Truncar con [:30] evita que nombres de archivo largos rompan la disposición.

Manejo de errores y lógica de reintentos

Un aspecto complicado del manejo de errores de FFmpeg es que un código de salida exitoso no garantiza que el archivo de salida sea válido. Si la entrada tiene corrupción de datos a mitad del archivo, FFmpeg puede terminar con código de salida 0 mientras produce un archivo no reproducible.

El script a continuación combina la lógica de reintentos con un paso básico de validación de salida usando 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"  Intento {attempt}/{retries}: error de FFmpeg (salida {result.returncode})")
            if attempt < retries:
                time.sleep(RETRY_DELAY)
            continue

        if not verify_output(dst):
            print(f"  Intento {attempt}/{retries}: validación de salida fallida")
            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"  Hecho: {dst}")
            success += 1
        else:
            print(f"  Omitido tras {MAX_RETRIES} intentos fallidos: {src}")
            failed.append(src)

    print(f"\nResultado: {success} exitosos / {len(failed)} fallidos / {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"Log de fallos guardado en: {log_path}")


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

Los archivos fallidos se escriben en failed.txt en el directorio de salida para que puedas revisarlos y reprocesarlos después.

Para una base sólida en FFmpeg en sí, consulta el tutorial de uso de FFmpeg.

Conclusión

Aquí tienes un resumen de lo que cubrió este artículo.

  • subprocess.run() es la forma más simple de llamar a FFmpeg desde Python, usando las mismas opciones que pasarías en la línea de comandos
  • ffmpeg-python te permite construir pipelines con encadenamiento de métodos, lo cual es especialmente útil para grafos de filtros complejos
  • La conversión por lotes usa pathlib.glob() para enumerar archivos y preserva la estructura de carpetas original en la salida
  • tqdm añade una barra de progreso en una línea — esencial para trabajos por lotes de larga duración
  • El manejo robusto de errores significa no confiar solo en el código de salida. Valida la salida con ffprobe y reintenta en caso de fallo

Para llevar estos scripts más lejos, considera añadir argparse para argumentos CLI y logging para escribir logs estructurados a un archivo.