Journal FFmpeg dans le navigateur : WebAssembly, mémoire et limites pratiques

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
-4
31
août
2026

Sommaire

Faire tourner FFmpeg dans un navigateur ressemble d’abord à une solution idéale : aucun binaire à installer, une interface web et, surtout, la possibilité de garder le média sur la machine de l’utilisateur. Avec ffmpeg.wasm, ce scénario est devenu suffisamment accessible pour construire de vrais outils. Mais la formule « traitement local dans le navigateur » masque plusieurs contraintes : téléchargement du cœur WebAssembly, copies en mémoire, performances, isolation interorigine et comportement très variable sur mobile.

J’ai récemment intégré ffmpeg.wasm dans des traitements audio simples. Voici les choix d’architecture qui m’ont paru importants, y compris les cas où renoncer au traitement local est plus honnête que de forcer le navigateur à se comporter comme une station d’encodage.

Ce qui s’exécute réellement

ffmpeg.wasm n’est pas un service distant déguisé. Le projet compile FFmpeg et plusieurs bibliothèques avec Emscripten, puis expose une API JavaScript. La commande multimédia s’exécute dans un Web Worker afin de ne pas bloquer le fil principal. La documentation d’architecture de ffmpeg.wasm distingue notamment :

  • la bibliothèque JavaScript @ffmpeg/ffmpeg ;
  • un worker qui orchestre les appels ;
  • le cœur WebAssembly, mono- ou multithread ;
  • un système de fichiers virtuel utilisé par FFmpeg.

Le chemin minimal ressemble à ceci :

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';

const ffmpeg = new FFmpeg();
await ffmpeg.load({ coreURL, wasmURL });

await ffmpeg.writeFile('input.webm', await fetchFile(file));
await ffmpeg.exec(['-i', 'input.webm', '-vn', '-b:a', '192k', 'output.mp3']);

const output = await ffmpeg.readFile('output.mp3');
const url = URL.createObjectURL(
  new Blob([output], { type: 'audio/mpeg' })
);

Le fichier choisi par l’utilisateur doit être rendu accessible au système de fichiers du cœur, puis la sortie doit être relue pour produire un Blob. Cette circulation explique une partie des besoins en mémoire : le navigateur peut conserver simultanément l’objet File, les données copiées pour le moteur, les tampons de travail et le résultat.

Dire « le fichier n’est pas envoyé » reste exact si aucune requête ne transporte son contenu vers un serveur. Cela ne veut pas dire que l’application fonctionne sans réseau : le code JavaScript, le worker et le cœur WebAssembly doivent d’abord être téléchargés, sauf s’ils sont déjà en cache ou servis dans une application hors ligne.

Le coût de démarrage fait partie du produit

L’exemple officiel annonce un cœur d’environ 31 Mo. Charger cette ressource au premier clic peut être acceptable sur une connexion fixe, beaucoup moins sur un téléphone connecté en réseau mobile. Il faut donc traiter le chargement du moteur comme un état visible, et non comme un détail caché derrière un bouton qui paraît inactif.

Quelques règles simples améliorent l’expérience :

  1. ne charger le cœur qu’après une intention claire de l’utilisateur ;
  2. distinguer « téléchargement du moteur », « préparation du fichier » et « traitement » ;
  3. mettre en cache les ressources avec une politique de version explicite ;
  4. héberger le cœur sur une origine maîtrisée lorsque la politique CORS ou la disponibilité d’un CDN ne sont pas garanties ;
  5. afficher une erreur compréhensible si le moteur ne peut pas démarrer.

La documentation d’installation recommande d’ailleurs d’héberger ces ressources plutôt que d’importer directement le paquet principal depuis un CDN, car celui-ci crée son propre worker.

Un échec de chargement n’a pas la même signification qu’un échec d’encodage. Les regrouper sous « conversion impossible » prive l’utilisateur d’une action utile. Selon le stade, il pourra réessayer le téléchargement, choisir le mode mono-thread, réduire le fichier ou basculer vers un traitement distant.

Multithread : plus rapide, mais pas gratuit

Le cœur multithread utilise SharedArrayBuffer. Dans les navigateurs modernes, cela suppose une page isolée entre origines, généralement au moyen de ces en-têtes :

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

MDN détaille les conditions d’isolation requises pour COEP. En pratique, activer ces en-têtes peut casser des ressources tierces qui ne fournissent ni CORS ni Cross-Origin-Resource-Policy compatible. Il faut vérifier les scripts, images, iframes, polices et outils d’assistance chargés par la page.

Le test d’activation doit être explicite :

const canUseMultithread = window.crossOriginIsolated
  && typeof SharedArrayBuffer !== 'undefined';

Si la condition échoue, le cœur mono-thread constitue un repli plus robuste. Il ne faut pas simplement laisser l’import multithread échouer après le choix du fichier.

Le multithread ne transforme pas WebAssembly en FFmpeg natif. Les mesures publiées par le projet montrent un écart important pour un transcodage vidéo, même avec plusieurs threads. Les nombres exacts dépendent du processeur, du navigateur, du codec et de la source ; la conclusion utile est surtout qu’il ne faut pas promettre les performances de la ligne de commande native.

La mémoire est souvent la vraie limite

Sur ordinateur, un traitement court peut fonctionner sans difficulté. Sur mobile, l’onglet peut être fermé par le système avant qu’une exception JavaScript exploitable ne soit levée. La taille du fichier seule ne suffit pas à prédire le pic de mémoire : le codec, la définition, le nombre d’images de référence, les filtres et la taille de sortie comptent aussi.

