mcpField
F
ffmpeg-cloudflare-containers

ffmpeg-cloudflare-containers

github.com/josiahsiegelクラウド・DevOps5972 回インストール19 週間前に更新
リポジトリ

ffmpeg-cloudflare-containers について

ffmpeg-cloudflare-containers は、Cloudflare Containers でネイティブ FFmpeg を動かす実践ガイドです。エッジ動画処理を構築する開発者向けに、コンテナ設定や GPU トランスコード、拡張、R2 連携、デプロイを解説します。

主な機能

  1. 01Cloudflare Containers と FFmpeg を設定
  2. 02GPU アクセラレーションによる変換を紹介
  3. 03オートスケーリング設定例を提示
  4. 04大容量ファイルのストリーミングに対応
  5. 05R2 連携、デプロイ、監視を解説

活用シーン

  1. 01エッジで大容量動画を処理
  2. 02GPU 加速のトランスコード処理を構築
  3. 03動画サムネイル生成や音声抽出
  4. 04コンテナ経由でライブ配信を中継

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill ffmpeg-cloudflare-containers

インストール後、Claude Code で/ffmpeg-cloudflare-containersを入力して読み込まれたか確認します。

詳細

クイックリファレンス

コンポーネント設定
コンテナクラスexport class FFmpegContainer extends Container
DockerfileFROM jrottenberg/ffmpeg:7.1-alpine
wrangler.tomlclass_name と image を含む [[containers]]
機能Workers(ffmpeg.wasm)Containers(ネイティブ)
サイズ制限10MB無制限
パフォーマンス10~100倍遅いネイティブ速度
GPU サポートなしNVIDIA を利用可能
コールドスタート即時2~3 秒

このスキルを使用する場面

大規模なエッジ動画処理に使用します:

  • Cloudflare エッジでのネイティブ FFmpeg パフォーマンス
  • コンテナ内での GPU アクセラレーションによるトランスコード
  • 大容量ファイルの処理(>10MB)
  • 複雑な FFmpeg パイプライン
  • 動画ファイル向けの R2 ストレージ統合

Cloudflare Containers での FFmpeg(2025)

概要

2025年6月にローンチされた Cloudflare Containers により、エッジのフル Linux コンテナ環境で FFmpeg をネイティブに実行できます。これにより、Workers ベースのアプローチにおける制限(ffmpeg.wasm のサイズ制限、SharedArrayBuffer の制約)をすべて解消できます。

主なメリット

機能Workers(ffmpeg.wasm)Containers(ネイティブ)
バイナリサイズ10MB に制限無制限(ディスク容量による)
パフォーマンスWebAssembly のオーバーヘッドネイティブ速度
GPU サポートなしNVIDIA GPU を利用可能
ファイルシステム仮想/メモリのみフル Linux FS
依存関係WASM にコンパイルする必要あり任意の Linux パッケージ
コールドスタート即時2~3 秒

はじめに

前提条件
# Docker must be running locally
docker info

# Verify wrangler installation
npx wrangler --version
コンテナプロジェクトの作成
# Create from template
npm create cloudflare@latest -- --template=cloudflare/templates/containers-template

# Or manually configure existing project
wrangler.toml の設定
name = "ffmpeg-processor"
main = "src/index.ts"
compatibility_date = "2025-12-19"

# Container configuration
[[containers]]
class_name = "FFmpegContainer"
image = "./Dockerfile"
max_instances = 10

# Durable Object binding for container
[[durable_objects.bindings]]
name = "FFMPEG_CONTAINER"
class_name = "FFmpegContainer"

# Required migration for SQLite-backed containers
[[migrations]]
tag = "v1"
new_sqlite_classes = ["FFmpegContainer"]
FFmpeg 用 Dockerfile
# Use official FFmpeg image
FROM jrottenberg/ffmpeg:7.1-alpine AS ffmpeg

# Production image
FROM node:22-alpine

# Install FFmpeg from the official image
COPY --from=ffmpeg /usr/local /usr/local

# Install additional dependencies
RUN apk add --no-cache \
    libva \
    libva-utils \
    mesa-va-gallium

# Set up application
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Container must listen on a port
EXPOSE 8080

CMD ["node", "server.js"]
Worker コード
// src/index.ts
import { Container, DurableObject } from "cloudflare:workers";

interface Env {
  FFMPEG_CONTAINER: DurableObjectNamespace<FFmpegContainer>;
}

export class FFmpegContainer extends Container {
  // Default port the container listens on
  defaultPort = 8080;

  // Sleep after 5 minutes of inactivity
  sleepAfter = "5m";

  // Environment variables for container
  envVars = {
    NODE_ENV: "production",
    FFMPEG_PATH: "/usr/local/bin/ffmpeg"
  };

  async onStart() {
    console.log("FFmpeg container started");
  }

  async onStop() {
    console.log("FFmpeg container stopped");
  }

