DojoClip

Comenzar

FFmpeg en el navegador: lanzamiento de Render Lab con DojoClip (análisis técnico)

By: Pansa LegrandDate: 2025-02-18Category: Engineering
Diagrama del flujo de FFmpeg WebAssembly dentro de DojoClip

Bienvenido a Render Lab, la serie en la que desglosamos la tecnología que mantiene a DojoClip veloz, privada y pensada para creadores. Este artículo va más allá del discurso general y entra en el cómo: FFmpeg compilado a WebAssembly (WASM), dónde brilla, dónde sufre y cómo lo combinamos con MediaRecorder y WebCodecs para crear un flujo híbrido de baja latencia.


TL;DR

  • FFmpeg WASM iguala precisión y cobertura de funciones (filtros, recortes, remux) con privacidad local.
  • WebCodecs ofrece la codificación y decodificación de menor latencia con ayuda de hardware, pero necesita un muxer.
  • MediaRecorder es la captura en tiempo real más sencilla, ideal para previsualizaciones con control limitado.
  • Trabajamos con un pipeline híbrido: WASM para transformaciones exactas y WebCodecs o MediaRecorder para previsualizaciones y exportaciones rápidas.

Conceptos (resumen en 2 minutos)

  • Contenedor vs. códec: MP4, MKV y WebM son contenedores; H.264, H.265, VP9, AV1, AAC y Opus son códecs. Editar suele implicar decodificar, filtrar, codificar y luego multiplexar.
  • Transcodificar vs. remultiplexar: La transcodificación vuelve a codificar (compromiso entre calidad y tamaño). El remux cambia el contenedor sin tocar los streams comprimidos (rápido y sin pérdidas).
  • CFR vs. VFR: Velocidad de fotogramas constante vs. variable. La captura web suele ser VFR; la edición puede preferir CFR para búsquedas cuadro a cuadro.
  • Fotogramas clave (IDR): Los cortes se alinean a fotogramas clave salvo que transcodifiques o apliques estrategias de smart-render.
  • CRF y bitrate: Controles de calidad. Un CRF menor aumenta calidad y tamaño. El bitrate limita el ancho de banda para objetivos de streaming.

Arquitectura en un vistazo

[Entrada de archivos]
   └─▶ Sistema de archivos del navegador (OPFS / RAM)
         ├─▶ Worker FFmpeg WASM (filtros precisos, remux, waveform)
         ├─▶ WebCodecs (decodificación/codificación rápida; preview/export)
         └─▶ MediaRecorder (captura de canvas/pestaña en tiempo real)
                     ▼
               [Muxer] → MP4/WebM → Descarga / OPFS / Subida

¿Por qué WASM?

Trae la mayor parte del CLI de FFmpeg al navegador. Eso habilita recortes cuadro a cuadro, grafos de filtros complejos y operaciones de canales de audio manteniendo el procesamiento local para medios sensibles.

¿Por qué no usar solo WASM?

Las codificaciones largas presionan memoria y CPU, la carga inicial pesa varios megabytes y los threads o SIMD requieren cabeceras de aislamiento de origen cruzado.

¿Por qué también WebCodecs y MediaRecorder?

WebCodecs aprovecha los codificadores y decodificadores de la plataforma para ganar velocidad. MediaRecorder es trivial para capturas en vivo y proxies rápidos.


Desplegar FFmpeg WASM correctamente

1. Activa threads y SIMD (ganancias reales)

Threads más SIMD suelen aportar mejoras de 1,5 a 3 veces según la carga. Requieren cabeceras de aislamiento de origen cruzado.

