关于 ffmpeg-cloudflare-containers
ffmpeg-cloudflare-containers 是关于在 Cloudflare Containers 中运行原生 FFmpeg 的实践指南,面向构建边缘视频处理服务的开发者。内容包括容器配置、GPU 转码、扩缩容、R2 存储集成和部署。
主要功能
- 01配置 Cloudflare Containers 与 FFmpeg
- 02介绍 GPU 加速转码
- 03提供自动扩缩容配置示例
- 04涵盖大型文件流式处理
- 05说明 R2 集成、部署与监控
适用场景
- 01在边缘处理大型视频文件
- 02构建 GPU 加速转码流程
- 03生成视频缩略图或提取音频
- 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 |
| Dockerfile | FROM jrottenberg/ffmpeg:7.1-alpine |
| wrangler.toml | 使用 class_name 和 image 的 [[containers]] |
| 功能 | Workers(ffmpeg.wasm) | Containers(原生) |
|---|---|---|
| 大小限制 | 10MB | 无限制 |
| 性能 | 慢 10-100 倍 | 原生速度 |
| GPU 支持 | 无 | 提供 NVIDIA GPU |
| 冷启动 | 即时 | 2-3 秒 |
何时使用此技能
适用于大规模边缘视频处理:
- 在 Cloudflare 边缘实现原生 FFmpeg 性能
- 在容器中进行 GPU 加速转码
- 处理大型文件(>10MB)
- 复杂的 FFmpeg 工作流
- 视频文件的 R2 存储集成
Cloudflare Containers 中的 FFmpeg(2025)
概述
Cloudflare Containers(于 2025 年 6 月推出)支持在边缘的完整 Linux 容器环境中原生运行 FFmpeg。这克服了基于 Workers 的方案的所有限制(ffmpeg.wasm 大小限制、SharedArrayBuffer 限制)。
主要优势
| 功能 | Workers(ffmpeg.wasm) | Containers(原生) |
|---|---|---|
| 二进制文件大小 | 限制为 10MB | 无限制(磁盘空间) |
| 性能 | WebAssembly 开销 | 原生速度 |
| GPU 支持 | 无 | 提供 NVIDIA GPU |
| 文件系统 | 仅虚拟/内存 | 完整的 Linux 文件系统 |
| 依赖项 | 必须编译为 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 });
}
}
参考资料
评论
支持 Markdown