When you add ffmpeg.wasm (FFmpeg that runs in the browser) to a web app, it can fail with errors such as SharedArrayBuffer is not defined. The cause is a change in browser security rules.

Typical Error Messages

SharedArrayBuffer is not defined
ReferenceError: SharedArrayBuffer is not defined
DataCloneError: Failed to execute 'postMessage' on 'Worker': SharedArrayBuffer transfer requires self.crossOriginIsolated.

Cause

SharedArrayBuffer lets several threads share the same memory. Browsers disabled it at the start of 2018 to protect against Spectre and Meltdown, two CPU security flaws. Current browsers make it available only on pages that are cross-origin isolated.

ffmpeg.wasm uses SharedArrayBuffer for multithreading, so the page that runs it must be cross-origin isolated.

Requirements for Cross-Origin Isolation

The page’s HTTP response must include both of these headers (COOP and COEP for short):

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

Nginx

location / {
    add_header Cross-Origin-Opener-Policy "same-origin";
    add_header Cross-Origin-Embedder-Policy "require-corp";
}

Apache (.htaccess)

Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"

Netlify (_headers file)

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

Vercel (vercel.json)

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
        { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
      ]
    }
  ]
}

Node.js / Express

app.use((req, res, next) => {
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  next();
});

Fix 2: Use a Service Worker (coi-serviceworker)

If you cannot change the server’s headers (on GitHub Pages or a static site built with Astro, for example), use coi-serviceworker. It is a Service Worker (a script that sits between the page and the network) that adds the COOP/COEP headers to the responses it handles.

Setup

1. Place coi-serviceworker.js in the public directory

Download the latest version from gzuidhof/coi-serviceworker and place it at the site root.

2. Load it from your HTML

<script src="/coi-serviceworker.js"></script>

The script registers itself as the Service Worker.

Caveats:

  • coi-serviceworker.js must be served from your own origin, not from a CDN
  • It works only over HTTPS or on localhost
  • On the first visit the page reloads once; later visits do not reload

Fix 3: credentialless Mode (Chrome 96+)

With credentialless instead of require-corp, you can load files from other sites (Google Fonts, CDNs and so on). The browser requests them without cookies or other credentials.

Cross-Origin-Embedder-Policy: credentialless

Note: Desktop Firefox supports it from version 119, but Safari does not. Browsers without support need require-corp instead.

Fix 4: Switch to the single-thread build and drop COOP/COEP

Only the multi-thread build, @ffmpeg/core-mt, needs SharedArrayBuffer (SAB). The single-thread build, @ffmpeg/core, needs neither SAB nor cross-origin isolation. If your converter sits on a page that also has ads, an embedded form or a comment section, use the single-thread build.

On pages that also had ads and an embedded form, COEP: credentialless with coi-serviceworker caused the problems below. Removing both headers fixed the embeds, and the tools kept working:

What happened (Chromium 147) Cause
The contact form (an embedded Tally iframe) was blank, in production only Under credentialless, an embedded iframe from another origin must send COEP itself. A CORP header on the resource does not help
AdSense / Adsterra / A-ADS ad iframes never appeared, for the same reason The HTML that ad networks serve does not send COEP (adsbygoogle.js does not even contain the string credentialless)
With crossOriginIsolated === false and typeof SharedArrayBuffer === "undefined", mute (-c copy) and speed change (libx264 re-encode) still gave correct output The page loads @ffmpeg/[email protected], the single-thread build

Rule of thumb

  • Tools on their own domain, no embeds, speed first → core-mt + COOP/COEP (Fix 1)
  • Tools on the same pages as articles, ads, forms or comments → single-thread core, and send neither header. It is slower than the multi-thread build (about 2x in ffmpeg.wasm’s own benchmark), but nothing else on the page breaks
  • Want both → move the tools to their own origin (a subdomain) and send COOP/COEP only there

If you keep a Service Worker, remove the header code and use it only to cache the wasm/core files. With skipWaiting and clients.claim, the new worker safely replaces the one that returning visitors already have registered.

Verifying in the Browser

To check whether the page is cross-origin isolated, run this in the Console of Chrome DevTools:

console.log(self.crossOriginIsolated); // true means you're good

Example: Astro Project Configuration

Astro’s development server needs the headers too. Headers set in Astro’s server.headers are sent by both astro dev and astro preview:

// astro.config.mjs
export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
});

When you publish the built site on Netlify or Vercel, use their configuration files shown above.

Environment Checklist for ffmpeg.wasm

Requirement Details
HTTPS or localhost Service Workers don’t run over plain HTTP in production
SharedArrayBuffer-capable browser Chrome 68+, Firefox 79+, Safari 15.2+
COOP/COEP headers Multi-thread build only. Set on the server, or injected via Service Worker. Not needed for the single-thread build
WebAssembly support All major current browsers support it

Frequently Asked Questions

What does SharedArrayBuffer have to do with FFmpeg?

ffmpeg.wasm uses SharedArrayBuffer to decode and encode on several threads. Without cross-origin isolation (the COOP and COEP headers), the browser blocks SAB and the WASM module fails to start.

Can I use ffmpeg.wasm without SharedArrayBuffer?

Yes. Switch to the single-thread build (@ffmpeg/core-mt → @ffmpeg/core). It runs without COOP/COEP, but it is slower than the multi-thread build on multi-core machines.

How do I add COOP/COEP headers?

Send Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp from your server or hosting service. On Cloudflare Pages, put a _headers file in your static files folder (with a framework, usually public/, which the build copies into the output).

Why does it work in dev but break in production?

Even on localhost, SAB is not available without COOP/COEP. If it works in dev, your dev server is sending the headers. Check that your production server or CDN sends them too, and confirm that crossOriginIsolated === true in the browser console.

Will adding the headers break my AdSense or analytics?

Ads will break. Scripts that do not use iframes, such as analytics, still load under credentialless even when their server sends no CORP header. Ad iframes do not appear even under credentialless, because an embedded iframe must send COEP itself. Either move the tools to a separate origin, or switch to the single-thread build and send no headers (Fix 4).