A few weeks ago, I needed to extract audio from a video file. Simple task, right? But every online tool I found either:
- Required uploading my file to some unknown server
- Had annoying file size limits
- Wanted me to pay or watch ads
As a developer, my first thought was: "Can I just do this in the browser?"
Turns out, you can. And it's pretty cool.
Why FFmpeg.wasm?
FFmpeg is the Swiss Army knife of video/audio processing. FFmpeg.wasm brings that power to the browser using WebAssembly.
The key benefits:
- No server needed - everything runs client-side
- Privacy by design - files never leave the user's device
- Cross-platform - works on any modern browser
The Basic Setup
First, install the packages:
npm install @ffmpeg/ffmpeg @ffmpeg/util
Here's a minimal example:
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';
const ffmpeg = new FFmpeg();
// Load FFmpeg core (this downloads ~30MB of WASM)
await ffmpeg.load();
// Write input file to virtual filesystem
await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
// Run the conversion
await ffmpeg.exec(['-i', 'input.mp4', '-vn', '-acodec', 'libmp3lame', '-q:a', '2', 'output.mp3']);
// Read the output
const data = await ffmpeg.readFile('output.mp3');
const blob = new Blob([data], { type: 'audio/mpeg' });
Looks simple, but I hit several walls building a production-ready tool.
Lesson 1: The WASM File is HUGE
The ffmpeg-core.wasm file is about 30MB. Your users will hate you if you load this on page load.
Solution: Lazy load it only when the user actually selects a file.
const ffmpegRef = useRef<FFmpeg | null>(null);
const ffmpegLoaded = useRef(false);
const loadFFmpeg = async () => {
if (ffmpegLoaded.current) return ffmpegRef.current;
const ffmpeg = new FFmpeg();
await ffmpeg.load();
ffmpegRef.current = ffmpeg;
ffmpegLoaded.current = true;
return ffmpeg;
};
// Only load when user selects a file
const handleFileSelect = async (file: File) => {
setStatus('loading');
const ffmpeg = await loadFFmpeg();
// ... process file
};
Lesson 2: CDN Loading Can Fail
The default setup loads WASM from unpkg or jsdelivr. In my testing, these sometimes failed due to:
- Network issues
- Corporate firewalls
- Regional CDN problems
Solution: Implement fallback CDNs with custom loading:
const cdnList = [
"https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm",
"https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd",
];
for (const baseURL of cdnList) {
try {
await ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
});
break; // Success, exit loop
} catch (e) {
continue; // Try next CDN
}
}
Lesson 3: Some Videos Have No Audio Track
This one caught me off guard. A user reported that conversion "failed" with a cryptic error:
Output file #0 does not contain any stream
After debugging, I realized: the video had no audio track. It was an AI-generated video (from tools like Runway, Pika, etc.) that was video-only.
Solution: Detect audio streams before attempting conversion:
// Run a probe command
ffmpegLogsRef.current = [];
ffmpeg.on("log", ({ message }) => {
ffmpegLogsRef.current.push(message);
});
await ffmpeg.exec(["-i", inputFileName, "-hide_banner"]);
// Check for audio stream in logs
const hasAudio = ffmpegLogsRef.current.some(log =>
log.includes("Audio:")
);
if (!hasAudio) {
setError("This video has no audio track");
return;
}
Lesson 4: Mobile Browsers Are Tricky
WebAssembly support varies wildly on mobile:
- Chrome/Firefox: Generally works well
- Safari: Works, but memory can be tight
- In-app browsers (WeChat, Facebook, etc.): Often broken
I added browser detection to warn users:
const ua = navigator.userAgent.toLowerCase();
if (ua.includes("micromessenger")) {
setWarning("WeChat browser has limited support. Please open in Safari/Chrome.");
}
Lesson 5: Show Progress or Users Will Leave
FFmpeg.wasm provides progress events, but they're not always accurate for audio extraction. I found that showing something moving is better than a static spinner:
ffmpeg.on("progress", ({ progress }) => {
setProgress(Math.round(progress * 100));
});
For the WASM download itself, I implemented custom progress tracking by streaming the fetch:
const response = await fetch(wasmURL);
const reader = response.body.getReader();
let loaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
loaded += value.length;
onProgress(loaded);
}
The Final Result
After solving these issues, I built Free Video to Audio - a tool that:
- Converts MP4/AVI/MOV/MKV to MP3/WAV/AAC/OGG/FLAC
- Runs 100% in the browser
- Supports basic audio trimming
- Works on desktop and most mobile browsers
The entire codebase is about 7000 lines of TypeScript (Next.js + React).
Key Takeaways
- FFmpeg.wasm is production-ready, but expect edge cases
- Lazy load the WASM - it's too big for initial page load
- Implement CDN fallbacks - network issues are common
- Validate input files - not all videos have audio
- Test on mobile - in-app browsers are problematic
- Show progress - users need feedback for large files
What's Next?
I'm considering:
- Adding video compression (though WASM performance might be an issue)
- Batch conversion support
- PWA for offline usage
Have you built anything with FFmpeg.wasm? I'd love to hear about your experiences in the comments!
Top comments (0)