FFmpeg dans le navigateur : lancement de Render Lab avec DojoClip (analyse technique)

Comment DojoClip exploite FFmpeg WebAssembly, MediaRecorder et WebCodecs pour les outils côté navigateur et les aperçus, et quand les exports complets passent par le service de rendu.

Pansa Legrandrender lab
Schéma du pipeline FFmpeg WebAssembly dans DojoClip

Bienvenue dans Render Lab, notre série dédiée aux technologies média derrière DojoClip. Cet article se concentre sur la pile côté navigateur utilisée par nos outils gratuits et nos flux d’aperçu : FFmpeg compilé en WebAssembly (WASM), ses points forts, ses limites, et la manière dont nous l’associons à MediaRecorder et WebCodecs. Les exports complets de projets passent désormais par le service de rendu DojoClip.


TL;DR

  • FFmpeg WASM rime avec précision et couverture fonctionnelle (filtres, coupes, remux) tout en gardant les médias en local.
  • WebCodecs apporte la latence la plus faible avec assistance matérielle pour l’encodage et le décodage, mais nécessite un muxer.
  • MediaRecorder est la capture temps réel la plus simple, parfaite pour les aperçus avec un contrôle limité.
  • Nous utilisons un pipeline hybride : WASM pour les transformations exactes côté navigateur, WebCodecs et MediaRecorder pour les aperçus et exports légers côté navigateur, et le service de rendu pour les exports complets de projets.

Concepts (rappel en 2 minutes)

  • Conteneur vs codec : MP4, MKV et WebM sont des conteneurs ; H.264, H.265, VP9, AV1, AAC et Opus sont des codecs. L’édition implique souvent décoder, filtrer, encoder puis multiplexer.
  • Transcodage vs remux : le transcodage réencode (compromis qualité/taille). Le remux change de conteneur sans toucher aux flux compressés (rapide et sans perte).
  • CFR vs VFR : fréquence d’images constante vs variable. Les captures web sont souvent VFR ; le montage préfère parfois CFR pour des recherches frame-perfect.
  • Images clés (IDR) : les coupes s’alignent sur les keyframes sauf si vous transcodez ou appliquez des stratégies smart-render.
  • CRF et bitrate : boutons de qualité. Un CRF bas augmente qualité et taille. Le bitrate plafonne le débit pour le streaming.

Architecture en un coup d’œil

[Entrée fichier]
   └─▶ FS navigateur (OPFS / RAM)
         ├─▶ Worker FFmpeg WASM (filtres précis, remux, waveform)
         ├─▶ WebCodecs (decode/encode rapide ; preview/export léger)
         └─▶ MediaRecorder (capture canvas/onglet en temps réel)
                     ▼
               [Muxer] → MP4/WebM → Téléchargement / OPFS / Upload

Pourquoi WASM ?

Il apporte la majorité de la CLI FFmpeg dans le navigateur. Cela déverrouille des coupes au frame près, des graphes de filtres complexes et des opérations audio pour les outils gratuits et d’autres flux côté navigateur, tout en gardant ces tâches locales pour les médias sensibles.

Pourquoi pas uniquement WASM ?

Les encodages longs sollicitent mémoire et CPU, la première charge pèse plusieurs mégaoctets, et les threads ou SIMD exigent des en-têtes d’isolation cross-origin.

Pourquoi aussi WebCodecs et MediaRecorder ?

WebCodecs exploite les encodeurs/décodeurs natifs pour la vitesse. MediaRecorder est idéal pour les captures en direct et les proxies rapides.


Déployer FFmpeg WASM correctement

1. Activer threads et SIMD (gains concrets)

Threads + SIMD offrent souvent un gain de 1,5 à 3 selon la charge. Ils requièrent des en-têtes d’isolation cross-origin.

// next.config.js (ou 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. Charger le core dans un worker

Évitez de bloquer le thread principal et isolez la mémoire 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]);
};
// Côté 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. Stocker les intermédiaires volumineux dans OPFS

L’Origin Private File System évite les pics 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();

Tâches du quotidien (recettes FFmpeg prêtes à coller)

Extraction audio (bit exact si déjà AAC) :

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

Réduction + transcodage 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

Segment frame-perfect (petite fenêtre ré-encodée) :

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 pour timeline :

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

En WASM, passez les mêmes arguments à ffmpeg.run(...). Pour un remux pur (sans transcodage) gardez -c copy afin de préserver qualité et vitesse.


WebCodecs : voie express

WebCodecs offre un accès direct aux encodeurs et décodeurs natifs. Il faut un muxer pour encapsuler les chunks encodés en MP4 ou 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);
}

Remarque muxing : WebCodecs délivre des flux élémentaires. Utilisez un muxer côté navigateur pour produire un fichier téléchargeable. Pour les aperçus, Media Source Extensions peut diffuser les chunks vers une balise video.

Face à FFmpeg WASM, la latence d’encodage est souvent plus faible et l’usage CPU réduit, surtout sur les appareils bénéficiant de l’accélération matérielle.


MediaRecorder : proxies sans prise de tête

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();
// ... rendu des frames ...
recorder.stop();

Avantages : simplicité absolue, idéal pour les revues rapides ou proxies sociaux. Inconvénients : contrôle limité du GOP, du CRF ou des filtres avancés.


Choisir le bon pipeline

Tâche Précision requise Latence cible Recommandé
Extraction audio, remux Élevée Faible FFmpeg WASM (-c copy)
Waveform ou miniatures Élevée Faible FFmpeg WASM (filtres)
Coupes frame-perfect Élevée Moyenne FFmpeg WASM (petite ré-encodage)
Preview live ou capture écran Moyenne Très faible MediaRecorder
Export léger depuis canvas dans le navigateur Moyenne Faible WebCodecs + muxer
Export complet de projet DojoClip Élevée Moyenne Service de rendu DojoClip
Long transcodage offline Moyenne Élevée Fallback natif ou serveur

