How to Set Up FFmpeg in React (Vite, 2025)
9/3/2025 • tech

0) What we’re building
- Load FFmpeg.wasm in the browser
- Convert an uploaded video to MP3 (sample task)
- Works in dev and production builds
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 youFFmpeg
)@ffmpeg/core
– the actual WebAssembly + worker binaries@ffmpeg/util
– helpers likefetchFile
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)
- 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.
- Versions mismatch / empty dist folders
Install all three packages together:
npm i @ffmpeg/ffmpeg @ffmpeg/core @ffmpeg/util
WASM not found in build
KeepassetsInclude: ['**/*.wasm']
invite.config
and use?url
imports.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.).
- “TypeError: ffmpeg.writeFile is not a function”
You’re mixing APIs. On the new API, useffmpeg.writeFile
,ffmpeg.exec
,ffmpeg.readFile
. Don’t copyFS
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
- Host with HTTPS (some browsers restrict SharedArrayBuffer on insecure origins).
- If you use a CDN, make sure the COOP/COEP headers survive.
- Large files consume RAM — FFmpeg runs in the browser. Consider chunked workflows for huge videos.
That’s it — the minimal, actually-working React + Vite setup for FFmpeg.wasm in 2025.