// next.config.js (o middleware/headers)
const securityHeaders = [
  { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
  { key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
  { key: 'Cross-Origin-Resource-Policy', value: 'same-site' },
];

module.exports = {
  async headers() {
    return [{ source: '/:path*', headers: securityHeaders }];
  },
};

2. Carga el core en un worker

Evita bloquear el hilo principal y mantén aislada la memoria de FFmpeg.

// ffmpeg.worker.ts
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({ log: true, corePath: '/wasm/ffmpeg-core.js' });

self.onmessage = async (event) => {
  const { name, file, args } = event.data;
  if (!ffmpeg.isLoaded()) {
    await ffmpeg.load();
  }
  ffmpeg.FS('writeFile', name, await fetchFile(file));
  await ffmpeg.run(...args);
  const out = ffmpeg.FS('readFile', 'out.bin');
  self.postMessage({ ok: true, data: out.buffer }, [out.buffer]);
};
// Lado UI
const worker = new Worker(new URL('./ffmpeg.worker.ts', import.meta.url));
worker.postMessage({
  name: 'in.mp4',
  file,
  args: ['-i', 'in.mp4', '-vn', '-acodec', 'copy', 'out.bin'],
});

worker.onmessage = ({ data }) => {
  const blob = new Blob([data.data], { type: 'audio/mp4' });
  download(blob, 'audio.m4a');
};

3. Guarda los intermedios grandes en OPFS

El Origin Private File System evita picos de RAM.

const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('clip.m4a', { create: true });
const writer = await fileHandle.createWritable();
await writer.write(blob);
await writer.close();

Tareas cotidianas (recetas FFmpeg listas para copiar)

Extracción de audio (bit a bit si ya es AAC):

ffmpeg -i input.mp4 -vn -acodec copy audio.m4a

Redimensionar y transcodificar con CRF (proxy H.264):

ffmpeg -i input_4k.mp4 -vf scale=-2:1080 -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 160k out_1080p.mp4

Segmento cuadro a cuadro (re-codifica una ventana pequeña):

ffmpeg -ss 00:00:12.300 -to 00:00:19.000 -i input.mp4 -c:v libx264 -crf 20 -pix_fmt yuv420p -c:a aac slice.mp4

Waveform PNG para una timeline:

ffmpeg -i audio.m4a -lavfi showwavespic=s=1200x200:colors=white waveform.png

En WASM pasas los mismos argumentos a ffmpeg.run(...). Para remux puro (sin transcodificar) conserva -c copy para mantener calidad y velocidad.


WebCodecs: autopista rápida

WebCodecs ofrece acceso directo a los decodificadores y codificadores de la plataforma. Necesitas un muxer para envolver los chunks codificados en MP4 o WebM.

const fps = 30;
const encoder = new VideoEncoder({
  output: handleChunk,
  error: console.error,
});

encoder.configure({
  codec: 'avc1.42E01E',
  width: canvas.width,
  height: canvas.height,
  bitrate: 3_000_000,
  framerate: fps,
});

let t0 = performance.now();
let frameIndex = 0;
const track = canvas.captureStream(fps).getVideoTracks()[0];
const reader = new MediaStreamTrackProcessor({ track }).readable.getReader();

async function pump() {
  const { value: frame, done } = await reader.read();
  if (done) {
    await encoder.flush();
    muxer.finalize();
    return;
  }
  const timestamp = Math.floor((performance.now() - t0) * 1000);
  const videoFrame = new VideoFrame(frame, { timestamp });
  encoder.encode(videoFrame, { keyFrame: frameIndex % (fps * 2) === 0 });
  frameIndex += 1;
  videoFrame.close();
  frame.close();
  pump();
}

function handleChunk(chunk) {
  muxer.addVideoChunk(chunk);
}

Nota de muxing: WebCodecs entrega streams elementales. Usa una librería de muxer en el navegador para generar un archivo descargable. Para previews, Media Source Extensions puede transmitir chunks a una etiqueta video.

Comparado con FFmpeg WASM, la latencia de codificación suele ser menor y el uso de CPU más bajo, especialmente en dispositivos con aceleración por hardware.


MediaRecorder: proxies sin complicaciones

const stream = canvas.captureStream(30);
const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9' });
const chunks = [];

recorder.ondataavailable = (event) => {
  if (event.data.size) {
    chunks.push(event.data);
  }
};

recorder.onstop = () => {
  const blob = new Blob(chunks, { type: recorder.mimeType });
  download(blob, 'preview.webm');
};

recorder.start();
// ... renderiza frames ...
recorder.stop();

Pros: Muy sencillo y perfecto para revisiones rápidas o proxies para redes sociales. Contras: Control limitado sobre GOP, CRF o filtros avanzados.


Cómo elegir el pipeline correcto

Tarea Precisión requerida Latencia objetivo Recomendado
Extracción de audio, remux Alta Baja FFmpeg WASM (-c copy)
Waveform o miniaturas Alta Baja FFmpeg WASM (filtros)
Cortes cuadro a cuadro Alta Media FFmpeg WASM (re-codifica ventana pequeña)
Previsualización en vivo o captura de pantalla Media Muy baja MediaRecorder
Exportaciones finales desde timeline en canvas Media Baja WebCodecs + muxer
Transcodificaciones largas offline Media Alta Fallback nativo o servidor

Matemática de memoria (planifica antes de renderizar)

  • Un frame de video decodificado equivale a ancho por alto por 1,5 bytes (YUV420p). Ejemplo: 1920x1080 son unos 3,1 MB por frame. Un buffer pequeño de 120 frames ronda los 372 MB.
  • La heap de WASM puede dispararse durante filtros como remuestreadores o escaladores. Guarda intermedios en disco (OPFS) y haz streaming cuando sea posible.
  • Audio: PCM estéreo de 48 kHz y 16 bits ocupa aproximadamente 192 KB por segundo. Cinco minutos equivalen a unos 57 MB si lo mantienes descomprimido en memoria.

Consejos prácticos

  • Prefiere el remux (-c copy) frente a una transcodificación completa cuando puedas.
  • Divide trabajos largos en segmentos, persiste en OPFS y concatena con FFmpeg.
  • Para previews, usa WebCodecs y evita mantener demasiados frames sin comprimir en memoria JavaScript.

Benchmarks reproducibles (completa antes de publicar)

Ejecuta estas pruebas en un build de producción limpio. Registra dispositivo y versión de navegador.

  1. Extracción de audio (remux)

    • Entrada: MP4 (H.264 + AAC), 1 a 2 minutos.
    • Comando: -i in.mp4 -vn -acodec copy out.m4a
    • Métricas: Tiempo total (ms), pico de heap JS (MB), heap WASM (MB), tamaño de salida.
  2. Transcodificación proxy (4K a 1080p)

    • Comando: -vf scale=-2:1080 -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 160k
    • Compara: FFmpeg WASM vs. WebCodecs con bitrate y fps similares. Registra tiempo, porcentaje de CPU y frames perdidos.
  3. Export desde timeline en canvas

    • Método A: WebCodecs + muxer MP4.
    • Método B: Captura con MediaRecorder a 30 fps.
    • Métricas: FPS de codificación, tiempo total, bitrate/tamaño de salida, calidad visual (SSIM/PSNR si puedes compararlo con un original).

Tabla plantilla (reemplaza N/A con valores medidos):

Dispositivo / Navegador Prueba Pipeline Tiempo (s) CPU promedio (%) Pico de memoria (MB) Notas
M2 Pro / Chrome 128 Extracción audio FFmpeg WASM Threads + SIMD
M2 Pro / Chrome 128 4K a 1080p FFmpeg WASM Threads + SIMD
M2 Pro / Chrome 128 4K a 1080p WebCodecs H.264 hardware
Pixel 8 / Chrome Export canvas MediaRecorder VP9

Ayudas de medición

const perf = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.duration);
  }
});

