关于 ffmpeg-webassembly-workers
这是一份开发技能指南,介绍如何在浏览器和边缘环境中通过 WebAssembly 运行 FFmpeg。内容涵盖浏览器转码、单线程与多线程设置、部署响应头、常见操作和 Cloudflare Workers 限制。
主要功能
- 01在浏览器中通过 WebAssembly 运行 FFmpeg
- 02配置单线程和多线程核心
- 03设置 COOP/COEP 响应头
- 04转换、裁剪、缩放和提取媒体
- 05了解 Cloudflare Workers 的限制
适用场景
- 01在 Web 应用中加入客户端视频转换
- 02为 React 或 Next.js 集成 FFmpeg 处理
- 03通过跨源隔离启用多线程
- 04规划 Cloudflare 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 内部创建 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. 将 Durable Objects 与 R2 存储搭配使用
// 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 中
- 显示进度——使用进度事件改善用户体验
缓存 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”
原因:缺少跨源隔离标头
解决方案:在服务器配置中添加 COOP/COEP 标头
“Worker is not defined”(Cloudflare Workers)
原因:ffmpeg.wasm 尝试启动 Web Workers,但 CF Workers 不支持
解决方案:使用单线程核心或外部 FFmpeg 服务
内存不足
原因:大型视频文件耗尽浏览器内存
解决方案:
- 分块处理
- 使用较低的分辨率/画质
- 使用多线程核心(效率更高)
- 实现流式处理
编码速度慢
原因:WebAssembly 比原生程序慢
解决方案:
- 使用
-preset ultrafast - 编码前降低分辨率
- 如果可用,使用硬件加速(实验性)
- 对于繁重的工作负载,可考虑在服务器端处理
本指南介绍了 ffmpeg.wasm 和 WebAssembly 部署。有关原生 FFmpeg,请参阅其他技能文档。