Extraer miniaturas de videos en navegadores con ffmpeg.wasm
Las miniaturas de video son esenciales para las aplicaciones web modernas: ofrecen a los usuarios una vista previa visual rápida y les ayudan a decidir si quieren interactuar con tu contenido. Con el auge de las potentes herramientas de WebAssembly (Wasm), ahora puedes crear esas miniaturas por completo en el navegador, manteniendo en local los archivos multimedia de los usuarios y reduciendo la carga del servidor.
¿Por qué extraer miniaturas en el navegador?
Extraer miniaturas del lado del cliente ofrece varias ventajas:
- Menor carga del servidor: el trabajo de codificación se realiza en el dispositivo del usuario, lo que libera recursos del back-end.
- Retroalimentación local: los usuarios pueden previsualizar los resultados sin subir el video a tu servidor.
- Mayor privacidad: los videos nunca salen del navegador, lo que resulta especialmente útil para contenido sensible o cuando hay que cumplir normativas de privacidad.
Conoce ffmpeg.wasm
FFmpeg.wasm es una adaptación a WebAssembly del popular conjunto de herramientas FFmpeg. Expone en JavaScript una API similar a la de línea de comandos y se ejecuta por completo dentro de los navegadores modernos.
Características principales:
- Incluye filtros y códecs de FFmpeg compilados en el core seleccionado.
- Ofrece cores de un solo hilo y de múltiples hilos.
- El wrapper de JavaScript está bajo licencia MIT; el core y los códecs incluidos tienen sus propias licencias.
Instala e inicializa
npm install @ffmpeg/ffmpeg@0.12.15 @ffmpeg/util@0.12.2
// Use a browser bundler that supports the package's module worker.
import { FFmpeg } from '@ffmpeg/ffmpeg'
import { fetchFile, toBlobURL } from '@ffmpeg/util'
const ffmpeg = new FFmpeg()
let loading
let busy = false
export async function loadFFmpeg() {
if (ffmpeg.loaded) return
if (!loading) {
loading = (async () => {
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.10/dist/esm'
const coreURL = await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript')
try {
const wasmURL = await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm')
try {
await ffmpeg.load({ coreURL, wasmURL })
} finally {
URL.revokeObjectURL(wasmURL)
}
} finally {
URL.revokeObjectURL(coreURL)
}
})().finally(() => {
loading = undefined
})
}
await loading
}
Llama a loadFFmpeg() de forma diferida cuando el usuario abra un diálogo de
subida. Esto carga el core de un solo hilo, que ocupa decenas de megabytes. El wrapper ya ejecuta
FFmpeg dentro de un web worker. Sirve la aplicación por HTTPS o localhost y asegúrate de que su
política de seguridad de contenido permita los workers, Wasm y las URL de recursos que utilices.
Consulta los ejemplos de carga oficiales.
Cumple los requisitos del navegador
El @ffmpeg/core de un solo hilo que se usó arriba necesita compatibilidad con
WebAssembly y workers; no requiere SharedArrayBuffer. Si cambias explícitamente a
@ffmpeg/core-mt, proporciona su recurso adicional de worker y habilita el aislamiento
de origen cruzado para los hilos de WebAssembly:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Si utilizas un service worker o tu propio CDN, asegúrate de que estas cabeceras se propaguen correctamente en cada salto.
Extrae una sola miniatura
Este ejemplo produce una miniatura PNG y libera sus archivos virtuales temporales después de cada llamada:
export async function extractThumbnail(videoFile, time = '00:00:01') {
if (busy) throw new Error('A thumbnail operation is already running')
busy = true
const id = crypto.randomUUID()
const input = `input-${id}.mp4`
const output = `thumb-${id}.png`
try {
await loadFFmpeg()
await ffmpeg.writeFile(input, await fetchFile(videoFile))
const code = await ffmpeg.exec([
'-ss',
time,
'-i',
input,
'-map',
'0:v:0',
'-frames:v',
'1',
output,
])
if (code !== 0) throw new Error(`FFmpeg failed with exit code ${code}`)
// Seeking past the end may return success without creating an image.
const data = await ffmpeg.readFile(output)
if (!(data instanceof Uint8Array) || data.length === 0) {
throw new Error('No thumbnail was produced')
}
return new Blob([data], { type: 'image/png' })
} finally {
await Promise.allSettled([ffmpeg.deleteFile(input), ffmpeg.deleteFile(output)])
busy = false
}
}
Ejemplo de uso con manejo de errores:
try {
const blob = await extractThumbnail(file, '00:00:05')
const url = URL.createObjectURL(blob)
thumbnailImg.onload = thumbnailImg.onerror = () => URL.revokeObjectURL(url)
thumbnailImg.src = url
} catch (err) {
console.error('Thumbnail extraction failed', err)
if (err.message && err.message.includes('load() failed')) {
console.error(
'FFmpeg failed to load. This might be due to browser compatibility or network issues.',
)
} else if (err.message && err.message.includes('SharedArrayBuffer')) {
console.error(
'SharedArrayBuffer is not available. Ensure Cross-Origin Isolation headers are set.',
)
}
}
Obtén varias miniaturas de forma secuencial
Reutiliza la misma instancia de FFmpeg sin volver a cargar Wasm. Esta sencilla función auxiliar espera cada extracción; escribe y elimina la entrada para cada marca de tiempo. Desactiva las acciones superpuestas mientras se ejecuta:
export async function extractThumbnails(videoFile, marks = ['00:00:01', '00:00:05']) {
const thumbnails = []
for (const mark of marks) thumbnails.push(await extractThumbnail(videoFile, mark))
return thumbnails
}
Mantén la interfaz fluida con un web worker
@ffmpeg/ffmpeg crea su propio worker, así que en este ejemplo no hace falta un worker envoltorio
adicional. Espera sus métodos asíncronos, desactiva los clics repetidos durante la extracción y
muestra un estado de carga mientras el core se descarga o procesa un archivo. La ejecución en el
worker mantiene disponible el hilo de la interfaz, pero la presión sobre la CPU y la memoria aún
puede afectar la fluidez.
Consejos de rendimiento
- Caché de recursos: almacena en caché los recursos versionados del core para reducir el tiempo de las descargas repetidas.
- Carga diferida: importa la biblioteca y llama a
loadFFmpeg()solo cuando el usuario interactúe con una función que lo requiera, como seleccionar un archivo de video. - Limita el tamaño de los archivos: los videos 4K grandes pueden superar la memoria del navegador o tardar demasiado en procesarse. Considera limitar las subidas a un tamaño razonable, por ejemplo, 200 MB.
- Reutiliza una sola instancia: crear varias instancias de FFmpeg desperdicia memoria y ralentiza el procesamiento.
- Recurre al servidor: si el core seleccionado no puede cargarse o un archivo supera los límites del dispositivo, ofrece procesamiento del lado del servidor con el consentimiento del usuario para subir el video.
Procesamiento en el navegador frente al servidor
| Aspecto | Navegador (FFmpeg.wasm) | Servidor (p. ej., Transloadit) |
|---|---|---|
| Latencia | Depende de la descarga y del dispositivo | Ida y vuelta + tiempo en cola |
| Privacidad | Los archivos nunca salen del dispositivo | Requiere subida y almacenamiento |
| Escalabilidad | Limitada por el hardware del usuario | Prácticamente ilimitada |
| Esfuerzo de implementación | Biblioteca JS + cabeceras | Llamada a la API |
| Uso de batería en móviles | Alto | Bajo |
| Compatibilidad | Navegadores modernos con funciones específicas | Universal |
Usa el modelo que mejor se adapte a tu producto, o combina ambos para obtener una solución robusta.
Soluciona problemas comunes
SharedArrayBufferno está disponible con el core multihilo: revisa con atención tus cabecerasCross-Origin-Embedder-Policy: require-corpyCross-Origin-Opener-Policy: same-origin. Asegúrate de que se apliquen correctamente a la página que sirve FFmpeg.wasm.RangeError: Out of memory: el video puede ser demasiado grande o complejo para la memoria disponible del navegador. Intenta recortar el video o reducir su resolución antes de procesarlo con FFmpeg.wasm, o recurre al procesamiento del lado del servidor.- Primera ejecución lenta: la descarga y la compilación iniciales de
ffmpeg-core.wasmpueden tardar. Implementa almacenamiento en caché con service worker y considera una llamada de «calentamiento» aloadFFmpeg()cuando la página se vuelva visible o esté inactiva, en lugar de esperar a una interacción directa del usuario. - Navegadores móviles: prueba la carga del core y archivos representativos en los dispositivos que admites. Los límites de memoria pueden variar considerablemente; no uses la detección por nombre de navegador como sustituto de probar las funciones necesarias.
Agiliza la producción con Transloadit
Cuando tu aplicación necesita procesar decenas de miles de videos al día, o debe ser compatible con todos los navegadores, nuestro 🤖 Robot /video/thumbs se encarga de la extracción de miniaturas por ti. Admite la extracción de miniaturas en paralelo, puede generar una cantidad personalizable de 1-999 miniaturas por video, permite marcas de tiempo personalizadas (mediante porcentaje o segundos), ofrece varios formatos de salida (JPEG, JPG, PNG) e incluye estrategias avanzadas de redimensionamiento como crop, fit, fillcrop, min_fit, pad y stretch. Junto con nuestro servicio de Encoding de video, escala automáticamente y nunca bloquea la interfaz.
Próximos pasos
Experimenta con FFmpeg.wasm en local, almacena en caché el core de Wasm para una experiencia de
usuario ágil y decide en qué punto tiene sentido para tu proyecto el equilibrio entre navegador y
servidor. Si superas los límites del lado del cliente, una Assembly que use el Robot /video/thumbs está a solo
una llamada a la API de distancia.
