Encoding de audio sin esfuerzo en el navegador con WebAssembly
Convierte un archivo de audio corto a MP3 sin subirlo. En este tutorial crearás una pequeña aplicación para el navegador con FFmpeg.wasm, un servidor local de archivos estáticos, un botón para cancelar y un enlace de descarga.
Introducción al encoding de audio en el navegador
El encoding en el navegador mantiene el audio seleccionado en el dispositivo del usuario. El navegador descarga los recursos del codificador, carga el archivo seleccionado en la memoria y ejecuta FFmpeg en un Web Worker. El encoding sigue consumiendo CPU y memoria, por lo que este ejemplo limita los archivos de entrada a 25 MiB y procesa un archivo a la vez.
El papel de WebAssembly en el procesamiento de audio
FFmpeg.wasm empaqueta FFmpeg como WebAssembly. Usamos la versión de un solo hilo de
@ffmpeg/core@0.12.10 con @ffmpeg/ffmpeg@0.12.15. El wrapper y el núcleo tienen
números de versión independientes; instalar el mismo número para ambos no garantiza un conjunto
de paquetes válido.
Ventajas de usar WebAssembly para el encoding de audio
- Procesamiento local: la aplicación no tiene un endpoint para subir audio.
- Capacidad de respuesta: un worker ejecuta el codificador fuera del hilo principal de la interfaz.
- Compatibilidad con formatos: FFmpeg proporciona los decodificadores y el codificador MP3 usados aquí.
WebAssembly no garantiza una velocidad de encoding nativa. Las preguntas frecuentes de FFmpeg.wasm explican sus limitaciones de rendimiento y memoria; prueba archivos representativos en los dispositivos que admites.
Configura una aplicación web sencilla para el encoding de audio
Requisitos previos
Usa Node.js 24, Yarn 4, un shell POSIX y un navegador actual compatible con WebAssembly y workers
de módulo. Empieza con un archivo WAV corto. Otros contenedores de audio solo funcionan cuando
su decodificador está incluido en la versión fija del núcleo de FFmpeg. El atributo
accept del selector de archivos facilita la selección, pero no la valida.
Configura el proyecto
Crea un directorio nuevo e instala versiones exactas. El enlazador node-modules permite acceder a las rutas de los paquetes que usan los comandos de copia de recursos:
mkdir webassembly-audio-encoder
cd webassembly-audio-encoder
corepack yarn init
corepack yarn config set nodeLinker node-modules
corepack yarn add --exact @ffmpeg/ffmpeg@0.12.15 @ffmpeg/core@0.12.10 express@5.1.0
mkdir -p public/vendor
cp -R node_modules/@ffmpeg/ffmpeg/dist/esm public/vendor/ffmpeg
cp -R node_modules/@ffmpeg/core/dist/esm public/vendor/core
Conserva todo el directorio ffmpeg: sus módulos JavaScript incluyen el
worker del wrapper y sus importaciones relativas. Ambos directorios vendor deben provenir de este
conjunto de paquetes instalado. No hay ninguna dependencia de CDN en tiempo de ejecución ni se
necesita un empaquetador para estas importaciones relativas en el navegador.
Configura el servidor de desarrollo
Guarda esto como server.ts. Solo sirve public/, por lo
que los archivos del proyecto no quedan expuestos:
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', () => {
console.log('Open http://127.0.0.1:3000')
})
Después de crear los archivos que se indican a continuación, inicia el servidor desde el directorio del proyecto:
node server.ts
Abre http://127.0.0.1:3000. Detén el servidor con Ctrl+C cuando termines.
Configura y usa FFmpeg.wasm
Guarda esto como 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>
Guarda esto como public/index.js. Cada intento tiene su propio worker y sistema
de archivos virtual. Al terminar ese worker, se libera la memoria de entrada, salida y del
codificador, incluso después de una conversión fallida.
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
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
}
clearDownload()
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.'
}
})
Los nombres fijos de los archivos virtuales evitan que el nombre del archivo seleccionado se interprete como una opción o ruta de FFmpeg. Una asignación explícita de audio selecciona el primer flujo de audio, y un código de salida distinto de cero impide que una salida parcial se ofrezca como una descarga correcta. El enlace al MP3 sigue siendo válido hasta el siguiente intento de encoding o hasta que se oculte la página.
Carga el núcleo de WebAssembly
El wrapper de FFmpeg inicia un worker de módulo desde la copia de
vendor/ffmpeg/worker.js. Ese worker importa el núcleo ESM desde el mismo origen y carga
su archivo Wasm. Este núcleo de un solo hilo no necesita un
ffmpeg-core.worker.js independiente ni SharedArrayBuffer. No lo sustituyas por
@ffmpeg/core-mt sin implementar también sus requisitos adicionales de worker y
ámbito de aislamiento entre orígenes.
Integra WebAssembly con JavaScript para mejorar las funciones de audio
La capa JavaScript gestiona la selección de archivos y las URL de descarga; FFmpeg se encarga de la decodificación y el encoding. No se necesita un AudioContext ni un AudioWorklet para convertir archivos. Si más adelante añades efectos en tiempo real o grabación, mantén ese pipeline de reproducción separado de este flujo de trabajo asíncrono de encoding de archivos.
La referencia de la API de FFmpeg documenta las operaciones con
archivos basadas en promesas, el tiempo límite de ejecución y terminate().
Crear un worker nuevo para cada tarea requiere tiempo de inicialización, pero simplifica la
cancelación y la limpieza de archivos virtuales.
Prueba y optimiza el rendimiento del encoding de audio
Convierte un WAV corto, abre el enlace de descarga y reproduce el resultado. Después prueba un segundo archivo, cancela durante la carga y durante el encoding, y selecciona un archivo vacío o no compatible. Tras cada intento, los botones deberían volver a estar listos para usarse. Sal de la página y vuelve a ella repetidamente: una página restaurada debería permitir otra conversión, y un enlace de descarga anterior debería permanecer oculto.
El límite de entrada de 25 MiB es una restricción de esta demostración, no una garantía del consumo máximo de memoria. El audio decodificado y el codificador pueden ocupar mucha más memoria que la entrada comprimida. El comando tiene un tiempo límite de encoding de 60 segundos, y el límite externo de 120 segundos termina una carga o llamada al worker que se haya bloqueado. Para archivos más grandes, considera el procesamiento del lado del servidor en lugar de aumentar los límites sin realizar mediciones.
Despliega tu aplicación web de audio y prepárala para el futuro
Despliega todo el contenido de public/ junto en la raíz del sitio mediante
HTTPS. El servidor Express local es solo para desarrollo. Asegúrate de que tu proveedor de
alojamiento sirva los módulos JavaScript y los archivos .wasm con los
tipos MIME correctos y permita workers de módulo del mismo origen. Mantén los archivos del
directorio vendor y el código de la aplicación bajo un mismo control de versiones, y vuelve a
ejecutar las comprobaciones de conversión cuando actualices una dependencia.
Conclusión
Ahora tienes un flujo local completo de conversión de audio a MP3: un conjunto compatible de paquetes FFmpeg, carga de workers, conversión con límites, cancelación y un enlace de descarga persistente. Para flujos de trabajo que necesitan subidas y procesamiento del lado del servidor, explora el servicio de encoding de audio de Transloadit.
