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.
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.PIPEystderr=subprocess.PIPEcapturan la salida de FFmpeg en lugar de dejar que inunde la terminal- FFmpeg escribe sus logs en
stderrincluso en éxito. Accede a la salida de error conresult.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.
# pip install ffmpeg-python
Construyes el pipeline de FFmpeg usando encadenamiento de métodos.
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.
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.
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.
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.
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 comandosffmpeg-pythonte 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 tqdmañ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
ffprobey 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.