perf.observe({ entryTypes: ['measure'] });
const memory = performance.memory; // Solo Chrome: usedJSHeapSize y totalJSHeapSize

Cómo manejar errores comunes

  • Exportaciones en cámara lenta: Asegura timestamps crecientes al usar WebCodecs o codifica CFR a fps fijo. No dependas solo de requestAnimationFrame.
  • Desfase A/V: Usa el audio como fuente de tiempo. Alinea el PTS de video a la línea temporal derivada del audio o remuestrea el audio.
  • Frames verdes o variaciones de color: Asegura el formato de píxel (por ejemplo, RGBA a I420) y la información de espacio de color al multiplexar.
  • Gaps en Safari: El soporte de WebCodecs es parcial; recurre a MediaRecorder o WASM con H.264.
  • Threads deshabilitados: Si SharedArrayBuffer no está disponible (sin COOP/COEP), carga el core WASM monohilo para evitar errores en tiempo de ejecución.

Nuestro híbrido en DojoClip (hoy)

  • FFmpeg WASM para extracción de audio, waveforms/miniaturas, recortes precisos, remux y preparación de subtítulos.
  • WebCodecs para previsualizaciones y exportaciones finales desde canvas cuando el navegador lo soporta.
  • MediaRecorder para proxies instantáneos, videos de compartido rápido y demos con captura de pestañas.
  • Almacenamiento: OPFS para intermedios y exportaciones reanudables; memoria para trabajos pequeños.

Publicaremos una segunda parte con tablas de benchmarks reales en múltiples dispositivos y navegadores una vez fijemos los parámetros de codificación actuales.


Apéndice A — valores seguros

  • H.264 (web): -preset veryfast -crf 23 -maxrate 4M -bufsize 8M -pix_fmt yuv420p
  • Audio: -c:a aac -b:a 160k (música), -b:a 96k (voz predominante)
  • Buscar y luego transcodificar (más rápido): -ss antes de -i input ... cuando no necesitas precisión cuadro a cuadro; de lo contrario -i input -ss después.
  • Subtítulos: Mantén texto como archivos .srt o .vtt cuando sea posible; quema subtítulos solo en entregables finales.

Apéndice B — mapa de capacidades (parcial)

Capacidad FFmpeg WASM WebCodecs MediaRecorder
Grafos de filtros exactos No No
Aceleración por hardware No (CPU)
Control preciso CFR/VFR Limitado
Captura en tiempo real No Sí (decode/encode)
Mux MP4 integrado No (requiere muxer) No (suele ser WebM)
Configuración más sencilla Media Media

¿Quieres probarlo?

Abre el Compresor de video, el Extractor de audio o el Estudio de subtítulos en DojoClip para ver esta pila en acción. Si eres desarrollador, clona nuestro harness de benchmarks (llega en el próximo post), ejecuta las pruebas y comparte tus métricas. Las agregaremos en una tabla pública para que los creadores tomen decisiones con datos.