How to Set Up FFmpeg in React (Vite, 2025)

9/3/2025 • tech

How to Set Up FFmpeg in React (Vite, 2025)

0) What we’re building


1) Install the right packages

# React app (skip if you already have one)
npm create vite@latest my-app -- --template react
cd my-app

# FFmpeg packages (new API)
npm i @ffmpeg/ffmpeg @ffmpeg/core @ffmpeg/util

Why 3 packages?

  • @ffmpeg/ffmpeg – the JS wrapper (gives you FFmpeg)
  • @ffmpeg/core – the actual WebAssembly + worker binaries
  • @ffmpeg/util – helpers like fetchFile

2) Vite config (COOP/COEP + WASM as an asset)

Create or edit vite.config.ts (or .js):

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  // Make sure the .wasm files are served/copied correctly
  assetsInclude: ['**/*.wasm'],
  server: {
    // Needed for SharedArrayBuffer in browsers (ffmpeg can use threads)
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  build: {
    target: 'esnext',
  },
})

If you deploy behind Nginx, mirror these two headers in your site config:

add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;

3) The “new way” to initialize FFmpeg

Key trick: import the URLs of the core and wasm files with ?url.

Create src/lib/ffmpeg.ts:

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

// Import the *URL strings* for core + wasm
import coreURL from '@ffmpeg/core?url'
import wasmURL from '@ffmpeg/core/wasm?url'

// Create a singleton so we only load once
let ffmpegInstance: FFmpeg | null = null

export async function getFFmpeg() {
  if (ffmpegInstance) return ffmpegInstance

  const ffmpeg = new FFmpeg()
  await ffmpeg.load({ coreURL, wasmURL }) // <-- the important part
  ffmpegInstance = ffmpeg
  return ffmpeg
}

// Example: transcode any uploaded file to MP3
export async function toMp3(file: File): Promise<Blob> {
  const ffmpeg = await getFFmpeg()

  // Put the input file into FFmpeg's virtual FS
  await ffmpeg.writeFile('input', await fetchFile(file))

  // Run FFmpeg
  await ffmpeg.exec(['-i', 'input', '-vn', '-acodec', 'libmp3lame', 'out.mp3'])

  // Read the output back
  const data = await ffmpeg.readFile('out.mp3')
  return new Blob([data], { type: 'audio/mpeg' })
}

4) A simple React component

Create src/App.tsx:

import { useState } from 'react'
import { toMp3 } from './lib/ffmpeg'

export default function App() {
  const [busy, setBusy] = useState(false)
  const [url, setUrl] = useState<string | null>(null)

  async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0]
    if (!file) return
    setBusy(true)
    try {
      const mp3 = await toMp3(file)
      setUrl(URL.createObjectURL(mp3))
    } finally {
      setBusy(false)
    }
  }

  return (
    <main style={{ padding: 24, fontFamily: 'system-ui' }}>
      <h1>FFmpeg + React (Vite)</h1>
      <input type="file" accept="video/*,audio/*" onChange={handleFile} />
      {busy && <p>Transcoding… this runs fully in your browser.</p>}
      {url && (
        <p>
          <audio controls src={url} />
          <br />
          <a href={url} download="output.mp3">Download MP3</a>
        </p>
      )}
    </main>
  )
}

Run it:

npm run dev

If the browser console warns about SharedArrayBuffer or threads, double-check those COOP/COEP headers in the Vite config (and on your production server).


5) Common “gotchas” (learned the hard way)

  1. Wrong imports
    Use the new style:
import { FFmpeg } from '@ffmpeg/ffmpeg'
import coreURL from '@ffmpeg/core?url'
import wasmURL from '@ffmpeg/core/wasm?url'
await new FFmpeg().load({ coreURL, wasmURL })

Do not use the old createFFmpeg API with these versions.

  1. Versions mismatch / empty dist folders
    Install all three packages together:
npm i @ffmpeg/ffmpeg @ffmpeg/core @ffmpeg/util
  1. WASM not found in build
    Keep assetsInclude: ['**/*.wasm'] in vite.config and use ?url imports.

  2. Cross-origin isolation
    For threaded builds, you need:

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

Add in Vite server.headers and also in your production server (Nginx, etc.).

  1. “TypeError: ffmpeg.writeFile is not a function”
    You’re mixing APIs. On the new API, use ffmpeg.writeFile, ffmpeg.exec, ffmpeg.readFile. Don’t copy FS examples from older blog posts.

6) Bonus: convert to GIF (quick snippet)

export async function toGif(file: File): Promise<Blob> {
  const ffmpeg = await getFFmpeg()
  await ffmpeg.writeFile('input.mp4', await fetchFile(file))
  await ffmpeg.exec([
    '-i', 'input.mp4',
    '-vf', 'fps=12,scale=480:-1:flags=lanczos',
    '-loop', '0',
    'out.gif'
  ])
  const data = await ffmpeg.readFile('out.gif')
  return new Blob([data], { type: 'image/gif' })
}

7) Production notes


That’s it — the minimal, actually-working React + Vite setup for FFmpeg.wasm in 2025.