ffmpeg.wasm es FFmpeg compilado a WebAssembly: permite convertir, comprimir y extraer audio de archivos de video completamente en el navegador, sin servidor, sin costos de infraestructura, y sin que los archivos del usuario salgan del dispositivo. Funciona con cualquier navegador moderno que soporte WebAssembly.
En este artículo iremos desde la instalación hasta un componente funcional de conversión de archivos en React/Next.js, cubriendo la configuración de SharedArrayBuffer/COOP/COEP que hace tropezar a la mayoría de los desarrolladores, ejemplos prácticos de conversión y los límites de rendimiento reales que deberías conocer.
Procesamiento de video sin servidor
Las herramientas tradicionales de conversión de video basadas en navegador funcionan así:
- El usuario sube un archivo de video al servidor
- El servidor ejecuta FFmpeg y convierte el archivo
- El usuario descarga el resultado convertido
Este enfoque tiene dos problemas: es lento (los archivos se transfieren dos veces) y cuesta dinero (CPU del servidor). Para archivos grandes, los usuarios pueden esperar minutos solo por el viaje de ida y vuelta.
ffmpeg.wasm es el código fuente en C de FFmpeg compilado a WebAssembly via Emscripten. Se ejecuta dentro del motor JavaScript del navegador. Todo el procesamiento ocurre en el cliente — nunca se transfiere ningún archivo a un servidor.
Ventajas principales:
- No requiere infraestructura de servidor (puro frontend)
- Los archivos nunca abandonan el dispositivo del usuario — fuerte garantía de privacidad
- Funciona sin conexión después de que el binario WASM se cachea
- Costo de escalado cero
Desventajas realistas:
- El binario WASM tarda unos segundos en descargarse en la primera carga
- La velocidad de procesamiento es más lenta que FFmpeg nativo (a menudo 5–20x más lento)
- Los archivos grandes pueden alcanzar los límites de memoria y fallar
Para archivos de tamaño pequeño a mediano en escenarios de conversión práctica, la compensación vale la pena.
Configuración y uso básico
Instala los paquetes:
npm install @ffmpeg/ffmpeg @ffmpeg/util
Aquí está el patrón de uso básico:
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
const ffmpeg = new FFmpeg();
async function convertVideo(inputFile: File): Promise<Blob> {
// Cargar el binario WASM (solo en la primera llamada)
if (!ffmpeg.loaded) {
const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(
`${baseURL}/ffmpeg-core.wasm`,
"application/wasm"
),
});
}
// Escribir el archivo de entrada en el sistema de archivos virtual de ffmpeg.wasm
await ffmpeg.writeFile("input.mp4", await fetchFile(inputFile));
// Ejecutar el comando FFmpeg (conversión MP4 → WebM)
await ffmpeg.exec(["-i", "input.mp4", "-c:v", "libvpx-vp9", "output.webm"]);
// Leer el archivo de salida
const data = await ffmpeg.readFile("output.webm");
// Convertir Uint8Array → Blob y retornar
return new Blob([data], { type: "video/webm" });
}
El array que pasas a ffmpeg.exec() se mapea directamente a los argumentos del comando FFmpeg después del nombre del binario ffmpeg. ffmpeg -i input.mp4 output.webm se convierte en ["-i", "input.mp4", "output.webm"].
El muro de SharedArrayBuffer (Headers COOP/COEP)
Para habilitar SharedArrayBuffer, tu servidor debe responder con dos headers HTTP:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
Para Next.js, agrega esto a next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async headers() {
return [
{
// Apply to all routes. Narrow the source path if you only
// need ffmpeg.wasm on specific pages.
source: "/(.*)",
headers: [
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
],
},
];
},
};
export default nextConfig;
Efectos secundarios a tener en cuenta:
COOP/COEP restringen la carga de recursos de origen cruzado. Si tu sitio carga fuentes externas, scripts de Google Analytics u otros assets de terceros, esos necesitarán atributos crossorigin="anonymous" o un header Cross-Origin-Resource-Policy en el servidor del recurso. La mayoría de los CDN importantes ya envían este header, pero puede causar roturas inesperadas si no lo has considerado.
Si usas la compilación de un solo hilo (@ffmpeg/core en lugar de @ffmpeg/core-mt), SharedArrayBuffer no es necesario y puedes omitir los headers COOP/COEP — a costa de un procesamiento más lento ya que no se usarán múltiples núcleos de CPU. Empezar con la compilación de un solo hilo es la forma más fácil de ponerse en marcha.
Al desplegar en Vercel, la configuración de headers en next.config.ts se respeta automáticamente — no se necesita configuración adicional específica de Vercel.
Ejemplos de conversión de video
Aquí hay ejemplos prácticos de conversión para casos de uso comunes.
MP4 → WebM (VP9, optimizado para tamaño de archivo):
await ffmpeg.exec([
"-i",
"input.mp4",
"-c:v",
"libvpx-vp9",
"-crf",
"33", // Calidad (menor = mayor calidad, mayor = archivo más pequeño)
"-b:v",
"0", // Modo VBR (se usa junto con -crf)
"-c:a",
"libopus",
"output.webm",
]);
MP4 → GIF (primeros 5 segundos):
await ffmpeg.exec([
"-i",
"input.mp4",
"-t",
"5", // Detener después de 5 segundos
"-vf",
"fps=15,scale=480:-1:flags=lanczos", // Tasa de cuadros y redimensionamiento
"-loop",
"0",
"output.gif",
]);
Extraer audio de video (MP3):
await ffmpeg.exec([
"-i",
"input.mp4",
"-vn", // Descartar flujo de video
"-c:a",
"libmp3lame",
"-q:a",
"2", // Calidad (0-9, menor = mayor calidad)
"output.mp3",
]);
Para seguir el progreso, registra un listener con ffmpeg.on("progress", callback) antes de llamar a exec:
ffmpeg.on("progress", ({ progress, time }) => {
// progress: 0.0 a 1.0
// time: posición actual del video en proceso (microsegundos)
console.log(`Progreso: ${Math.round(progress * 100)}%`);
});
Construir un componente React/Next.js
Aquí hay un componente completo de conversión de archivos donde los usuarios pueden soltar un archivo y recibir una descarga convertida.
"use client";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
import { useRef, useState } from "react";
const CORE_BASE_URL = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
export function VideoConverter() {
const ffmpegRef = useRef<FFmpeg>(new FFmpeg());
const [loaded, setLoaded] = useState(false);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState<number | null>(null);
const [outputUrl, setOutputUrl] = useState<string | null>(null);
async function loadFFmpeg() {
if (loaded) return;
setLoading(true);
const ffmpeg = ffmpegRef.current;
await ffmpeg.load({
coreURL: await toBlobURL(
`${CORE_BASE_URL}/ffmpeg-core.js`,
"text/javascript"
),
wasmURL: await toBlobURL(
`${CORE_BASE_URL}/ffmpeg-core.wasm`,
"application/wasm"
),
});
ffmpeg.on("progress", ({ progress }) => {
setProgress(Math.round(progress * 100));
});
setLoaded(true);
setLoading(false);
}
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
await loadFFmpeg();
const ffmpeg = ffmpegRef.current;
setProgress(0);
setOutputUrl(null);
// Escribir el archivo de entrada en el sistema de archivos virtual
await ffmpeg.writeFile("input.mp4", await fetchFile(file));
// Ejecutar la conversión
await ffmpeg.exec(["-i", "input.mp4", "-c:v", "libvpx-vp9", "-crf", "33", "-b:v", "0", "output.webm"]);
// Leer la salida y crear una URL de descarga
const data = await ffmpeg.readFile("output.webm");
const blob = new Blob([data as Uint8Array], { type: "video/webm" });
const url = URL.createObjectURL(blob);
setOutputUrl(url);
setProgress(null);
}
return (
<div className="space-y-4 p-6 border rounded-lg">
<h2 className="text-xl font-bold">Convertidor de video (MP4 → WebM)</h2>
<input
type="file"
accept="video/mp4"
onChange={handleFileChange}
disabled={loading || progress !== null}
className="block"
/>
{loading && (
<p className="text-sm text-muted-foreground">Cargando FFmpeg...</p>
)}
{progress !== null && (
<div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm mt-1">{progress}%</p>
</div>
)}
{outputUrl && (
<div className="space-y-2">
<video src={outputUrl} controls className="w-full max-w-lg" />
<a
href={outputUrl}
download="output.webm"
className="inline-block px-4 py-2 bg-primary text-primary-foreground rounded"
>
Descargar
</a>
</div>
)}
</div>
);
}
Algunas cosas a destacar sobre esta implementación. La instancia de FFmpeg vive en un useRef — no en useState — porque no quieres que React la recree en cada renderizado. Cargar el binario WASM es costoso, así que quieres hacerlo una vez y reutilizar la misma instancia.
La llamada a URL.createObjectURL crea una URL de Blob que debería revocarse cuando ya no se necesite. Para código de producción, llama a URL.revokeObjectURL(url) en una función de limpieza de useEffect para evitar fugas de memoria.
Limitaciones y rendimiento
Velocidad de procesamiento:
ffmpeg.wasm es notablemente más lento que FFmpeg nativo. Un benchmark aproximado: convertir un MP4 de 5 minutos a WebM que toma 30 segundos de forma nativa podría tomar 3–10 minutos en el navegador, dependiendo de la CPU del dispositivo. Este es un costo inherente del sandbox WASM.
Disponibilidad de códecs:
La compilación por defecto de ffmpeg.wasm incluye un conjunto limitado de códecs. Los códecs con complicaciones de patentes — como la codificación H.265 (HEVC) o la codificación AV1 — pueden no estar disponibles en la compilación estándar. Los casos de uso comunes (decodificación H.264, codificación VP9, conversión de formato) funcionan bien.
Compatibilidad de navegadores:
Cualquier navegador moderno que soporte WebAssembly y SharedArrayBuffer funcionará: Chrome 92+, Firefox 79+, Safari 15.2+. Consulta la tabla de Can I Use SharedArrayBuffer para los datos de compatibilidad más actualizados. IE no está soportado, pero en 2026 eso rara vez es una preocupación.
Modelo de hilos:
ffmpeg.wasm usa Web Workers internamente, así que las conversiones no bloquean el hilo principal — tu interfaz se mantiene responsiva durante el procesamiento. El callback de progreso se dispara desde dentro del Worker, así que puedes manejar una barra de progreso sin ninguna lógica extra de hilos de tu lado.
Preguntas frecuentes (FAQ)
¿ffmpeg.wasm es gratis para proyectos comerciales?
Sí. ffmpeg.wasm en sí tiene licencia MIT. Sin embargo, las bibliotecas subyacentes de FFmpeg llevan licencias LGPL/GPL dependiendo de qué códecs estén habilitados. Para uso comercial, revisa la guía de licencias de FFmpeg para entender tus obligaciones.
¿Cuánto pesa la descarga del binario WASM?
El binario WASM principal pesa aproximadamente 25–30 MB. Se descarga una vez y puede cachearse por el navegador o un service worker. En visitas posteriores, la carga es casi instantánea.
¿Puedo usar ffmpeg.wasm con Vue, Svelte o JavaScript puro?
Sí. El componente React en este artículo es solo un ejemplo. ffmpeg.wasm es una biblioteca JavaScript pura — funciona con cualquier framework o sin ninguno. La API principal (new FFmpeg(), load(), exec(), readFile(), writeFile()) es agnóstica al framework.
¿ffmpeg.wasm soporta codificación H.265 (HEVC)?
La compilación por defecto no incluye codificación H.265 debido a problemas de licencias de patentes. La decodificación H.264 y la codificación VP9/VP8 sí están disponibles. Si necesitas H.265, tendrías que compilar un binario WASM personalizado con las flags de códec apropiadas — lo que también implica asumir la responsabilidad de licenciamiento.
¿Puedo procesar múltiples archivos al mismo tiempo?
Una sola instancia de FFmpeg procesa un archivo a la vez. Para manejar múltiples archivos, puedes ponerlos en cola secuencialmente o crear múltiples instancias de FFmpeg — aunque ten en cuenta que cada instancia consume su propia memoria para el sistema de archivos virtual. Para flujos de procesamiento por lotes en el servidor, consulta Automatiza el procesamiento de video con FFmpeg y Python.
¿Por qué ffmpeg.wasm es mucho más lento que FFmpeg nativo?
WebAssembly se ejecuta en un entorno sandbox sin acceso directo a la aceleración por hardware. FFmpeg nativo puede usar codificación GPU (NVENC/QSV) e instrucciones SIMD que WASM no puede aprovechar completamente. Espera un procesamiento 5–20 veces más lento comparado con nativo, dependiendo del códec y el dispositivo.
¿Necesito headers COOP/COEP para la compilación de un solo hilo?
No. La compilación de un solo hilo (@ffmpeg/core) no usa SharedArrayBuffer y funciona sin headers de aislamiento de origen cruzado. Es más lenta porque no puede usar múltiples núcleos de CPU, pero es la forma más fácil de empezar sin tocar la configuración del servidor.
¿ffmpeg.wasm funciona sin conexión?
Sí, una vez que el binario WASM está cacheado (ya sea por la caché HTTP del navegador o un service worker), ffmpeg.wasm funciona completamente sin conexión. No se hacen solicitudes de red durante la conversión — todo el procesamiento ocurre localmente.
Conclusión
ffmpeg.wasm abre la puerta a herramientas de procesamiento de video sin servidor construidas completamente en el frontend.
Puntos clave:
- Usa la API v0.12+: Importa la clase
FFmpege instancia connew FFmpeg()— no la antigua factorycreateFFmpeg() - Configura los headers COOP/COEP: El aislamiento de origen cruzado es necesario para SharedArrayBuffer; configúralo en
next.config.tsantes de hacer cualquier otra cosa - Protege contra archivos grandes: Los archivos de más de 1 GB pueden agotar la memoria del navegador — agrega una verificación de tamaño en tu interfaz
- Mantén la instancia de FFmpeg en un ref: Usa
useRefpara mantenerla entre renderizados y evitar pagar el costo de carga WASM más de una vez - Establece expectativas sobre la velocidad: El procesamiento es más lento que nativo; una barra de progreso hace la espera más llevadera
Para conversión de archivos de tamaño pequeño a mediano, generación de GIF y extracción de audio, ffmpeg.wasm entrega un rendimiento práctico. Si estás construyendo una herramienta centrada en la privacidad donde enviar archivos a un servidor no es una opción, o un servicio donde quieres cero costo de infraestructura por conversión, ffmpeg.wasm es una de las herramientas más interesantes del ecosistema frontend ahora mismo.
Artículos relacionados:
- Comandos FFmpeg: Guía práctica de lo básico a lo avanzado
- La guía completa de compresión de video con FFmpeg
- Transmite video HLS con FFmpeg y un CDN
- ¿FFmpeg es muy difícil? Alternativas GUI comparadas
- Automatiza el procesamiento de video con FFmpeg y Python
- Codificación GPU con FFmpeg: Guía NVENC y QSV
- Guía de licencia comercial de FFmpeg