Transcode Videos in React with ffmpeg.wasm

Client-side video transcoding reduces server infrastructure costs, minimizes network bandwidth, and keeps sensitive media securely within the user's browser. This guide provides a direct implementation path for integrating ffmpeg.wasm into a React project, configuring required browser security headers, and converting media files using WebAssembly.

1. Configure Cross-Origin Isolation

ffmpeg.wasm uses SharedArrayBuffer for multi-threading and high-performance memory operations. Modern web browsers require explicit HTTP response headers to enable this API.

Add the following headers to your local development server and production deployment:

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

If you are using Vite, add this to your vite.config.js:

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

export default defineConfig({
  plugins: [react()],
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});

2. Install Required Dependencies

Install the core @ffmpeg/ffmpeg package and its helper utilities:

npm install @ffmpeg/ffmpeg @ffmpeg/util

3. Implement the React Component

The modern version of ffmpeg.wasm (v0.12+) separates the execution logic from the core WebAssembly binary. You load the core binaries dynamically via Blob URLs to prevent cross-origin issues with standard CDNs.

Below is a complete, minimal React component for transcoding a video to MP4:

import React, { useState, useRef } from 'react';
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';

export default function VideoTranscoder() {
  const [loaded, setLoaded] = useState(false);
  const [transcoding, setTranscoding] = useState(false);
  const [videoSrc, setVideoSrc] = useState('');
  const [progress, setProgress] = useState(0);
  const ffmpegRef = useRef(new FFmpeg());
  const messageRef = useRef(null);

  const loadFFmpeg = async () => {
    const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
    const ffmpeg = ffmpegRef.current;

    ffmpeg.on('log', ({ message }) => {
      if (messageRef.current) messageRef.current.innerText = message;
    });

    ffmpeg.on('progress', ({ progress: prog }) => {
      setProgress(Math.round(prog * 100));
    });

    // Load WebAssembly binaries
    await ffmpeg.load({
      coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
      wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
    });

    setLoaded(true);
  };

  const transcode = async (event) => {
    const file = event.target.files?.[0];
    if (!file) return;

    if (!loaded) {
      await loadFFmpeg();
    }

    const ffmpeg = ffmpegRef.current;
    setTranscoding(true);

    const inputName = 'input_file';
    const outputName = 'output.mp4';

    // Write file into FFmpeg's virtual filesystem
    await ffmpeg.writeFile(inputName, await fetchFile(file));

    // Execute the standard FFmpeg command
    await ffmpeg.exec(['-i', inputName, outputName]);

    // Read the result from the virtual filesystem
    const data = await ffmpeg.readFile(outputName);

    // Convert output buffer to a downloadable/playable URL
    const url = URL.createObjectURL(
      new Blob([data.buffer], { type: 'video/mp4' })
    );

    setVideoSrc(url);
    setTranscoding(false);
  };

  return (
    <div style={{ padding: '2rem' }}>
      {!loaded && (
        <button onClick={loadFFmpeg}>Initialize FFmpeg</button>
      )}

      {loaded && (
        <div>
          <label>Select a video to transcode to MP4: </label>
          <input type="file" onChange={transcode} disabled={transcoding} />
        </div>
      )}

      {transcoding && <p>Transcoding: {progress}%</p>}

      {videoSrc && (
        <div style={{ marginTop: '1rem' }}>
          <h3>Output Video:</h3>
          <video src={videoSrc} controls width="400" />
          <br />
          <a href={videoSrc} download="transcoded.mp4">Download Transcoded File</a>
        </div>
      )}

      <p ref={messageRef} style={{ fontSize: '0.8rem', color: '#666' }}></p>
    </div>
  );
}

4. Technical Considerations and Limitations

  • Memory Usage: Browser tabs typically have a memory limit (usually between 2GB and 4GB depending on the OS and architecture). Processing large files (over 500MB) can cause the tab to crash.
  • Execution Speed: While WebAssembly performs near-native speeds for basic computations, client-side transcoding is CPU-bound and lacks full hardware acceleration (GPU encoding). Transcoding long or 4K videos will cause noticeable device battery drain and thermal throttling.
  • Storage Clean-up: Always clear unused virtual files using ffmpeg.deleteFile('filename') when processing multiple files in the same session to avoid memory leaks within the WebAssembly heap.