DojoClip

Commencer

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

By: Pansa LegrandDate: 2025-02-18Category: Engineering
Schéma du pipeline FFmpeg WebAssembly dans DojoClip

Bienvenue dans Render Lab, notre série dédiée aux technologies qui rendent DojoClip rapide, respectueux de la vie privée et pensé pour les créateurs. Cet article dépasse le discours général pour expliquer le comment : FFmpeg compilé en WebAssembly (WASM), ses points forts, ses limites, et la manière dont nous l’associons à MediaRecorder et WebCodecs pour bâtir un flux hybride à faible latence.


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, WebCodecs et MediaRecorder pour les prévisualisations et exports rapides.

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)
         └─▶ 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 tout en gardant le traitement local 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 final depuis timeline canvas Moyenne Faible WebCodecs + muxer
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. Export timeline 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 extraction audio, waveforms/miniatures, coupes précises, remux, préparation de sous-titres.
  • WebCodecs pour previews de timeline canvas et exports finaux lorsque supporté.
  • MediaRecorder pour proxies instantanés, partages rapides et captures d’onglet.
  • Stockage : OPFS pour les intermédiaires et exports reprenables ; mémoire pour les petits jobs.

Nous publierons un suivi avec de vrais tableaux de benchmarks multi-appareils et navigateurs dès que nos paramètres d’encodage 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 le Compresseur vidéo, l’Extracteur audio ou le Studio de sous-titres dans DojoClip pour voir cette pile en action. Développeur ? Clonez notre banc de tests (annoncé dans le prochain article), exécutez les benchmarks et partagez vos chiffres. Nous les agrégerons dans un tableau public pour guider les créateurs.