ffmpeg.wasm(ブラウザで動く FFmpeg)を Web アプリに組み込むと、SharedArrayBuffer is not defined などのエラーが出て動かないことがあります。ブラウザのセキュリティの仕様が変わったためです。

典型的なエラーメッセージ

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

原因

SharedArrayBuffer は、複数のスレッドでメモリを共有するための仕組みです。CPU の脆弱性 Spectre/Meltdown への対策として、2018年の初めにいったん使えなくなりました。現在のブラウザでは、Cross-Origin Isolated(クロスオリジン分離)なページでしか使えません。

ffmpeg.wasm は、マルチスレッドで処理するために、内部で SharedArrayBuffer を使います。そのため、ページがクロスオリジン分離されている必要があります。

Cross-Origin Isolated にするための条件

ページの HTTP レスポンスヘッダーに、次の両方が必要です(それぞれ COOP、COEP と略します)。

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

解決方法1: サーバーでHTTPヘッダーを設定する(推奨)

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 ファイル)

/*
  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();
});

解決方法2: Service Worker(coi-serviceworker)を使う

GitHub Pages や Astro の静的サイトなど、サーバーの設定を変えられない場合は coi-serviceworker を使います。Service Worker(ページの通信の間に入って処理するスクリプト)が、COOP/COEP ヘッダーを付け足してくれます。

セットアップ手順

1. coi-serviceworker.js を public ディレクトリに置く

gzuidhof/coi-serviceworker から最新版をダウンロードして、サイトのルート(いちばん上の階層)に置きます。

2. HTMLで読み込む

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

このファイルが、自分自身を Service Worker として登録します。

注意点

  • coi-serviceworker.js は、CDN からではなく、サイトと同じオリジンから配信する必要があります
  • HTTPS か localhost でしか動きません
  • 初めて開いたときは、ページが自動で再読み込みされます(次からは不要です)

解決方法3: credentialless モード(Chrome 96+)

COEP: require-corp の代わりに credentialless を使うと、Google Fonts や CDN など外部サイトのファイルを、Cookie などの認証情報を付けずに読み込めます。

Cross-Origin-Embedder-Policy: credentialless

注意: Firefox はデスクトップ版の 119 以降で対応していますが、Safari は対応していません。対応していないブラウザでは require-corp を使う必要があります。

解決方法4: シングルスレッド版に切り替えて COOP/COEP をやめる

SharedArrayBuffer(SAB)が必要なのは、マルチスレッド版の @ffmpeg/core-mt だけです。シングルスレッド版の @ffmpeg/core は、SAB もクロスオリジン分離も使いません。広告や埋め込みフォーム、コメント欄があるサイトに変換ツールを置くなら、シングルスレッド版が現実的です。

広告と埋め込みフォームがあるページで、COEP: credentialless と coi-serviceworker を使ったところ、次の問題が起きました。COOP と COEP の両方のヘッダーを外すと、埋め込みが表示され、ツールもそのまま動きました。

起きたこと(Chromium 147 で確認) 原因
お問い合わせフォーム(Tally の iframe 埋め込み)が本番だけ真っ白 credentialless のもとでは、埋め込んだ別オリジンの iframe 自身も COEP を返す必要がある。CORP ヘッダーでは解決しない
AdSense / Adsterra / A-ADS の広告 iframe が同じ理由で表示されない 広告ネットワークが配信する HTML は COEP を返さない(adsbygoogle.js には credentialless という文字列もない)
crossOriginIsolated === false、typeof SharedArrayBuffer === "undefined" の状態でも、音声削除(-c copy)と速度変更(libx264 で再エンコード)は正常に出力できた 使っているのが @ffmpeg/[email protected](シングルスレッド版)だから

判断の目安

  • ツール専用のドメインで、埋め込みがなく、速度を最優先したい → core-mt + COOP/COEP(解決方法 1)
  • 記事・広告・フォーム・コメントと同じサイトに置く → core(シングルスレッド版)にして、ヘッダーを送らない。マルチスレッド版より遅くなる(ffmpeg.wasm の公式の計測では約 2 倍)が、ページのほかの部分は壊れない
  • 両方を取りたい → ツールのページだけを別オリジン(サブドメイン)に分け、そこだけ COOP/COEP を送る

Service Worker(SW)を残す場合は、ヘッダーの付け足しをやめて、wasm/core ファイルのキャッシュだけをする SW に変えます。こうすると、既存ユーザーのブラウザに登録済みの SW を、skipWaiting と clients.claim で安全に置き換えられます。

ブラウザの動作確認

ページがクロスオリジン分離されているかは、Chrome DevTools の Console で次を実行すると確認できます。

console.log(self.crossOriginIsolated); // true であれば OK

Astroプロジェクトでの設定例

Astro の開発用サーバーでも、ヘッダーの設定が必要です。Astro の server.headers に書くと、astro dev と astro preview の両方でヘッダーが付きます。

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

ビルドしたサイトを Netlify や Vercel で公開するときは、上の各サービスの設定ファイルを使います。

ffmpeg.wasm が動作する環境の条件まとめ

条件 詳細
HTTPS または localhost HTTP の本番サイトでは Service Worker が動かない
SharedArrayBuffer 対応ブラウザ Chrome 68+, Firefox 79+, Safari 15.2+
COOP/COEP ヘッダー マルチスレッド版だけ必要。サーバーで設定するか、Service Worker で付け足す。シングルスレッド版なら不要
WebAssembly 対応 現在の主要ブラウザはすべて対応

関連ツール

関連記事

よくある質問

SharedArrayBuffer と FFmpeg の関係は?

ffmpeg.wasm は、マルチスレッドでデコードやエンコードをするときに SharedArrayBuffer を使います。クロスオリジン分離(COOP と COEP のヘッダー)がないと、ブラウザが SAB を使えなくするので、WASM モジュールが起動できません。

SAB なしで ffmpeg.wasm を使える?

使えます。シングルスレッド版に切り替えます(@ffmpeg/core-mt → @ffmpeg/core)。COOP/COEP は不要ですが、マルチコアの環境ではマルチスレッド版より遅くなります。

COOP/COEP ヘッダはどう設定する?

Cross-Origin-Opener-Policy: same-origin と Cross-Origin-Embedder-Policy: require-corp を、サーバーやホスティングサービスの設定で送ります。Cloudflare Pages なら、_headers ファイルを静的ファイルのフォルダ(フレームワークなら public/ など、ビルドでそのまま出力にコピーされるフォルダ)に置きます。

dev では動くが本番で壊れる

localhost でも、COOP/COEP がなければ SAB は使えません。dev で動くのは、開発サーバーがヘッダーを送っている場合です。本番のサーバーや CDN でも同じヘッダーを送っているか、ブラウザのコンソールで crossOriginIsolated === true になっているかを確認してください。

これらのヘッダを追加すると AdSense / アナリティクスが壊れる?

広告は表示されなくなります。アナリティクスのように iframe を使わないスクリプトは、credentialless なら配信元に CORP ヘッダーがなくても読み込めますが、広告の iframe は credentialless でも表示されません(埋め込まれた iframe 自身に COEP が必要なため)。ツールのページだけを別オリジンに分けるか、シングルスレッド版に切り替えてヘッダーを送らないか(解決方法 4)のどちらかにします。