摘要

想在网页里做「视频压缩 / 格式转换」,又不想把用户文件上传到服务器?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 或任何第三方服务器


二、整体架构

用户浏览器

核心加载源(按优先级)

同源 public/ffmpeg/0.12.6

jsDelivr UMD

unpkg UMD

Next.js 静态页 /video/compress

VideoFfmpegShell

useFfmpegTool

loadFfmpegSingleton

ffmpeg-core.js + .wasm

FFmpeg 虚拟文件系统

请求路径上没有你的转码 API:用户选文件 → 写入 WASM 内存文件系统 → ffmpeg.exec(...) → 读出 Uint8ArrayBlob 下载。


三、Next.js 静态站要注意什么?

项目使用 output: "export" 纯静态导出,页面在构建时生成 HTML,运行时没有 Node 服务端。ffmpeg 相关代码必须满足:

  1. 仅客户端执行:组件文件顶行 "use client"
  2. 动态 import@ffmpeg/ffmpeg@ffmpeg/util 在点击/进入页面后再加载,避免打进首屏大包。
  3. serverExternalPackages:构建阶段不要把 ffmpeg 打进服务端 bundle:
// next.config.ts(节选)
serverExternalPackages: [
  "@ffmpeg/ffmpeg",
  "@ffmpeg/util",
  "@ffmpeg/core",
],
  1. 开发环境:官方 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/coreUMD 构建,让 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
}

加载顺序(失败自动换源):

  1. 环境变量 NEXT_PUBLIC_FFMPEG_CORE_BASE(可指向自建 OSS)
  2. jsDelivr UMD
  3. unpkg UMD
  4. 同源 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 / err
  • transcode(file, outputFileName, args)Blob
  • 单文件上限 200MBFFMPEG_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 性能有差异,极端场景桌面版更稳。


十四、总结

  1. 静态 Next 站 + ffmpeg.wasm = 零转码后端、隐私友好、适合工具站。
  2. UMD + importScripts 是 Next 场景下的稳定加载方式,ESM+toBlobURL 易踩 Worker 坑。
  3. public 同源托管 core + CDN 降级 + 单例,决定首屏体验与成功率。
  4. ffmpegTranscode 统一管线 + VideoFfmpegShell 复用,多个视频工具只需换参数预设。
  5. 压缩 = scale + libx264 + CRF,产品上要给用户 CRF/分辨率说明,并诚实告知耗时。

立即体验(免费 · 免登录 · 本地处理)

打开即用,无需安装客户端,无需注册账号。


原创声明:本文架构与代码思路基于开源项目 Tools Ku(https://www.toolsku.com)中的 ffmpeg-singletontranscodeVideoFfmpegShell 等实现,转载请注明出处。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