  async onError(error: Error) {
    console.error("Container error:", error);
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Route to container for video processing
    if (url.pathname.startsWith("/process")) {
      // Create unique container instance per job
      const id = env.FFMPEG_CONTAINER.idFromName(crypto.randomUUID());
      const container = env.FFMPEG_CONTAINER.get(id);

      // Forward request to container
      return container.fetch(request);
    }

    // Load balance across containers
    if (url.pathname === "/lb") {
      const id = env.FFMPEG_CONTAINER.newUniqueId();
      const container = env.FFMPEG_CONTAINER.get(id);
      return container.fetch(request);
    }

    return new Response("FFmpeg Container Service", { status: 200 });
  }
};
コンテナサーバー(Node.js)
// server.js - Runs inside the container
import express from 'express';
import { spawn } from 'child_process';
import fs from 'fs/promises';
import path from 'path';

const app = express();
app.use(express.raw({ type: '*/*', limit: '100mb' }));

const TEMP_DIR = '/tmp/ffmpeg-work';

app.post('/transcode', async (req, res) => {
  const jobId = Date.now().toString();
  const workDir = path.join(TEMP_DIR, jobId);

  await fs.mkdir(workDir, { recursive: true });

  try {
    // Write input file
    const inputPath = path.join(workDir, 'input');
    await fs.writeFile(inputPath, req.body);

    // Get output format from query
    const format = req.query.format || 'mp4';
    const outputPath = path.join(workDir, `output.${format}`);

    // Run FFmpeg
    const result = await runFFmpeg([
      '-i', inputPath,
      '-c:v', 'libx264',
      '-preset', 'veryfast',
      '-crf', '23',
      '-c:a', 'aac',
      '-b:a', '128k',
      '-movflags', '+faststart',
      outputPath
    ]);

    if (result.exitCode !== 0) {
      throw new Error(result.stderr);
    }

    // Return processed file
    const output = await fs.readFile(outputPath);
    res.set('Content-Type', `video/${format}`);
    res.send(output);

  } finally {
    // Cleanup
    await fs.rm(workDir, { recursive: true, force: true });
  }
});

app.post('/gif', async (req, res) => {
  const jobId = Date.now().toString();
  const workDir = path.join(TEMP_DIR, jobId);

  await fs.mkdir(workDir, { recursive: true });

  try {
    const inputPath = path.join(workDir, 'input');
    const palettePath = path.join(workDir, 'palette.png');
    const outputPath = path.join(workDir, 'output.gif');

    await fs.writeFile(inputPath, req.body);

    // Two-pass GIF for better quality
    // Pass 1: Generate palette
    await runFFmpeg([
      '-i', inputPath,
      '-vf', 'fps=15,scale=480:-1:flags=lanczos,palettegen',
      '-y', palettePath
    ]);

    // Pass 2: Generate GIF with palette
    await runFFmpeg([
      '-i', inputPath,
      '-i', palettePath,
      '-lavfi', 'fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse',
      '-y', outputPath
    ]);

    const output = await fs.readFile(outputPath);
    res.set('Content-Type', 'image/gif');
    res.send(output);

  } finally {
    await fs.rm(workDir, { recursive: true, force: true });
  }
});

app.get('/health', (req, res) => {
  res.json({ status: 'healthy', ffmpeg: process.env.FFMPEG_PATH });
});

function runFFmpeg(args) {
  return new Promise((resolve) => {
    const proc = spawn('ffmpeg', args, { stdio: ['pipe', 'pipe', 'pipe'] });

    let stdout = '';
    let stderr = '';

    proc.stdout.on('data', (data) => { stdout += data; });
    proc.stderr.on('data', (data) => { stderr += data; });

    proc.on('close', (exitCode) => {
      resolve({ exitCode, stdout, stderr });
    });
  });
}

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`FFmpeg server listening on port ${PORT}`);
});

GPU アクセラレーション対応コンテナ

Cloudflare Containers は、ハードウェアアクセラレーションによるエンコードに NVIDIA GPU をサポートしています。

GPU コンテナの設定
[[containers]]
class_name = "FFmpegGPU"
image = "./Dockerfile.gpu"
max_instances = 5
# GPU instances have higher resource allocation
GPU Dockerfile
FROM nvidia/cuda:12.4-runtime-ubuntu24.04

