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.


Releated Posts

Best Free Tools for Indie Game Dev (2025)
tech

Best Free Tools for Indie Game Dev (2025)

Editors, art tools, audio, and pipeline helpers you can use on a $0 budget.