浏览器 ffmpeg.wasm 视频压缩:Next.js 静态站集成完整指南
摘要
想在网页里做「视频压缩 / 格式转换」,又不想把用户文件上传到服务器?ffmpeg.wasm 是目前最成熟的方案之一:在浏览器里跑 FFmpeg 的 WebAssembly 版本,全程本地转码。本文以 Tools Ku(工具库) 的生产实现为例,讲清 Next.js 静态导出站点 如何集成 ffmpeg.wasm、踩过哪些坑(UMD vs ESM、Worker、核心包 30MB 加载),并给出可复用的 Shell + Hook + 转码管线 设计。文末附 免费在线视频压缩 链接:无需注册、无需登录、文件不上传服务器。
在线体验(免费 · 免登录 · 本地转码)
一、为什么选 ffmpeg.wasm?
| 方案 | 优点 | 缺点 |
|---|---|---|
| 服务端 FFmpeg | 性能好、格式全 | 需上传、带宽与合规成本高 |
| 浏览器 MediaRecorder | 实现简单 | 控制能力弱,难做精细压缩 |
| ffmpeg.wasm | 命令行级能力、数据不出本机 | 首次加载大(~30MB)、CPU 吃紧、慢 |
适合场景:在线工具站、隐私敏感视频、免后端转码的静态站(OSS + CDN 托管 HTML/JS)。
与「PDF 转 Word 走云端 DocMind」不同,视频压缩在 Tools Ku 上刻意做成 100% 浏览器本地,页面上会明确提示:转码在本地完成,不会上传到 toolsku.com 或任何第三方服务器。
二、整体架构
请求路径上没有你的转码 API:用户选文件 → 写入 WASM 内存文件系统 → ffmpeg.exec(...) → 读出 Uint8Array → Blob 下载。
三、Next.js 静态站要注意什么?
项目使用 output: "export" 纯静态导出,页面在构建时生成 HTML,运行时没有 Node 服务端。ffmpeg 相关代码必须满足:
- 仅客户端执行:组件文件顶行
"use client"。 - 动态 import:
@ffmpeg/ffmpeg、@ffmpeg/util在点击/进入页面后再加载,避免打进首屏大包。 serverExternalPackages:构建阶段不要把 ffmpeg 打进服务端 bundle:
// next.config.ts(节选)
serverExternalPackages: [
"@ffmpeg/ffmpeg",
"@ffmpeg/util",
"@ffmpeg/core",
],
- 开发环境:官方 Worker 与 Turbopack 组合易出问题;项目建议用
npm run dev(文档中注明勿用dev:turbo调试视频页)。
四、关键踩坑:必须用 UMD,不要用 ESM
很多人照抄官方 ESM 示例 + toBlobURL:
// ❌ 在 Next.js + Worker 场景下常失败
const baseURL = `https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm`;
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
});
Worker 内若走 import(https://...),在 Next 打包的 Worker 里常会报 Cannot find module。
正确做法:使用 @ffmpeg/core 的 UMD 构建,让 Worker 通过 importScripts(coreURL) 加载:
const FFMPEG_UMD_SUBPATH = `dist/umd`;
async function loadFfmpegCoreFromBase(ffmpeg, baseURL, onLog?) {
const coreURL = `${baseURL}/ffmpeg-core.js`;
const wasmURL = `${baseURL}/ffmpeg-core.wasm`;
await ffmpeg.load({ coreURL, wasmURL }); // 不要 toBlobURL
}
加载顺序(失败自动换源):
- 环境变量
NEXT_PUBLIC_FFMPEG_CORE_BASE(可指向自建 OSS) - jsDelivr UMD
- unpkg UMD
- 同源
https://你的域名/ffmpeg/0.12.6/
五、把 core 拷到 public:postinstall 脚本
核心包约 30MB,不宜打进 JS bundle,而是放到 public/ 随静态站一起部署:
// scripts/copy-ffmpeg-core.mjs(逻辑摘要)
// 从 node_modules/@ffmpeg/core/dist/umd 复制到 public/ffmpeg/0.12.6/
cpSync("ffmpeg-core.js", "public/ffmpeg/0.12.6/");
cpSync("ffmpeg-core.wasm", "public/ffmpeg/0.12.6/");
package.json 中:
"postinstall": "node scripts/copy-ffmpeg-core.mjs && ..."
生产构建后,用户优先从 同源 拉 core,速度、稳定性都优于纯 CDN,且不依赖外网策略。
六、单例加载:避免重复 30MB 下载
多个视频工具页(压缩、转 MP4、剪切、GIF)共享一个 FFmpeg 实例:
let instance: FFmpeg | null = null;
let loadPromise: Promise<FFmpeg> | null = null;
export async function loadFfmpegSingleton(handlers?) {
if (instance) {
// 复用实例,仅重新绑定 log/progress
return instance;
}
if (loadPromise) return loadPromise;
loadPromise = (async () => {
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
const ffmpeg = new FFmpeg();
ffmpeg.on("log", ...);
ffmpeg.on("progress", ...);
for (const baseURL of getLoadBaseUrls()) {
try {
await loadFfmpegCoreFromBase(ffmpeg, baseURL);
instance = ffmpeg;
return ffmpeg;
} catch (e) { /* 尝试下一个源 */ }
}
throw new Error("LOAD_FAILED: ...");
})();
return loadPromise;
}
进入 /video/compress 时 自动开始加载 core,界面显示「正在加载视频处理组件…」,加载完成后再展示上传区。
七、转码管线:writeFile → exec → readFile → 清理
统一封装在 ffmpegTranscode:
export async function ffmpegTranscode(
ffmpeg: FFmpeg,
file: File,
outputFileName: string,
args: string[],
): Promise<Uint8Array> {
const { fetchFile } = await import("@ffmpeg/util");
const inputName = virtualInputName(file); // 如 input.mp4
await ffmpeg.writeFile(inputName, await fetchFile(file));
const code = await ffmpeg.exec(["-y", "-i", inputName, ...args, outputFileName]);
if (code !== 0) throw new FfmpegExitError(code);
const data = await ffmpeg.readFile(outputFileName);
await ffmpeg.deleteFile(inputName);
await ffmpeg.deleteFile(outputFileName);
return data instanceof Uint8Array ? data : new TextEncoder().encode(String(data));
}
-y:覆盖输出- 失败时抛
FfmpegExitError(exitCode),UI 可展示「FFmpeg 退出码」 - 转码结束务必 deleteFile,否则大文件多次操作会撑爆 WASM 内存
八、视频压缩参数:分辨率 + CRF
压缩不是简单改扩展名,而是 重新编码。项目预设:
export function buildCompressArgs(height: number, crf: number): string[] {
return [
"-vf", `scale=-2:${height}`, // 高度限制,宽度按比例 -2 保证偶数
"-c:v", "libx264",
"-crf", String(crf), // 18~36,越大体积越小、画质越低
"-preset", "fast", // 编码速度预设
"-c:a", "aac",
"-b:a", "128k",
];
}
UI 提供:
- 高度:1080 / 720 / 480 / 360
- CRF 滑块:18–36,默认约 28
等价命令行概念:
ffmpeg -y -i input.mp4 -vf scale=-2:720 -c:v libx264 -crf 28 -preset fast -c:a aac -b:a 128k output.mp4
CRF 经验值(与产品文案一致):
| CRF | 适用 |
|---|---|
| ~23 | 画质优先 |
| 28–32 | 分享、附件、体积优先 |
九、UI 分层:Shell + 业务 Client
1. VideoFfmpegShell(通用外壳)
- 自动
load()ffmpeg - 文件拖拽 / 大小限制提示
- 进度条(
progress事件 0–100%) - FFmpeg 日志面板(最近 120 行)
- 通过 render props 把
transcode交给子组件
2. VideoCompressClient(压缩业务)
<VideoFfmpegShell accept="video/*">
{({ file, busy, transcode }) => (
<>
{/* 高度、CRF 控件 */}
<button onClick={async () => {
const blob = await transcode(file, "output.mp4", buildCompressArgs(height, crf));
setResult(blob);
}}>开始压缩</button>
<VideoDownloadLink blob={result} fileName={`${base}_compressed.mp4`} />
</>
)}
</VideoFfmpegShell>
3. useFfmpegTool Hook
loaded / loading / busy / progress / log / errtranscode(file, outputFileName, args)→Blob- 单文件上限 200MB(
FFMPEG_MAX_FILE_BYTES),超出返回FILE_TOO_LARGE
同一套 Shell 复用在 转 MP4、剪切、提取音频、转 GIF 等页,仅 buildXxxArgs 不同——这是工具站可维护性的关键。
十、限制与用户体验(写进文档 = 少被喷)
| 项 | 说明 |
|---|---|
| 首次加载 | core 约 30MB,需等待,应用 loading 态 |
| 转码速度 | 依赖 CPU,长视频 + 1080p 可能数分钟~十几分钟 |
| 浏览器 | 建议 Chrome / Edge 桌面版,移动端易 OOM |
| 文件大小 | 建议 ≤200MB(内存为文件数倍) |
| 格式 | 取决于 wasm 内建解码器,常见 MP4/MOV/WebM 可用 |
| 隐私 | 本地转码,不上传;但仍勿在公共电脑处理机密素材 |
进度条来自 FFmpeg 的 progress 事件,不是假动画,用户等待时心理负担更小。
十一、与「云端转码」如何选型?
| 维度 | ffmpeg.wasm(Tools Ku 视频) | 云端 API(如 PDF→Word) |
|---|---|---|
| 隐私 | 文件不出本机 | 需上传 |
| 成本 | 用户承担 CPU,你无算力账单 | 按量计费 |
| 能力 | 受 wasm 与内存限制 | 更强、更快 |
| 部署 | 纯静态站即可 | 需代理 / 函数计算 |
工具站策略:视频走本地,Office 高保真走云端,在文案里写清楚,用户信任度更高。
十二、本地开发与部署清单
# 1. 安装依赖(自动 copy ffmpeg core 到 public)
npm install
# 2. 开发
npm run dev
# 打开 http://localhost:3002/zh-CN/video/compress/
# 3. 生产构建(静态 export)
npm run build
# 将 out/ 上传到 OSS/CDN,确保 public/ffmpeg/0.12.6/ 可访问
可选环境变量:
# 自建 OSS 托管 core,减轻主站带宽
NEXT_PUBLIC_FFMPEG_CORE_BASE=https://your-cdn.com/ffmpeg/0.12.6
十三、FAQ
Q:视频会上传到服务器吗?
不会。Tools Ku 视频压缩使用 ffmpeg.wasm,转码在浏览器完成,免费使用、无需注册登录。
Q:为什么第一次很慢?
需下载 wasm core;之后同会话复用单例,不再重复下载。
Q:压缩后变模糊?
降低 CRF 或提高输出高度(如 720→1080)。
Q:能否批量?
当前单文件;批量需排队 + 注意内存,可后续扩展。
Q:和桌面 FFmpeg 画质一样吗?
编码器同为 x264/aac 思路,但 preset、线程与 wasm 性能有差异,极端场景桌面版更稳。
十四、总结
- 静态 Next 站 + ffmpeg.wasm = 零转码后端、隐私友好、适合工具站。
- UMD + importScripts 是 Next 场景下的稳定加载方式,ESM+toBlobURL 易踩 Worker 坑。
- public 同源托管 core + CDN 降级 + 单例,决定首屏体验与成功率。
ffmpegTranscode统一管线 +VideoFfmpegShell复用,多个视频工具只需换参数预设。- 压缩 = scale + libx264 + CRF,产品上要给用户 CRF/分辨率说明,并诚实告知耗时。
立即体验(免费 · 免登录 · 本地处理)
- 工具库:https://www.toolsku.com/zh-CN
- 视频压缩:https://www.toolsku.com/zh-CN/video/compress/
- 视频转 MP4:https://www.toolsku.com/zh-CN/video/to-mp4/
- 视频剪切:https://www.toolsku.com/zh-CN/video/trim/
- 视频转 GIF:https://www.toolsku.com/zh-CN/video/to-gif/
- 提取音频:https://www.toolsku.com/zh-CN/video/extract-audio/
打开即用,无需安装客户端,无需注册账号。
原创声明:本文架构与代码思路基于开源项目 Tools Ku(https://www.toolsku.com)中的 ffmpeg-singleton、transcode、VideoFfmpegShell 等实现,转载请注明出处。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)