Calcul mémoire (prévoir avant de rendre)

  • Un frame vidéo décodé pèse environ largeur × hauteur × 1,5 octet (YUV420p). Exemple : 1920x1080 ≈ 3,1 Mo par frame. Un buffer de 120 frames ≈ 372 Mo.
  • La heap WASM peut grimper lors de filtres (resamplers, scalers). Stockez les intermédiaires sur disque (OPFS) et streamez quand possible.
  • Audio : PCM stéréo 48 kHz 16 bits ≈ 192 Ko/s. Cinq minutes ≈ 57 Mo si conservé non compressé en mémoire.

Astuces pratiques

  • Préférez le remux (-c copy) à un transcodage complet quand c’est possible.
  • Découpez les longs traitements, persistez-les dans OPFS, puis concaténez avec FFmpeg.
  • Pour les previews, privilégiez WebCodecs afin d’éviter de stocker trop de frames brutes en mémoire JavaScript.

Benchmarks reproductibles (à remplir avant publication)

Exécutez ces tests sur un build production propre. Notez appareil et version du navigateur.

  1. Extraction audio (remux)

    • Entrée : MP4 (H.264 + AAC), 1 à 2 minutes.
    • Commande : -i in.mp4 -vn -acodec copy out.m4a
    • Métriques : Temps total (ms), pic heap JS (Mo), heap WASM (Mo), taille de sortie.
  2. Transcodage proxy (4K → 1080p)

    • Commande : -vf scale=-2:1080 -c:v libx264 -preset veryfast -crf 23 -c:a aac -b:a 160k
    • Comparer : FFmpeg WASM vs WebCodecs à bitrate et fps proches. Notez temps, CPU %, frames perdues.
  3. Benchmark d’export léger depuis canvas

    • Méthode A : Encodage WebCodecs + muxer MP4.
    • Méthode B : Capture MediaRecorder @30 fps.
    • Métriques : FPS d’encodage, temps total, bitrate/taille, qualité visuelle (SSIM/PSNR si référence disponible).

Tableau gabarit (remplacez N/A par vos mesures) :

Appareil / Navigateur Test Pipeline Temps (s) CPU moy. (%) Pic mémoire (Mo) Notes
M2 Pro / Chrome 128 Extraction audio FFmpeg WASM Threads + SIMD
M2 Pro / Chrome 128 4K → 1080p FFmpeg WASM Threads + SIMD
M2 Pro / Chrome 128 4K → 1080p WebCodecs H.264 hardware
Pixel 8 / Chrome Export canvas MediaRecorder VP9

Outils de mesure

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; // Chrome uniquement : usedJSHeapSize et totalJSHeapSize

Gérer les pièges fréquents

  • Export en ralenti : fournissez des timestamps strictement croissants à WebCodecs ou encodez en CFR à fps fixe. Ne comptez pas uniquement sur requestAnimationFrame.
  • Décalage audio/vidéo : utilisez l’audio comme source temporelle. Alignez le PTS vidéo sur la timeline audio ou resamplez l’audio.
  • Frames vertes ou dérives colorimétriques : garantissez un format de pixel cohérent (ex. RGBA → I420) et renseignez l’espace couleur lors du muxing.
  • Lacunes sur Safari : WebCodecs y est partiel ; basculez sur MediaRecorder ou WASM H.264.
  • Threads indisponibles : si SharedArrayBuffer n’est pas accessible (pas de COOP/COEP), chargez le core WASM mono-thread pour éviter les erreurs.

Notre hybride chez DojoClip (état actuel)

  • FFmpeg WASM pour les outils gratuits côté navigateur comme l’extraction audio, les waveforms/miniatures, les coupes précises, le remux et la préparation de sous-titres.
  • WebCodecs pour les aperçus de timeline canvas et les exports légers côté navigateur lorsque supporté.
  • MediaRecorder pour proxies instantanés, partages rapides et captures d’onglet.
  • Service de rendu DojoClip pour les exports complets après sauvegarde du projet.
  • Stockage : OPFS pour les intermédiaires côté navigateur ; stockage serveur pour les entrées et sorties du service de rendu.

Nous publierons un suivi avec de vrais tableaux de benchmarks multi-appareils et navigateurs dès que nos paramètres d’encodage côté navigateur seront stabilisés.


Annexe A — valeurs sûres

  • H.264 (web) : -preset veryfast -crf 23 -maxrate 4M -bufsize 8M -pix_fmt yuv420p
  • Audio : -c:a aac -b:a 160k (musique), -b:a 96k (voix)
  • Seek puis transcode (plus rapide) : -ss avant -i input ... quand la précision frame-perfect n’est pas nécessaire ; sinon -i input -ss après.
  • Sous-titres : gardez-les en .srt ou .vtt quand c’est possible ; incrustez seulement pour les livrables finaux.

Annexe B — cartographie des fonctionnalités (partielle)

Fonctionnalité FFmpeg WASM WebCodecs MediaRecorder
Graphes de filtres précis Oui Non Non
Accélération matérielle Non (CPU) Oui Oui
Contrôle CFR/VFR précis Oui Oui Limité
Capture temps réel Non Oui (decode/encode) Oui
Mux MP4 intégré Oui Non (muxer requis) Non (WebM courant)
Mise en place la plus simple Moyenne Moyenne Oui

Envie d’essayer ?

Ouvrez des outils gratuits comme le Compresseur vidéo ou l’Extracteur audio pour voir cette pile navigateur en action. Dans l’éditeur principal, sauvegardez le projet puis exportez avec le service de rendu DojoClip pour les rendus complets de timeline.