# Install FFmpeg with NVIDIA support
RUN apt-get update && apt-get install -y \
    ffmpeg \
    libnvidia-encode-550 \
    libnvidia-decode-550 \
    && rm -rf /var/lib/apt/lists/*

# Verify NVENC support
RUN ffmpeg -encoders | grep nvenc

WORKDIR /app
COPY . .

EXPOSE 8080
CMD ["node", "server.js"]
コンテナ内での NVENC エンコード
app.post('/transcode-gpu', async (req, res) => {
  const args = [
    '-hwaccel', 'cuda',
    '-hwaccel_output_format', 'cuda',
    '-i', inputPath,
    '-c:v', 'h264_nvenc',
    '-preset', 'p4',
    '-cq', '23',
    '-c:a', 'aac',
    '-b:a', '128k',
    outputPath
  ];

  await runFFmpeg(args);
});

オートスケーリングの設定

export class FFmpegContainer extends Container {
  // Autoscaling based on CPU usage
  static scaling = {
    minInstances: 1,      // Always keep one warm
    maxInstances: 20,
    targetCPU: 75,        // Scale up at 75% CPU
    scaleDownDelay: "5m"  // Wait before scaling down
  };

  sleepAfter = "10m";  // Sleep idle containers after 10 min
}

料金に関する考慮事項

指標課金
コンピューティングアクティブな間、10ms ごと
アイドル状態課金なし(コンテナはスリープ)
ストレージGB-月あたり
エグレスCloudflare の標準エグレス

ベストプラクティス

1. コンテナサイズの最適化
# Use multi-stage builds
FROM jrottenberg/ffmpeg:7.1-alpine AS ffmpeg
FROM node:22-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Final minimal image
FROM node:22-alpine
COPY --from=ffmpeg /usr/local/bin/ffmpeg /usr/local/bin/
COPY --from=ffmpeg /usr/local/bin/ffprobe /usr/local/bin/
COPY --from=builder /app/node_modules ./node_modules
COPY . .

CMD ["node", "server.js"]
2. ウォームインスタンス戦略
// Keep containers warm for low-latency
export class FFmpegContainer extends Container {
  sleepAfter = "30m";  // Longer idle before sleep
}

// Pre-warm containers on deploy
export default {
  async scheduled(controller: ScheduledController, env: Env) {
    // Ping containers to keep them warm
    const id = env.FFMPEG_CONTAINER.idFromName("warmup");
    const container = env.FFMPEG_CONTAINER.get(id);
    await container.fetch(new Request("http://internal/health"));
  }
};
3. 大容量ファイルのストリーミング
// Stream output instead of buffering
app.post('/transcode-stream', async (req, res) => {
  const ffmpeg = spawn('ffmpeg', [
    '-i', 'pipe:0',
    '-c:v', 'libx264',
    '-f', 'mp4',
    '-movflags', 'frag_keyframe+empty_moov',
    'pipe:1'
  ]);

  req.pipe(ffmpeg.stdin);
  ffmpeg.stdout.pipe(res);

  ffmpeg.stderr.on('data', (data) => {
    console.log('FFmpeg:', data.toString());
  });
});
4. R2 ストレージ統合
// Store processed files in R2
export default {
  async fetch(request: Request, env: Env) {
    // Process video in container
    const result = await container.fetch(request);

    // Store in R2
    const key = `processed/${Date.now()}.mp4`;
    await env.R2_BUCKET.put(key, result.body);

    return new Response(JSON.stringify({ key }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

一般的なユースケース

エッジでの動画サムネイル生成
app.post('/thumbnail', async (req, res) => {
  await runFFmpeg([
    '-i', inputPath,
    '-ss', '00:00:01',
    '-vframes', '1',
    '-vf', 'scale=320:-1',
    outputPath
  ]);
});
音声の抽出
app.post('/extract-audio', async (req, res) => {
  await runFFmpeg([
    '-i', inputPath,
    '-vn',
    '-c:a', 'aac',
    '-b:a', '192k',
    outputPath
  ]);
});
ライブストリーミングのリレー
// Relay RTMP to HLS at edge
const ffmpeg = spawn('ffmpeg', [
  '-i', rtmpInput,
  '-c:v', 'copy',
  '-c:a', 'copy',
  '-hls_time', '4',
  '-hls_list_size', '10',
  '-hls_flags', 'delete_segments',
  '/output/stream.m3u8'
]);

デプロイ

# Deploy to Cloudflare
wrangler deploy

# First deployment takes several minutes
# Subsequent deploys are faster with incremental image updates

モニタリング

// Health check endpoint
app.get('/metrics', async (req, res) => {
  const metrics = {
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    deploymentId: process.env.CLOUDFLARE_DEPLOYMENT_ID
  };
  res.json(metrics);
});

比較:Workers と Containers

ユースケース推奨
シンプルなフィルター(リサイズ、クロップ)Workers + ffmpeg.wasm
フルサイズのトランスコードContainers
GPU アクセラレーションContainers(GPU インスタンス)
低レイテンシー(<100ms)Workers
大容量ファイル(>10MB)Containers
複雑なパイプラインContainers

トラブルシューティング

コンテナが起動しない
# Check Docker locally
docker build -t test-ffmpeg .
docker run --rm test-ffmpeg ffmpeg -version

# Verify architecture
docker inspect test-ffmpeg | grep Architecture
# Must be linux/amd64
FFmpeg が見つからない
# Ensure FFmpeg is in PATH
ENV PATH="/usr/local/bin:${PATH}"

# Verify installation
RUN which ffmpeg && ffmpeg -version
タイムアウトの問題
// Increase timeout for long operations
export class FFmpegContainer extends Container {
  // Extend request timeout
  async fetch(request: Request) {
    // Container internal timeout handling
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 300000); // 5 min

    return super.fetch(request, { signal: controller.signal });
  }
}

参考資料

コメント