When you embed ffmpeg.wasm into a web app, you may run into errors like SharedArrayBuffer is not defined or Atomics is not defined. These come from a change in browser security policy.


Typical Error Messages

SharedArrayBuffer is not defined
ReferenceError: SharedArrayBuffer is not defined
Cannot use SharedArrayBuffer in a cross-origin isolated context
Uncaught (in promise) ReferenceError: Atomics is not defined

Cause

SharedArrayBuffer was restricted as part of mitigations for Spectre/Meltdown; since 2018, it has only been available on cross-origin isolated pages.

Internally, ffmpeg.wasm uses SharedArrayBuffer for multithreading — which means the hosting page must be cross-origin isolated.

Requirements for Cross-Origin Isolation

The page’s HTTP response must include both of the following headers:

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)

When you can’t modify server headers (GitHub Pages, static sites built with Astro, etc.), use coi-serviceworker. It injects COOP/COEP headers across requests via a Service Worker.

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. Add the registration code to your HTML

<script>
  if (!self.crossOriginIsolated) {
    navigator.serviceWorker.register('/coi-serviceworker.js').then(function(reg) {
      if (!navigator.serviceWorker.controller) {
        // Reload is required when the Service Worker registers for the first time
        window.location.reload();
      }
    }).catch(function(e) {
      console.warn('coi-serviceworker registration failed:', e);
    });
  }
</script>

Caveats:

  • Service Workers only work for same-origin requests
  • Only runs on HTTPS or localhost
  • A page reload occurs on first visit (not needed after that)

Fix 3: credentialless Mode (Chrome 96+)

Using credentialless instead of require-corp lets you load third-party resources (Google Fonts, CDNs, etc.) without credentials.

Cross-Origin-Embedder-Policy: credentialless

Note: Supported in Firefox 119+ and Safari 17+. Older browsers need a require-corp fallback.


Fix 4: Switch to the single-thread build and drop COOP/COEP (what this site runs)

Only the multi-thread build @ffmpeg/core-mt needs SharedArrayBuffer. The single-thread @ffmpeg/core uses neither SAB nor cross-origin isolation. If your converter has to live on a page that also carries ads, an embedded form or a comment widget, this is the practical answer.

This site’s 43 browser tools ran under COEP: credentialless + coi-serviceworker until 2026-09. Measuring the production site showed the following, so we removed the headers entirely:

Observed (Chromium 147, measured against the production URL with Playwright) Cause
The contact form (a Tally iframe embed) rendered blank in production only Under credentialless, a nested cross-origin iframe must itself send COEP. A CORP header on the resource does not help
AdSense / Adsterra / A-ADS ad iframes never rendered, for the same reason Ad networks’ creative HTML does not send COEP (adsbygoogle.js doesn’t even contain the string credentialless)
With crossOriginIsolated === false and typeof SharedArrayBuffer === "undefined", mute (-c copy) and speed change (libx264 re-encode) still produced correct output The site loads @ffmpeg/[email protected], the single-thread build

Rule of thumb

  • Dedicated tool domain, no embeds, speed first → core-mt + COOP/COEP (Fix 1)
  • Tools sharing pages with articles, ads, forms, comments → single-thread core and send no headers. For conversions that take tens of seconds the difference is barely noticeable, and nothing else on the page breaks
  • Want both → move the tools to their own origin (a subdomain) and apply COOP/COEP only there

If you keep a Service Worker, strip the header injection and reduce it to a wasm/core cache; with skipWaiting + clients.claim it safely replaces the worker existing visitors already registered.


Verifying in the Browser

Confirm whether the page is actually cross-origin isolated:

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

You can also check from the Chrome DevTools Console.


Example: Astro Project Configuration

You’ll also need to configure Astro’s dev server:

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

When deploying a static build to Netlify or Vercel, use the platform-specific 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+
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

All of our browser-based tools are already configured for this:


Frequently Asked Questions

What does SharedArrayBuffer have to do with FFmpeg?

ffmpeg.wasm uses SharedArrayBuffer for multi-threaded decode/encode. Without cross-origin isolation (COOP+COEP headers) the browser blocks SAB and the WASM module fails to start.

Can I use ffmpeg.wasm without SharedArrayBuffer?

Yes — load the single-threaded build (@ffmpeg/core-mt → @ffmpeg/core). It runs without COOP/COEP but is 2–4× slower 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 hosting layer. On Cloudflare Pages, use _headers at the project root.

Why does it work in dev but break in production?

Local dev servers usually allow SAB without COOP/COEP. Production CDNs lock it down. Test with crossOriginIsolated === true in the browser console.

Will adding the headers break my AdSense or analytics?

Yes. Scripts can be let through with CORP, but ad iframes still fail to render even under credentialless, because a nested 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).



Primary sources: developer.mozilla.org — SharedArrayBuffer / gzuidhof/coi-serviceworker