DEV Community

felix
felix

Posted on

Building a Privacy-First Video to Audio Converter with FFmpeg.wasm

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
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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

  1. FFmpeg.wasm is production-ready, but expect edge cases
  2. Lazy load the WASM - it's too big for initial page load
  3. Implement CDN fallbacks - network issues are common
  4. Validate input files - not all videos have audio
  5. Test on mobile - in-app browsers are problematic
  6. 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)