FFmpeg.wasm断念の経緯とWebM出力への切り替え
概要
proportional-animation-qqq.vueのアニメーションエクスポート機能において、当初はTwitter投稿に対応するためMP4(H.264)出力を目指していた。しかしFFmpeg.wasmの読み込みで問題が発生しWebM出力に切り替えた。
発生した問題
症状
エクスポートボタンをクリックすると「FFmpegを読み込み中...」で永遠に止まり、処理が進まない。
エラーログ
ブラウザのコンソールとVite開発サーバーに以下の警告が表示された:
The file does not exist at "worker.js?worker_file&type=module" which is in the optimize deps directory. The dependency might be incompatible with the dep optimizer. Try adding it to optimizeDeps.exclude.
原因
FFmpeg.wasmは内部でWeb Workerを使用しておりViteの依存関係最適化(optimizeDeps)と互換性がない。
具体的には:
- FFmpeg.wasmはSharedArrayBufferを使用したマルチスレッド処理を行う
- Web Workerファイルを動的にロードしようとする
- Viteのoptimize depsディレクトリ内でworker.jsが見つからず404エラーになる
検討した解決策
1. Viteの設定変更(未実施)
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
optimizeDeps: {
exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util']
}
}
})
2. COOP/COEPヘッダーの設定(未実施)
FFmpeg.wasmはSharedArrayBufferを使用するため、以下のHTTPヘッダーが必要:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
断念した理由
- 設定の複雑さ: nuxt.config.tsの変更、サーバーヘッダーの設定など、複数の変更が必要
- 副作用のリスク: COOP/COEPヘッダーは他の機能(外部リソースの読み込みなど)に影響を与える可能性がある
- 即時性: 今回は動作確認を優先し、シンプルな解決策を選択
採用した解決策
WebM出力への切り替え
MediaRecorder APIはブラウザネイティブでWebM(VP9)をサポートしているため、FFmpegを経由せずに直接WebMファイルを出力する方式に変更。
変更内容
useAnimationExport.ts:
// 変更前
format = 'mp4',
// 変更後
format = 'webm',
// FFmpeg変換をスキップ
if (format === 'mp4') {
console.warn('MP4出力はFFmpegの設定が必要です。WebMで出力します。')
}
proportional-animation-qqq.vue:
// 変更前
format: 'mp4', // Twitter投稿対応
// 変更後
format: 'webm', // WebMで出力(MP4はFFmpeg設定が必要)
トレードオフ
メリット
- FFmpegの読み込み・変換時間が不要
- 追加の設定変更が不要
- エラーなく動作
デメリット
- Twitter非対応: TwitterはWebMの直接アップロードをサポートしていない
- MP4が必要な場合は外部ツールで変換が必要
今後の対応(必要な場合)
MP4出力が必須になった場合、以下の手順で対応:
nuxt.config.tsにoptimizeDeps.excludeを追加- サーバーミドルウェアでCOOP/COEPヘッダーを設定
- FFmpeg.wasmの初期化コードを調整
- テスト環境と本番環境の両方で動作確認
参考リンク
結論
WebM出力は「動く」ことを優先した実用的な選択。Twitter投稿など特定の要件がある場合は、FFmpegの正式対応を検討する。
補足:なぜバックエンドで変換しなかったのか
問題の本質
今回の問題はフロントエンド(ブラウザ内)でFFmpeg.wasmを使おうとしたことにある。
代替案:バックエンドで変換
[ブラウザ] [サーバー]
WebM生成 → アップロード → ffmpeg変換 → MP4をダウンロード
バックエンド(Node.js等)なら:
- ネイティブffmpegが使える → SharedArrayBuffer不要
- COOP/COEPヘッダー設定不要
- 変換も高速
実装例(概念)
// フロントエンド
const webmBlob = await exportAnimation()
const formData = new FormData()
formData.append('video', webmBlob, 'animation.webm')
const response = await fetch('/api/convert-to-mp4', {
method: 'POST',
body: formData
})
const mp4Blob = await response.blob()
downloadFile(mp4Blob, 'animation.mp4')
// バックエンド (Node.js)
import ffmpeg from 'fluent-ffmpeg'
app.post('/api/convert-to-mp4', async (req, res) => {
ffmpeg(inputPath)
.outputFormat('mp4')
.pipe(res)
})
方式比較
| 方式 | 難易度 | 備考 |
|---|---|---|
| フロントのみ(FFmpeg.wasm) | 高 | ヘッダー設定、Vite互換性問題 |
| バックエンドで変換 | 低〜中 | Node.js + ffmpegで簡単 |
| 外部ツールで手動変換 | 最も簡単 | UXは劣る |
Cloudflare環境での選択肢
Cloudflare Workersではffmpegは直接使えないため:
- Cloudflare R2 + 外部変換サービス
- Cloudflare Stream(動画アップロードサービス)
- 自前のVPS/EC2でffmpegを動かす
今回の判断
バックエンドAPIを新規構築するコストとWebM出力で十分なユースケース(Discord、YouTube等)を考慮しWebM出力で妥協した。MP4が必須になった場合はバックエンド変換APIの追加を検討する。