FFmpeg es una herramienta de línea de comandos escrita en C que lleva décadas siendo el estándar para trabajar con vídeo y audio. Hasta hace poco, si querías usarlo en una aplicación web tenías dos opciones: procesarlo en el servidor o pedirle al usuario que lo instalara. Las dos tienen sus inconvenientes.
WebAssembly cambió esto. Permite compilar código C y C++ a un bytecode que el navegador ejecuta a velocidad cercana a la nativa. FFmpeg está escrito en C, así que con Emscripten se puede compilar directamente a WASM. FFmpeg.wasm es exactamente eso: el FFmpeg de siempre, empaquetado para que lo ejecute el navegador, con bindings en JavaScript para llamarlo desde tu código.
El resultado es que puedes convertir formatos, extraer fotogramas o recortar clips directamente en el cliente, sin que un solo byte del vídeo salga del dispositivo del usuario.
Instalación y requisitos previos
El paquete está en npm y la instalación es directa:
npm install @ffmpeg/ffmpeg @ffmpeg/util
Pero hay un requisito que mucha gente se salta y luego no entiende por qué el rendimiento es penoso: SharedArrayBuffer. FFmpeg.wasm usa multihilo a través de Web Workers y SharedArrayBuffer. El navegador solo permite esto cuando el contexto está aislado de forma segura, lo que se consigue añadiendo dos cabeceras HTTP a tu servidor:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Sin estas cabeceras, el navegador desactiva SharedArrayBuffer y FFmpeg.wasm cae a modo de un solo hilo. La conversión sigue funcionando, pero es mucho más lenta. Si usas Vite, puedes añadirlas en el plugin de desarrollo; si sirves desde Express o Nginx, las configuras en las cabeceras de respuesta.
Primer ejemplo: convertir MP4 a WebM
El flujo básico siempre sigue el mismo patrón: crear la instancia, cargar el módulo WASM, escribir el fichero en el sistema de ficheros virtual de FFmpeg, ejecutar el comando y leer el resultado.
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';
const ffmpeg = new FFmpeg();
await ffmpeg.load();
// Escribir el fichero de entrada en el FS virtual
const inputFile = await fetchFile('video.mp4');
await ffmpeg.writeFile('input.mp4', inputFile);
// Ejecutar la conversión
await ffmpeg.exec(['-i', 'input.mp4', 'output.webm']);
// Leer el resultado
const data = await ffmpeg.readFile('output.webm');
// Crear una URL para reproducir el vídeo convertido
const url = URL.createObjectURL(
new Blob([data.buffer], { type: 'video/webm' })
);
document.getElementById('player').src = url;
El array que pasas a exec() funciona exactamente igual que los argumentos de la línea de comandos de FFmpeg. Si sabes usar FFmpeg en la terminal, ya sabes usarlo aquí.
Ejemplo práctico: extraer un fotograma del vídeo
Uno de los casos de uso más útiles es generar una miniatura antes de subir el vídeo al servidor. En vez de subir el vídeo completo, pedir al servidor que extraiga un frame y devolvérselo al cliente, puedes hacer todo en el navegador:
await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
await ffmpeg.exec([
'-i', 'input.mp4',
'-ss', '00:00:05', // segundo 5
'-frames:v', '1', // un solo fotograma
'thumbnail.png'
]);
const data = await ffmpeg.readFile('thumbnail.png');
const url = URL.createObjectURL(
new Blob([data.buffer], { type: 'image/png' })
);
document.getElementById('preview').src = url;
El resultado es un PNG que puedes mostrar como previsualización, enviarlo al servidor junto con el vídeo o simplemente usarlo en la interfaz antes de que el usuario confirme la subida.
Mostrar el progreso al usuario
La conversión de vídeo tarda. En el navegador, si no muestras ningún indicador, el usuario pensará que la página se ha colgado. FFmpeg.wasm emite eventos de progreso que puedes escuchar antes de llamar a exec():
ffmpeg.on('progress', ({ progress, time }) => {
const porcentaje = Math.round(progress * 100);
document.getElementById('barra').style.width = porcentaje + '%';
document.getElementById('estado').textContent = porcentaje + '%';
});
await ffmpeg.exec(['-i', 'input.mp4', 'output.webm']);
El campo progress va de 0 a 1 y time indica el tiempo del vídeo procesado en microsegundos. Con eso puedes construir una barra de progreso o mostrar cuántos segundos van procesados sobre el total. No es opcional: sin feedback visual, la experiencia es horrible.
Limitaciones reales que hay que tener en cuenta
FFmpeg.wasm funciona bien para casos concretos, pero tiene límites claros que conviene conocer antes de meter la pata.
Velocidad
Es entre 5 y 10 veces más lento que FFmpeg nativo ejecutándose en el servidor. Para vídeos de menos de un minuto o para operaciones ligeras como extraer un frame o recortar unos segundos, el tiempo sigue siendo asumible. Para un vídeo de 20 minutos, el usuario va a esperar mucho.
Memoria
El vídeo tiene que caber entero en la memoria del navegador. Los navegadores de escritorio modernos tienen bastante margen, pero un vídeo 4K largo puede fácilmente superar lo que el navegador puede manejar y provocar un crash de la pestaña. Hay que ser realista con el tipo de ficheros que va a manejar tu aplicación.
Safari
El soporte de SharedArrayBuffer en Safari ha mejorado, pero sigue siendo menos predecible que en Chrome o Firefox. Si tu audiencia usa mucho Safari, testa en dispositivos reales antes de desplegar.
Cuándo no usarlo
Si el vídeo tiene más de unos pocos minutos, si la velocidad de procesamiento es importante o si los usuarios suben contenido desde dispositivos con poca RAM, lo mejor es procesar en el servidor. No tiene sentido forzar FFmpeg.wasm donde no encaja.
Cuándo sí tiene sentido usarlo
Con las limitaciones claras, los casos donde FFmpeg.wasm brilla son bastante concretos:
- Previsualización antes de subir: el usuario graba o selecciona un clip corto, lo recortas o ajustas en el cliente y solo subes el resultado final al servidor. Ahorras ancho de banda y el servidor no toca ficheros temporales.
- Herramientas de edición ligera en el navegador: recortar, silenciar audio, cambiar formato. Para operaciones simples sobre vídeos cortos, es una solución limpia.
- Generación de thumbnails: extraer el frame que el usuario prefiere sin necesidad de una petición al servidor.
- Apps offline-first: si la aplicación tiene que funcionar sin conexión, no puedes enviar nada a un servidor de transcodificación. FFmpeg.wasm permite mantener la funcionalidad aunque el usuario no tenga red.
Alternativas para cuando FFmpeg.wasm no llega
Si las limitaciones del cliente no se ajustan a tu caso, hay otras vías bien establecidas.
FFmpeg en el servidor con Node.js
El paquete fluent-ffmpeg para Node.js para procesar vídeo en el servidor da acceso completo a FFmpeg sin los límites de memoria ni la penalización de velocidad de WASM. Es la opción natural cuando el procesamiento es pesado o los vídeos son largos.
APIs de transcodificación en la nube
Servicios como Mux o Cloudflare Stream reciben el vídeo y se encargan de convertirlo en sus servidores. Son de pago, pero eliminan la complejidad de gestionar tu propia infraestructura de vídeo.
WebCodecs API
Es una API nativa del navegador, disponible en Chrome y Edge, que da acceso de bajo nivel a codecs de vídeo y audio. No tiene ni de lejos la flexibilidad de FFmpeg, pero para casos sencillos como decodificar frames o reempaquetar sin recodificar es mucho más eficiente que pasar por WASM. Vale la pena conocerla si solo necesitas operaciones básicas.
Un proyecto que lleva tiempo madurando
JavaScript y WebAssembly: la evolución del navegador han abierto la puerta a llevar al cliente herramientas que antes eran territorio exclusivo del servidor. FFmpeg.wasm es un buen ejemplo de hasta dónde ha llegado esto: una herramienta de procesamiento de vídeo con treinta años de historia funcionando dentro de una pestaña del navegador.
No es la solución para todo. Pero para los casos donde encaja vídeos cortos, generación de previsualizaciones, edición ligera o apps sin conexión, es una opción sólida que evita montar infraestructura de servidor y mantiene los datos del usuario en su propio dispositivo.
La documentación oficial está en ffmpegwasm.netlify.app y los ejemplos son bastante claros para empezar. El repositorio en GitHub tiene también varios demos interactivos donde puedes probar la conversión directamente en el navegador antes de escribir una línea de código.
Imagen: Pexels / Markus Spiske
