本文介绍如何使用 FFmpeg.wasm 在浏览器端实现视频压缩和格式转换,全程不上传服务器,保护用户隐私。含完整代码示例和踩坑记录。
为什么需要浏览器端视频处理?
传统视频处理方案是:用户上传视频到服务器 → 服务器用 FFmpeg 处理 → 用户下载处理后的文件。这个方案有几个问题:
- 带宽成本高:视频文件通常几十 MB 甚至几百 MB,上传下载都吃带宽
- 服务器算力开销大:视频编码是 CPU 密集型任务,并发处理几个请求就能把服务器打满
- 隐私风险:用户的视频文件要上传到服务器,很多人不愿意
FFmpeg.wasm 把 FFmpeg 编译成 WebAssembly,直接在浏览器里跑。视频文件不离开用户设备,服务器只负责提供静态资源文件(wasm 引擎),零算力开销。
FFmpeg.wasm 简介
FFmpeg.wasm 是 FFmpeg 的 WebAssembly 移植版本。核心组件:
ffmpeg-core.wasm:FFmpeg 核心逻辑,约 30MBffmpeg-core.js:wasm 的 JS 胶水代码ffmpeg-core.worker.js:Web Worker 入口,FFmpeg 在 Worker 线程中运行,不阻塞 UI
版本选择
目前主流是 v0.12.x,API 和 v0.11.x 完全不同。本文使用 v0.12.x。
bash
# npm 安装
npm install @ffmpeg/ffmpeg @ffmpeg/util
如果你不用构建工具,也可以直接下载静态文件放到服务器上私有化部署(推荐,后面会讲为什么)。
核心实现
1. 加载 FFmpeg 引擎
javascript
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
const ffmpeg = new FFmpeg();
// 监听日志(可选,用于调试)
ffmpeg.on('log', ({ message }) => {
console.log(message);
});
// 监听进度(用于显示进度条)
ffmpeg.on('progress', ({ progress, time }) => {
const percent = Math.round(progress * 100);
console.log(`转换进度: ${percent}%`);
});
async function loadFFmpeg() {
const baseURL = 'https://your-domain.com/lib/ffmpeg/0.12.10';
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'),
});
}
关键点 :toBlobURL 把远程文件转成 Blob URL,绕过跨域限制。FFmpeg 的 Worker 需要同源访问,直接用 CDN 地址会报 Failed to construct 'Worker' 错误。
2. 视频压缩
javascript
async function compressVideo(file, options) {
// options: { crf: 28, scale: 1, audioBitrate: '128k', format: 'mp4' }
// 写入输入文件
await ffmpeg.writeFile('input.mp4', await fetchFile(file));
// 构建 FFmpeg 命令
const args = ['-i', 'input.mp4'];
// 视频编码(H.264)
args.push('-c:v', 'libx264');
args.push('-crf', String(options.crf)); // 18-28,越大压缩率越高
// 分辨率缩放
if (options.scale < 1) {
args.push('-vf', `scale=iw*${options.scale}:ih*${options.scale}`);
}
// 预设(编码速度 vs 压缩率)
args.push('-preset', 'medium');
// 音频编码
args.push('-c:a', 'aac');
args.push('-b:a', options.audioBitrate);
// 输出
args.push('output.mp4');
await ffmpeg.exec(args);
// 读取输出文件
const data = await ffmpeg.readFile('output.mp4');
return new Blob([data.buffer], { type: 'video/mp4' });
}
CRF 参数说明:
- CRF 18-23:高质量,压缩率低
- CRF 24-28:中等质量,压缩率高(推荐)
- CRF 29-35:低质量,压缩率很高
3. 格式转换
javascript
async function convertVideo(file, targetFormat) {
const inputName = 'input' + getFileExtension(file.name);
const outputName = 'output.' + targetFormat;
await ffmpeg.writeFile(inputName, await fetchFile(file));
const args = ['-i', inputName];
// 根据目标格式设置编码器
switch (targetFormat) {
case 'mp4':
args.push('-c:v', 'libx264', '-c:a', 'aac');
break;
case 'webm':
// ⚠️ WebM 用 libvorbis 而不是 libvpx 做音频编码
args.push('-c:v', 'libvpx', '-c:a', 'libvorbis');
break;
case 'avi':
args.push('-c:v', 'mpeg4', '-c:a', 'mp3');
break;
case 'mov':
args.push('-c:v', 'libx264', '-c:a', 'aac');
break;
case 'mkv':
args.push('-c:v', 'libx264', '-c:a', 'aac');
break;
case 'gif':
// GIF 特殊处理:去掉音频,降低帧率
args.push('-vf', 'fps=10,scale=480:-1:flags=lanczos');
args.push('-an'); // 无音频
break;
}
args.push(outputName);
await ffmpeg.exec(args);
const data = await ffmpeg.readFile(outputName);
const mimeType = getMimeType(targetFormat);
return new Blob([data.buffer], { type: mimeType });
}
踩坑记录
坑 1:Worker 跨域错误
现象 :Failed to construct 'Worker': Script at 'https://cdn.xxx/ffmpeg-core.worker.js' cannot be accessed from origin 'https://your-domain.com'
原因:浏览器同源策略限制,Worker 脚本必须同源。
解决 :用 toBlobURL 把远程脚本转成 Blob URL。或者把 ffmpeg 文件下载到自己服务器上私有化部署。
推荐方案 :私有化部署。把 ffmpeg-core.js、ffmpeg-core.wasm、ffmpeg-core.worker.js 放到你的 wwwroot/lib/ffmpeg/ 目录下,完全同源,没有跨域问题。
坑 2:WebM 音频编码器
现象 :转 WebM 时报错 Unknown encoder 'libvpx'
原因 :libvpx 是视频编码器,不是音频编码器。WebM 的音频编码器是 libvorbis。
解决 :WebM 用 -c:v libvpx -c:a libvorbis,不是 -c:a libvpx。
坑 3:加载无进度反馈
现象:FFmpeg 核心文件约 30MB,首次加载时用户看不到任何反馈,以为页面卡死。
解决 :用 XMLHttpRequest 预下载 wasm 文件,监听 onprogress 事件显示下载进度:
javascript
async function fetchWithProgress(url, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
onProgress(percent);
}
};
xhr.onload = () => {
if (xhr.status === 200) {
resolve(new Blob([xhr.response]));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
}
坑 4:SharedArrayBuffer 需要特定 HTTP 头
FFmpeg.wasm 的多线程版本需要 SharedArrayBuffer,要求服务器返回以下 HTTP 头:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
如果你用的是单线程版本(默认),不需要这些头。单线程版本性能稍差,但兼容性更好。
性能参考
在普通笔记本(i5-8265U)上的实测数据:
| 操作 | 输入大小 | 耗时 | 输出大小 | 压缩率 |
|---|---|---|---|---|
| MP4 压缩 CRF=28 | 45MB | ~35s | 12MB | 73% |
| MP4 → WebM | 45MB | ~50s | 18MB | 60% |
| MP4 → GIF | 45MB | ~20s | 8MB | 82% |
| MOV → MP4 | 120MB | ~90s | 35MB | 71% |
浏览器端处理比服务器慢 2-3 倍,但对用户来说是"零等待上传"------处理完直接下载,整体体验反而更快。
在线体验
如果你想直接体验浏览器端视频处理,可以试试这些在线工具:
- 在线视频压缩 :https://www.aixwriter.cn/tools/video-compressor
- 视频格式转换 :https://www.aixwriter.cn/tools/video-converter
- 视频工具集 (提取音频/剪辑/转GIF/加水印/字幕/拼接):https://www.aixwriter.cn/tools/video-tools
全部纯浏览器端处理,不上传服务器。
总结
FFmpeg.wasm 让浏览器端视频处理成为可能。核心优势:
- 零服务器算力:所有处理在客户端完成
- 隐私保护:视频文件不离开用户设备
- 部署简单:只需要静态文件托管,不需要后端
主要限制是性能------浏览器端比服务器慢 2-3 倍,但对日常使用足够了。如果追求极致性能,可以考虑 WebGPU 加速的方案,但目前生态还不成熟。