Il est néanmoins raisonnable de placer une limite conservatrice avant de copier le fichier dans le moteur. Cette limite ne devrait pas être présentée comme une capacité universelle : elle dépend du mode, de l’appareil et de l’opération.

Pour une extraction audio, un remuxage ou la suppression d’une piste, le travail peut être suffisamment léger. Une recompression vidéo 4K longue avec filtres et changement de codec est une autre catégorie. Dans ce cas, forcer le navigateur à essayer « jusqu’à ce que ça passe » conduit à une mauvaise expérience : chauffe, batterie consommée, progression immobile, puis perte de tout le travail.

Une stratégie plus utile combine :

  • une limite de taille différente selon l’opération ;
  • la durée et la définition lorsque les métadonnées sont disponibles ;
  • une détection prudente des appareils à mémoire limitée ;
  • un arrêt accessible ;
  • un mode serveur proposé avant l’échec lorsque la tâche est manifestement lourde.

L’API fournit un délai d’exécution et permet aussi d’interrompre une commande. Après une interruption brutale, il est plus sûr de considérer l’instance comme jetable, de libérer les URL d’objet et de recréer le moteur pour la tentative suivante.

Une barre de progression n’est pas une preuve

L’événement progress de ffmpeg.wasm est documenté comme expérimental et ne convient pas à tous les graphes de traitement. Une concaténation, une entrée aux horodatages inhabituels ou plusieurs passages peuvent produire une valeur trompeuse.

Il vaut mieux modéliser des étapes stables :

type Stage =
  | 'loading-runtime'
  | 'reading-input'
  | 'processing'
  | 'building-output'
  | 'done'
  | 'failed';

La progression fine ne doit apparaître que lorsque l’on sait la calculer. Sinon, une activité indéterminée accompagnée du nom de l’étape est plus honnête qu’un « 97 % » immobile pendant deux minutes.

Cette séparation aide aussi l’observabilité. On peut compter les échecs de chargement du runtime, les erreurs d’entrée et les interruptions sans collecter le nom du fichier ni le contenu du média. Les métriques doivent rester agrégées et minimales : le fait de traiter localement le fichier perd une partie de son intérêt si la télémétrie reconstruit une histoire détaillée de l’utilisateur.

Local, serveur ou hybride

Il n’existe pas de réponse unique. Le bon choix dépend du type d’opération.

Le traitement local convient bien lorsque :

  • le fichier est court ou modéré ;
  • l’opération évite une recompression vidéo coûteuse ;
  • la confidentialité ou la latence d’envoi est prioritaire ;
  • l’utilisateur possède un appareil raisonnablement récent ;
  • l’application peut annoncer clairement les limites.

Le serveur est souvent préférable lorsque :

  • le fichier est volumineux ;
  • plusieurs rendus ou codecs lourds sont demandés ;
  • il faut reprendre un travail après la fermeture de l’onglet ;
  • la durée d’encodage doit être prévisible ;
  • le navigateur ne fournit pas les capacités requises.

Un mode hybride demande davantage de code, mais rend le compromis visible. Dans un outil de conversion de vidéo en MP3 que je maintiens, le mode navigateur sert aux tâches compatibles avec ffmpeg.wasm, tandis qu’un mode cloud distinct reste disponible. La page ne doit jamais faire croire qu’un traitement est local lorsqu’un fichier est envoyé : le choix, le texte d’état et la politique de conservation doivent décrire le chemin réellement emprunté.

Cette distinction vaut aussi côté analytique. Le mode de traitement doit faire partie de l’événement technique afin de ne pas attribuer un échec réseau au moteur WebAssembly, ou une limite mémoire au serveur.

Les vérifications que je garderais avant mise en production

Une petite matrice de tests apporte plus qu’un test heureux sur un ordinateur de développement :

  • Chrome, Firefox et Safari sur ordinateur ;
  • au moins un téléphone Android et un iPhone ;
  • cœur déjà en cache et premier chargement ;
  • mode mono-thread et multithread ;
  • source courte, source proche de la limite et source refusée ;
  • arrêt volontaire au milieu du traitement ;
  • onglet placé en arrière-plan ;
  • codec non pris en charge par le cœur choisi ;
  • politique COOP/COEP avec toutes les ressources tierces ;
  • libération de chaque Blob URL et suppression des fichiers virtuels après usage.

Je vérifierais également que les messages ne promettent pas « aucune donnée ne quitte votre appareil » si une vignette, un diagnostic ou une solution de repli envoie une partie du média. La promesse doit être testable depuis l’onglet réseau.

Conclusion

ffmpeg.wasm rend possibles des traitements multimédias réellement locaux dans une interface web. Son intérêt ne se limite pas à éviter un serveur : il permet de réduire les transferts, d’offrir un mode plus privé et de réutiliser l’écosystème FFmpeg avec une API familière.

En contrepartie, le navigateur impose ses propres limites. Le cœur doit être téléchargé, les fichiers passent par un système de fichiers virtuel, le multithread exige une isolation interorigine et la mémoire mobile reste difficile à prévoir. Une bonne intégration ne cache pas ces contraintes. Elle choisit les opérations adaptées, refuse tôt les cas risqués, distingue précisément les étapes et propose un autre chemin lorsque le navigateur n’est plus le bon outil.

Transparence : je développe le service cité comme exemple. Le présent journal décrit les choix techniques et les limites observées ; il ne s’agit pas d’un communiqué de lancement.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.