Encoder de l’audio en MP3 dans le navigateur avec WebAssembly
Convertissez un court fichier audio en MP3 sans le téléverser. Ce tutoriel crée une petite application pour navigateur avec FFmpeg.wasm, un serveur statique local, un bouton d’annulation et un lien de téléchargement.
L’encodage dans le navigateur conserve l’audio sélectionné sur l’appareil de l’utilisateur. Le navigateur télécharge les ressources de l’encodeur, lit le fichier sélectionné en mémoire et exécute FFmpeg dans un Web Worker. L’encodage consomme néanmoins des ressources processeur et de la mémoire. Cet exemple limite donc les fichiers d’entrée à 25 MiB et traite un seul fichier à la fois.
Choisir l’ensemble de paquets FFmpeg
FFmpeg.wasm fournit FFmpeg sous forme de WebAssembly. Nous utilisons
@ffmpeg/core@0.12.10, à un seul thread, avec @ffmpeg/ffmpeg@0.12.15.
La bibliothèque d’interface et le noyau ont des numéros de version distincts ; installer le même
numéro pour les deux ne constitue pas un ensemble de paquets valide.
- Traitement local : l’application ne dispose d’aucun point de terminaison de téléversement audio.
- Réactivité : un worker exécute l’encodeur en dehors du thread principal de l’interface.
- Prise en charge des formats : FFmpeg fournit les décodeurs et l’encodeur MP3 utilisés ici.
WebAssembly ne garantit pas la vitesse d’un encodage natif. La FAQ de FFmpeg.wasm explique ses limites de performance et de mémoire ; testez des fichiers représentatifs sur les appareils que vous prenez en charge.
Configurer le projet local
Prérequis
Utilisez Node.js 24.15 ou une version ultérieure de la branche 24.x, ou Node.js 26.5 ou une version
ultérieure de la branche 26.x, Bash et un navigateur prenant en charge WebAssembly et les workers de modules.
Ce tutoriel a été testé sous Linux avec Node.js 24.15.0, 26.5.0 et 26.8.1, Yarn 4.12.0 et Chromium 145.
Il s’agit des environnements d’exécution serveur testés, pas des exigences de l’encodeur du navigateur.
Node exécute directement server.ts grâce à la
prise en charge native de TypeScript ;
aucun compilateur ni outil de regroupement n’est nécessaire.
Vérifiez que node et corepack figurent dans votre PATH
avant de créer les fichiers. Installez Corepack séparément si votre
distribution de Node ne l’inclut pas. La configuration ci-dessous demande explicitement Yarn 4.12.0
à Corepack.
Commencez par un court fichier WAV intact, mono ou stéréo, à 44,1 kHz ou 48 kHz. Les autres conteneurs
audio ne fonctionnent que si leur décodeur est inclus dans le noyau FFmpeg à version fixe ; les
fréquences d’échantillonnage ou les configurations de canaux non prises en charge peuvent être
rééchantillonnées ou remixées avec moins de canaux pour le MP3. L’attribut
accept du sélecteur de fichiers facilite la sélection, mais ne valide pas les
fichiers. Les autres navigateurs et les appareils mobiles nécessitent leurs propres tests.
Créer le projet
Collez ce bloc dans Bash depuis le répertoire où vous souhaitez créer le projet. Il refuse un
répertoire webassembly-audio-encoder existant et conserve votre shell dans son répertoire
d’origine, même si l’installation échoue. Le fichier de verrouillage du répertoire enfant crée un
projet Yarn distinct, et le nom de fichier de configuration personnalisé évite d’hériter des
paramètres habituels de .yarnrc.yml d’un projet parent.
(
command -v node >/dev/null &&
command -v corepack >/dev/null &&
mkdir webassembly-audio-encoder &&
cd webassembly-audio-encoder &&
printf '%s\n' \
'{"private":true,"type":"module","packageManager":"yarn@4.12.0",' \
'"dependencies":{"@ffmpeg/ffmpeg":"0.12.15","@ffmpeg/core":"0.12.10","express":"5.1.0"}}' \
> package.json &&
printf '\n' > yarn.lock &&
printf '%s\n' 'nodeLinker: node-modules' 'enableGlobalCache: false' \
> .audio-yarnrc.yml &&
YARN_RC_FILENAME=.audio-yarnrc.yml corepack yarn@4.12.0 install &&
mkdir -p public/vendor/ffmpeg public/vendor/core &&
cp -R node_modules/@ffmpeg/ffmpeg/dist/esm/. public/vendor/ffmpeg/ &&
cp -R node_modules/@ffmpeg/core/dist/esm/. public/vendor/core/
)
Conservez l’intégralité du répertoire ffmpeg : ses modules JavaScript incluent
le worker de la bibliothèque d’interface et ses imports relatifs. Les deux répertoires vendor doivent
provenir de cet ensemble de paquets installé. Ces imports relatifs dans le navigateur ne nécessitent
ni dépendance à un CDN à l’exécution ni outil de regroupement.
Si l’installation ou la copie échoue après l’écriture des fichiers de configuration, conservez le répertoire, résolvez l’erreur signalée et réessayez les étapes restantes depuis le même répertoire parent :
(
cd webassembly-audio-encoder &&
YARN_RC_FILENAME=.audio-yarnrc.yml corepack yarn@4.12.0 install &&
mkdir -p public/vendor/ffmpeg public/vendor/core &&
cp -R node_modules/@ffmpeg/ffmpeg/dist/esm/. public/vendor/ffmpeg/ &&
cp -R node_modules/@ffmpeg/core/dist/esm/. public/vendor/core/
)
La nouvelle tentative remplace les fichiers des paquets copiés dans vendor et préserve les fichiers
de votre application. Utilisez le même sélecteur YARN_RC_FILENAME pour toute commande
Yarn ultérieure dans ce projet.
Configurer le serveur de développement
Enregistrez ce code dans webassembly-audio-encoder/server.ts. Il ne sert que
public/, les fichiers du projet ne sont donc pas exposés. Le port 3000 doit être
libre. Express 5 transmet les échecs de liaison au rappel de listen ;
le code affiche une erreur et se termine avec un statut non nul :
import { fileURLToPath } from 'node:url'
import express from 'express'
const app = express()
app.use(express.static(fileURLToPath(new URL('./public/', import.meta.url))))
app.listen(3000, '127.0.0.1', (error) => {
if (error) {
console.error(`Cannot start the audio server: ${error.message}`)
process.exitCode = 1
return
}
console.log('Open http://127.0.0.1:3000')
})
Après avoir créé les fichiers ci-dessous, démarrez le serveur depuis le répertoire parent :
(cd webassembly-audio-encoder && node server.ts)
Ouvrez http://127.0.0.1:3000. Arrêtez le serveur avec Ctrl+C lorsque vous avez terminé ;
votre shell reste dans le répertoire parent. Ce serveur sur l’interface de bouclage est destiné au
tutoriel local.
Ajouter les commandes de l’encodeur
Enregistrez ce code dans webassembly-audio-encoder/public/index.html :
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Browser audio encoder</title>
</head>
<body>
<h1>Encode audio to MP3</h1>
<label for="uploader">Audio file, up to 25 MiB</label>
<input type="file" id="uploader" accept="audio/*" />
<button id="encodeButton" type="button">Encode audio</button>
<button id="cancelButton" type="button" disabled>Cancel</button>
<p id="status" role="status">Choose an audio file.</p>
<a id="download" download="output.mp3" hidden>Download MP3</a>
<script type="module" src="./index.js"></script>
</body>
</html>
Enregistrez ce code dans webassembly-audio-encoder/public/index.js. Chaque tentative dispose de son propre worker
et de son propre système de fichiers virtuel. L’arrêt de ce worker supprime ses fichiers virtuels et
l’état de l’encodeur, y compris après une conversion échouée. Le Blob du téléchargement terminé reste
disponible jusqu’à la révocation de son URL.
import { FFmpeg } from './vendor/ffmpeg/index.js'
const uploader = document.getElementById('uploader')
const encodeButton = document.getElementById('encodeButton')
const cancelButton = document.getElementById('cancelButton')
const status = document.getElementById('status')
const download = document.getElementById('download')
let active = null
let downloadURL = null
function clearDownload() {
download.hidden = true
download.removeAttribute('href')
if (downloadURL !== null) URL.revokeObjectURL(downloadURL)
downloadURL = null
}
function cancelEncoding() {
if (active === null) return
active.canceled = true
active.ffmpeg.terminate()
}
async function encodeFile() {
if (active !== null) return
clearDownload()
const file = uploader.files?.[0]
if (!file || file.size === 0 || file.size > 25 * 1024 * 1024) {
status.textContent = 'Choose a nonempty audio file of at most 25 MiB.'
return
}
const job = { ffmpeg: new FFmpeg(), canceled: false }
active = job
uploader.disabled = true
encodeButton.disabled = true
cancelButton.disabled = false
status.textContent = 'Loading the encoder…'
// Also bound loading and worker failures that may never reply to the wrapper.
const deadline = setTimeout(() => {
job.ffmpeg.terminate()
}, 120_000)
try {
await job.ffmpeg.load({
coreURL: new URL('./vendor/core/ffmpeg-core.js', location.href).href,
wasmURL: new URL('./vendor/core/ffmpeg-core.wasm', location.href).href,
})
const input = new Uint8Array(await file.arrayBuffer())
if (job.canceled) return
await job.ffmpeg.writeFile('input.audio', input)
status.textContent = 'Encoding…'
const exitCode = await job.ffmpeg.exec(
['-i', 'input.audio', '-map', '0:a:0', '-vn', '-c:a', 'libmp3lame', '-b:a', '192k', 'output.mp3'],
60_000,
)
if (exitCode !== 0) throw new Error('Encoder failed or timed out')
const output = await job.ffmpeg.readFile('output.mp3')
if (!(output instanceof Uint8Array) || output.byteLength === 0) {
throw new Error('Encoder returned no audio')
}
downloadURL = URL.createObjectURL(new Blob([output], { type: 'audio/mpeg' }))
download.href = downloadURL
download.hidden = false
status.textContent = 'Done. Your MP3 is ready to download.'
} catch {
status.textContent = job.canceled
? 'Encoding canceled.'
: 'Encoding failed. Try a shorter supported audio file and check the encoder assets.'
} finally {
clearTimeout(deadline)
job.ffmpeg.terminate()
active = null
uploader.disabled = false
encodeButton.disabled = false
cancelButton.disabled = true
if (job.canceled) status.textContent = 'Encoding canceled.'
}
}
encodeButton.addEventListener('click', encodeFile)
cancelButton.addEventListener('click', cancelEncoding)
window.addEventListener('pagehide', () => {
cancelEncoding()
clearDownload()
})
window.addEventListener('pageshow', (event) => {
if (event.persisted && active === null) {
status.textContent = 'Choose an audio file to encode again.'
}
})
Les noms de fichiers virtuels fixes évitent de traiter le nom d’un fichier sélectionné comme une
option ou un chemin FFmpeg. Un mappage audio explicite sélectionne le premier flux audio, et un code
de sortie non nul empêche de proposer un résultat partiel comme un téléchargement réussi. Le lien
MP3 reste valide jusqu’à la prochaine tentative d’encodage ou jusqu’à ce que la page soit masquée.
Pour cette commande, -b:a 192k sélectionne un débit MP3 constant. Les
options de libmp3lame distinguent le débit des
paramètres de qualité à débit variable.
Charger les ressources locales correspondantes
La bibliothèque d’interface FFmpeg lance un worker de module à partir
de la copie de vendor/ffmpeg/worker.js. Ce worker importe le noyau ESM depuis la même origine
et charge son fichier Wasm. Ce noyau à un seul thread ne nécessite ni
ffmpeg-core.worker.js distinct ni SharedArrayBuffer. Ne le remplacez pas par
@ffmpeg/core-mt sans également mettre en œuvre son worker supplémentaire et ses
exigences d’isolation entre origines.
Nettoyer les ressources de chaque conversion
La couche JavaScript gère la sélection des fichiers et les URL de téléchargement ; FFmpeg prend en charge le décodage et l’encodage. Un AudioContext ou un AudioWorklet n’est pas nécessaire pour cette conversion de fichiers.
La documentation de référence de l’API FFmpeg décrit les opérations
sur les fichiers fondées sur des promesses, le délai maximal d’exécution et
terminate(). Un nouveau worker pour chaque tâche prend du temps à initialiser,
mais simplifie l’annulation et le nettoyage des fichiers virtuels.
Convertir et vérifier un enregistrement
Sélectionnez un court fichier WAV contenant un son reconnaissable au début comme à la fin, puis
appuyez sur Encode audio. Une fois l’encodage
terminé, le statut indique
Done. Your MP3 is ready to download.
Suivez le lien Download MP3. Ouvrez le fichier
output.mp3 enregistré dans un lecteur audio et écoutez-le jusqu’à la fin. Sa durée,
ses canaux et son contenu doivent correspondre à l’enregistrement sélectionné.
Essayez ensuite un autre enregistrement, puis un fichier vide ou non pris en charge après une conversion réussie. Le lien de téléchargement précédent devrait disparaître lorsque vous appuyez sur Encode audio. Appuyez sur Cancel pendant le chargement et pendant l’encodage ; ensuite, Encode audio devrait redevenir disponible. Quittez la page puis revenez-y : elle devrait permettre une nouvelle conversion tout en masquant l’ancien lien de téléchargement.
Une exécution réussie de l’encodeur ne prouve pas qu’un fichier d’entrée endommagé était intact. FFmpeg peut récupérer l’audio de certains fichiers tronqués et tout de même renvoyer zéro. Utilisez des fichiers d’entrée intacts et vérifiez l’intégralité de l’enregistrement téléchargé avant de vous y fier.
La limite de 25 MiB pour les fichiers d’entrée est une règle de cette démonstration, pas une garantie sur le pic de consommation mémoire. L’audio décodé et l’encodeur peuvent occuper bien plus de mémoire que le fichier d’entrée compressé. La commande impose un délai maximal d’encodage de 60 secondes, et la limite globale de 120 secondes met fin à un chargement ou à un appel au worker bloqué. Pour les fichiers plus volumineux, envisagez un traitement côté serveur plutôt que d’augmenter les limites sans mesurer la consommation.
Conservez ensemble les fichiers vendor et le code de l’application, et répétez les vérifications de conversion lorsque vous mettez à jour les paquets à version fixe. Cet exemple ne fournit aucun point de terminaison de téléversement ou de stockage ; les téléchargements sont enregistrés par votre navigateur.
Pour les flux de travail nécessitant des téléversements et un traitement côté serveur, découvrez le service d’encodage audio de Transloadit.
