Use ffmpeg.wasm in React for Client-Side Video Transcoding

This article provides a step-by-step guide on how to integrate the ffmpeg.wasm library into a React application to perform client-side video transcoding. You will learn how to set up the environment, configure the required security headers, and write the React code necessary to load FFmpeg, process a video file, and download the transcoded output directly from the browser.


Step 1: Install the Dependencies

To get started, you need to install the latest versions of the FFmpeg WebAssembly library and its utility package. Run the following command in your React project directory:

npm install @ffmpeg/ffmpeg @ffmpeg/util

Step 2: Configure SharedArrayBuffer Headers

ffmpeg.wasm relies on SharedArrayBuffer, which requires your server to serve documents with specific HTTP headers for security reasons (cross-origin isolation). If these headers are missing, the browser will block the WebAssembly execution.

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

export default {
  server: {
    headers: {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
    },
  },
};

If you are using Create React App (Webpack), you can configure these headers in your local development server or set them up on your hosting provider (e.g., Netlify, Vercel, or Cloudflare Pages) for production.

Step 3: Implement the Transcoding Component

Create a React component to handle loading FFmpeg, accepting a video file input, running the transcoding process, and generating a downloadable preview of the output.

Here is the complete implementation using modern functional React and the @ffmpeg/ffmpeg v0.12+ API:

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

function VideoTranscoder() {
  const [loaded, setLoaded] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [videoFile, setVideoFile] = useState(null);
  const [transcodedUrl, setTranscodedUrl] = useState('');
  const [status, setStatus] = useState('');
  const ffmpegRef = useRef(new FFmpeg());

  // Load FFmpeg from a CDN
  const loadFFmpeg = async () => {
    setIsLoading(true);
    setStatus('Loading FFmpeg core...');
    const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd';
    const ffmpeg = ffmpegRef.current;

    try {
      await ffmpeg.load({
        coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
        wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
      });
      setLoaded(true);
      setStatus('FFmpeg loaded successfully!');
    } catch (error) {
      console.error(error);
      setStatus('Failed to load FFmpeg.');
    } finally {
      setIsLoading(false);
    }
  };

  // Handle video transcoding (e.g., MP4 to WebM)
  const transcodeVideo = async () => {
    if (!videoFile) return;
    setStatus('Transcoding started...');
    const ffmpeg = ffmpegRef.current;

    // Write the file to FFmpeg's virtual file system
    await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));

    // Execute the FFmpeg CLI command to transcode the video
    // This example converts input.mp4 to output.webm
    await ffmpeg.exec(['-i', 'input.mp4', 'output.webm']);
    setStatus('Transcoding complete!');

    // Read the result from the virtual file system
    const data = await ffmpeg.readFile('output.webm');

    // Create a URL for the generated WebM file
    const url = URL.createObjectURL(new Blob([data.buffer], { type: 'video/webm' }));
    setTranscodedUrl(url);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>Client-Side Video Transcoder</h2>
      
      {!loaded ? (
        <button onClick={loadFFmpeg} disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Load FFmpeg (WASM)'}
        </button>
      ) : (
        <div>
          <p>FFmpeg is ready.</p>
          <input 
            type="file" 
            accept="video/*" 
            onChange={(e) => setVideoFile(e.target.files[0])} 
          />
          <button onClick={transcodeVideo} disabled={!videoFile}>
            Transcode to WebM
          </button>
        </div>
      )}

      <p><strong>Status:</strong> {status}</p>

      {transcodedUrl && (
        <div style={{ marginTop: '20px' }}>
          <h3>Transcoded Video Preview:</h3>
          <video src={transcodedUrl} controls width="500"></video>
          <br />
          <a href={transcodedUrl} download="transcoded.webm">
            Download Transcoded Video
          </a>
        </div>
      )}
    </div>
  );
}

export default VideoTranscoder;

Step 4: How the Code Works

  1. Initialization (loadFFmpeg): We load the WebAssembly binary from a public CDN (unpkg.com) to avoid bundling large WebAssembly files inside our main bundle.
  2. Virtual File System (writeFile): FFmpeg operates on a virtual in-memory file system. We must use fetchFile to convert our local React file state into a Uint8Array and write it into the virtual environment as input.mp4.
  3. Execution (exec): We call ffmpeg.exec() with standard command-line arguments. In this scenario, ['-i', 'input.mp4', 'output.webm'] takes the input file and transcodes it into WebM format.
  4. Retrieval (readFile): Once execution finishes, we read the output file output.webm from the virtual file system, wrap it in a JavaScript Blob, and generate a local URL using URL.createObjectURL for downloading or displaying inside an HTML5 <video> element.