ffmpeg-webassembly-workers について
ブラウザーやエッジ環境で WebAssembly 経由で FFmpeg を実行するための開発スキルガイドです。ブラウザー内の変換、シングル・マルチスレッド設定、配信ヘッダー、一般的な処理、Cloudflare Workers の制約を説明します。
主な機能
- 01WebAssembly でブラウザー内の FFmpeg を実行
- 02シングル・マルチスレッドコアの設定
- 03COOP/COEP ヘッダーの構成
- 04メディアの変換、トリミング、拡大縮小、抽出
- 05Cloudflare Workers の制約の把握
活用シーン
- 01Web アプリへのクライアント側動画変換の追加
- 02React または Next.js への FFmpeg 処理の導入
- 03クロスオリジン分離によるマルチスレッドの有効化
- 04Cloudflare Workers での動画処理方法の検討
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill ffmpeg-webassembly-workersインストール後、Claude Code で/ffmpeg-webassembly-workersを入力して読み込まれたか確認します。
詳細
重要なガイドライン
Windows のファイルパス要件
必須:Windows ではファイルパスに常にバックスラッシュを使用してください
Windows で Edit または Write ツールを使用する場合は、ファイルパスにスラッシュ(/)ではなく、バックスラッシュ(\)を必ず使用してください。
クイックリファレンス
| パッケージ | サイズ | スレッド処理 | インストール |
|---|---|---|---|
@ffmpeg/core | ~31MB | シングル | npm install @ffmpeg/ffmpeg @ffmpeg/util |
@ffmpeg/core-mt | ~31MB | マルチ | COOP/COEP ヘッダーが必要 |
| ヘッダー | 値 | 用途 |
|---|---|---|
| Cross-Origin-Embedder-Policy | require-corp | SharedArrayBuffer |
| Cross-Origin-Opener-Policy | same-origin | マルチスレッド |
| 操作 | コマンド |
|---|---|
| WebM→MP4 に変換 | await ffmpeg.exec(['-i', 'input.webm', 'output.mp4']) |
| フレームを抽出 | await ffmpeg.exec(['-i', 'video.mp4', '-ss', '5', '-vframes', '1', 'thumb.jpg']) |
このスキルを使用する場面
ブラウザベースの動画処理に使用します:
- サーバーを使わないクライアント側のトランスコード
- React/Vue/Next.js への FFmpeg の統合
- COOP/COEP ヘッダーの設定
- Cloudflare Workers における FFmpeg の制限
- メモリ管理とクリーンアップ
FFmpeg WebAssembly と Cloudflare Workers(2025)
WebAssembly を使用して、ブラウザやエッジ環境で FFmpeg を実行するためのガイドです。
ffmpeg.wasm の概要
ffmpeg.wasm は、サーバー側の処理なしでブラウザ上で直接実行できる、純粋な WebAssembly/JavaScript 版の FFmpeg です。
主な機能
- ブラウザベース:動画処理にサーバーは不要です
- クロスプラットフォーム:最新のブラウザで動作します
- シングルスレッド:@ffmpeg/core(~31MB)
- マルチスレッド:@ffmpeg/core-mt(SharedArrayBuffer が必要)
- カスタマイズ可能:特定のコーデックを含む独自のコアをビルドできます
制限事項
- パフォーマンス:ネイティブ FFmpeg より約 10~100 倍遅くなります
- メモリ:ブラウザのメモリ制限を受けます
- ファイルサイズ:コアは約 31MB です(カスタムビルドで縮小できます)
- スレッド処理:マルチスレッドには特定のヘッダー(COOP/COEP)が必要です
- コーデック:すべてのコーデックを利用できるわけではありません(ライセンス上の制約)
インストール
npm
npm install @ffmpeg/ffmpeg @ffmpeg/util
CDN
<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd/ffmpeg.min.js"></script>
基本的な使い方
シングルスレッド(ブラウザ)
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
const ffmpeg = new FFmpeg();
// Load FFmpeg core
const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
// Transcode video
await ffmpeg.writeFile('input.webm', await fetchFile(videoFile));
await ffmpeg.exec(['-i', 'input.webm', 'output.mp4']);
const data = await ffmpeg.readFile('output.mp4');
// Create blob URL for playback
const videoURL = URL.createObjectURL(
new Blob([data.buffer], { type: 'video/mp4' })
);
マルチスレッド(COOP/COEP ヘッダーが必要)
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
const ffmpeg = new FFmpeg();
// Load multi-threaded core
const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript'),
});
// Use multi-threaded encoding
await ffmpeg.exec(['-i', 'input.webm', '-threads', '4', 'output.mp4']);
クロスオリジン分離(SharedArrayBuffer)
マルチスレッド版の ffmpeg.wasm には SharedArrayBuffer が必要で、これを利用するにはクロスオリジン分離ヘッダーが必要です。
Vite の設定
// vite.config.js
export default {
server: {
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
},
},
optimizeDeps: {
exclude: ["@ffmpeg/ffmpeg", "@ffmpeg/util"],
},
};
Next.js の設定
// next.config.js
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
],
},
];
},
};
Express.js ミドルウェア
import express from 'express';
const app = express();
app.use((req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
next();
});
app.use(express.static('public'));
app.listen(3000);
Nginx の設定
server {
listen 443 ssl;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
location / {
root /var/www/html;
}
}
React との統合
React コンポーネント
import { useState, useRef } from 'react';
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
function VideoTranscoder() {
const [loaded, setLoaded] = useState(false);
const [progress, setProgress] = useState(0);
const [outputURL, setOutputURL] = useState(null);
const ffmpegRef = useRef(new FFmpeg());
const load = async () => {
const ffmpeg = ffmpegRef.current;
// Progress handler
ffmpeg.on('progress', ({ progress }) => {
setProgress(Math.round(progress * 100));
});
const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
setLoaded(true);
};
const transcode = async (file) => {
const ffmpeg = ffmpegRef.current;
await ffmpeg.writeFile('input.webm', await fetchFile(file));
await ffmpeg.exec([
'-i', 'input.webm',
'-c:v', 'libx264',
'-preset', 'ultrafast',
'-c:a', 'aac',
'output.mp4'
]);
const data = await ffmpeg.readFile('output.mp4');
const url = URL.createObjectURL(
new Blob([data.buffer], { type: 'video/mp4' })
);
setOutputURL(url);
};
return (
<div>
{!loaded ? (
<button onClick={load}>Load FFmpeg (~31MB)</button>
) : (
<>
<input
type="file"
accept="video/*"
onChange={(e) => transcode(e.target.files[0])}
/>
<p>Progress: {progress}%</p>
{outputURL && <video src={outputURL} controls />}
</>
)}
</div>
);
}
よくある操作
WebM を MP4 に変換
await ffmpeg.exec(['-i', 'input.webm', '-c:v', 'libx264', 'output.mp4']);
音声を抽出
await ffmpeg.exec(['-i', 'video.mp4', '-vn', '-c:a', 'libmp3lame', 'audio.mp3']);
サムネイルを作成
await ffmpeg.exec(['-i', 'video.mp4', '-ss', '00:00:05', '-vframes', '1', 'thumb.jpg']);
動画をトリミング
await ffmpeg.exec([
'-i', 'input.mp4',
'-ss', '00:00:10',
'-t', '00:00:30',
'-c', 'copy',
'output.mp4'
]);
透かしを追加
await ffmpeg.writeFile('logo.png', await fetchFile(logoFile));
await ffmpeg.exec([
'-i', 'input.mp4',
'-i', 'logo.png',
'-filter_complex', 'overlay=10:10',
'output.mp4'
]);
動画を拡大・縮小
await ffmpeg.exec([
'-i', 'input.mp4',
'-vf', 'scale=1280:720',
'-c:a', 'copy',
'output.mp4'
]);
Cloudflare Workers
現在の制限事項(2025年12月)
Cloudflare Workers で ffmpeg.wasm を実行するには、次のような重大な課題があります。
- サイズ制限:Workers には 10MB の圧縮サイズ制限があります(有料プラン)
- Web Workers 非対応:CF Worker 内からワーカーを起動できません
- SharedArrayBuffer:Workers 環境では利用できません
- 環境検出:ffmpeg.wasm の環境チェックが失敗する場合があります
回避策
1. 外部 FFmpeg サービスを使用する
// Cloudflare Worker calling external FFmpeg API
export default {
async fetch(request) {
const formData = await request.formData();
const video = formData.get('video');
// Send to external FFmpeg service
const response = await fetch('https://your-ffmpeg-api.com/transcode', {
method: 'POST',
body: video,
});
return response;
},
};
2. R2 ストレージと Durable Objects を使用する
// Store video in R2, process with external service
export default {
async fetch(request, env) {
const video = await request.arrayBuffer();
// Store in R2
await env.BUCKET.put('input.mp4', video);
// Trigger external processing
await env.QUEUE.send({
bucket: 'BUCKET',
key: 'input.mp4',
});
return new Response('Processing started');
},
};
3. カスタム ffmpeg.wasm ビルド(実験的)
単純な操作であれば、最小構成のカスタムビルドで制限内に収まる可能性があります。
# Build minimal ffmpeg.wasm (~8MB compressed)
# Only include essential codecs
git clone https://github.com/ffmpegwasm/ffmpeg.wasm
cd ffmpeg.wasm
# Modify build scripts to include only needed codecs
npm run build:core -- --disable-all --enable-libx264
代替案:Cloudflare Stream
Cloudflare エコシステムで動画処理を行う場合は、Cloudflare Stream をご検討ください。
// Upload to Cloudflare Stream for processing
export default {
async fetch(request, env) {
const formData = new FormData();
formData.append('file', await request.arrayBuffer());
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/stream`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${env.API_TOKEN}`,
},
body: formData,
}
);
return response;
},
};
カスタム ffmpeg.wasm コアのビルド
コアサイズを縮小する
# Custom Dockerfile for minimal build
FROM emscripten/emsdk:3.1.50
WORKDIR /src
# Clone FFmpeg
RUN git clone https://github.com/FFmpeg/FFmpeg.git ffmpeg
WORKDIR /src/ffmpeg
# Configure with minimal codecs
RUN emconfigure ./configure \
--target-os=none \
--arch=x86_32 \
--enable-cross-compile \
--disable-x86asm \
--disable-inline-asm \
--disable-stripping \
--disable-programs \
--disable-doc \
--disable-debug \
--disable-runtime-cpudetect \
--disable-autodetect \
--enable-small \
--enable-gpl \
--enable-libx264 \
--extra-cflags="-O3 -s USE_PTHREADS=1" \
--extra-ldflags="-O3 -s USE_PTHREADS=1"
RUN emmake make -j$(nproc)
ビルドスクリプト
#!/bin/bash
# build-minimal.sh
# Configure build
./configure \
--disable-all \
--enable-avcodec \
--enable-avformat \
--enable-avutil \
--enable-swresample \
--enable-swscale \
--enable-decoder=h264 \
--enable-decoder=aac \
--enable-encoder=libx264 \
--enable-encoder=aac \
--enable-muxer=mp4 \
--enable-demuxer=mov \
--enable-protocol=file \
--enable-gpl \
--enable-libx264
make -j$(nproc)
パフォーマンスの最適化
ブラウザでのベストプラクティス
- コアを遅延読み込みする - 必要になるまで読み込まないでください
- Web Workers を使用する - メインスレッドの応答性を保ちます
- ストリーム処理を使用する - 大きなファイルはチャンク単位で処理します
- コアをキャッシュする - IndexedDB または Cache API に保存します
- 進捗を表示する - UX のために進捗イベントを使用します
ffmpeg-core のキャッシュ
// Cache ffmpeg core in IndexedDB
async function loadCachedFFmpeg() {
const cacheKey = 'ffmpeg-core-0.12.10';
// Check cache
const cached = await getCachedCore(cacheKey);
if (cached) {
await ffmpeg.load({
coreURL: URL.createObjectURL(cached.core),
wasmURL: URL.createObjectURL(cached.wasm),
});
return;
}
// Download and cache
const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd';
const [core, wasm] = await Promise.all([
fetch(`${baseURL}/ffmpeg-core.js`).then(r => r.blob()),
fetch(`${baseURL}/ffmpeg-core.wasm`).then(r => r.blob()),
]);
await cacheCore(cacheKey, { core, wasm });
await ffmpeg.load({
coreURL: URL.createObjectURL(core),
wasmURL: URL.createObjectURL(wasm),
});
}
メモリ管理
// Clean up after processing
async function processAndCleanup(inputFile) {
const ffmpeg = new FFmpeg();
await ffmpeg.load({ ... });
try {
await ffmpeg.writeFile('input.mp4', await fetchFile(inputFile));
await ffmpeg.exec(['-i', 'input.mp4', 'output.mp4']);
const data = await ffmpeg.readFile('output.mp4');
// Clean up virtual filesystem
await ffmpeg.deleteFile('input.mp4');
await ffmpeg.deleteFile('output.mp4');
return data;
} finally {
// Terminate FFmpeg to free memory
ffmpeg.terminate();
}
}
トラブルシューティング
「SharedArrayBuffer is not defined」
原因:Cross-Origin Isolation ヘッダーがありません
解決策:サーバー構成に COOP/COEP ヘッダーを追加します
「Worker is not defined」(Cloudflare Workers)
原因:ffmpeg.wasm は Web Workers の起動を試みますが、CF Workers では利用できません
解決策:シングルスレッドコアまたは外部 FFmpeg サービスを使用します
メモリ不足
原因:大きな動画ファイルによってブラウザーのメモリが使い果たされます
解決策:
- チャンク単位で処理します
- 解像度または品質を下げます
- マルチスレッドコアを使用します(より効率的です)
- ストリーミング処理を実装します
エンコードが遅い
原因:WebAssembly はネイティブより低速です
解決策:
-preset ultrafastを使用します- エンコード前に解像度を下げます
- 利用可能な場合はハードウェアアクセラレーションを使用します(実験的)
- 負荷の高い処理ではサーバー側での処理を検討します
このガイドでは、ffmpeg.wasm と WebAssembly のデプロイについて説明しています。ネイティブ FFmpeg については、ほかのスキルドキュメントを参照してください。