mcpField
F
ffmpeg-webassembly-workers

ffmpeg-webassembly-workers

github.com/josiahsiegel開発5974 回インストール19 週間前に更新
リポジトリ

ffmpeg-webassembly-workers について

ブラウザーやエッジ環境で WebAssembly 経由で FFmpeg を実行するための開発スキルガイドです。ブラウザー内の変換、シングル・マルチスレッド設定、配信ヘッダー、一般的な処理、Cloudflare Workers の制約を説明します。

主な機能

  1. 01WebAssembly でブラウザー内の FFmpeg を実行
  2. 02シングル・マルチスレッドコアの設定
  3. 03COOP/COEP ヘッダーの構成
  4. 04メディアの変換、トリミング、拡大縮小、抽出
  5. 05Cloudflare Workers の制約の把握

活用シーン

  1. 01Web アプリへのクライアント側動画変換の追加
  2. 02React または Next.js への FFmpeg 処理の導入
  3. 03クロスオリジン分離によるマルチスレッドの有効化
  4. 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-Policyrequire-corpSharedArrayBuffer
Cross-Origin-Opener-Policysame-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 を実行するには、次のような重大な課題があります。

  1. サイズ制限:Workers には 10MB の圧縮サイズ制限があります(有料プラン)
  2. Web Workers 非対応:CF Worker 内からワーカーを起動できません
  3. SharedArrayBuffer:Workers 環境では利用できません
  4. 環境検出: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)

パフォーマンスの最適化

ブラウザでのベストプラクティス
  1. コアを遅延読み込みする - 必要になるまで読み込まないでください
  2. Web Workers を使用する - メインスレッドの応答性を保ちます
  3. ストリーム処理を使用する - 大きなファイルはチャンク単位で処理します
  4. コアをキャッシュする - IndexedDB または Cache API に保存します
  5. 進捗を表示する - 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 サービスを使用します

メモリ不足

原因:大きな動画ファイルによってブラウザーのメモリが使い果たされます

解決策:

  1. チャンク単位で処理します
  2. 解像度または品質を下げます
  3. マルチスレッドコアを使用します(より効率的です)
  4. ストリーミング処理を実装します
エンコードが遅い

原因:WebAssembly はネイティブより低速です

解決策:

  1. -preset ultrafast を使用します
  2. エンコード前に解像度を下げます
  3. 利用可能な場合はハードウェアアクセラレーションを使用します(実験的)
  4. 負荷の高い処理ではサーバー側での処理を検討します

このガイドでは、ffmpeg.wasm と WebAssembly のデプロイについて説明しています。ネイティブ FFmpeg については、ほかのスキルドキュメントを参照してください。

コメント