简介:这份资源围绕 ffmpeg.js 展开,面向希望在前端直接完成音视频处理的 Web 开发者与 JavaScript 学习者,解决传统转码必须依赖后端服务、部署成本高的问题。借助其封装好的 API,只需几行代码即可在浏览器中完成视频转码、格式转换等操作,适合在线剪辑、本地预览、教学演示等场景。压缩包共 122 个文件,约 3.44MB,以 61 个 png 截图、27 个 js 脚本、8 个 md 说明文档为主,另含 html 示例页、yml 配置、json 与 ts 文件,以及 avi、wav、ogg 等测试音视频素材,覆盖源码、文档与演示资源。内容预览中可见转码演示、摄像头采集、图片转视频、concat 解复用等示例页面,便于读者对照理解不同输入源的处理方式。目前已有 6396 人学习下载,适合想快速上手浏览器端 FFmpeg、研究前端音视频处理方案的开发者参考。
1. 浏览器里跑 FFmpeg:为什么我最后选了 ffmpeg.js 而不是服务端转码
去年做一个在线音频剪辑的小工具,需求很朴素:用户拖进来一个 m4a,裁掉头尾、转成 mp3、再压一下码率,全程不想让文件离开浏览器。第一版我老老实实写了后端接口,结果上线第二天就翻车——用户传的是几十兆的录音,上传排队、磁盘爆、并发一上来 CPU 直接打满,运维半夜给我打电话。后来我把整条链路挪到前端,用 ffmpeg.js 在浏览器里直接跑 FFmpeg,文件一个字节都没出过本机,服务器只发静态资源,成本瞬间归零。
ffmpeg.js 本质是把 FFmpeg 编译成 WebAssembly,再配一层 JavaScript 胶水,让你在浏览器里调用ffmpeg命令。它解决的就是「不想为一次转码养一台服务器」这件事,适合做在线音视频处理、格式转换、截图抽帧、批量压缩这类工具站,也适合 Electron 或纯前端项目里需要离线处理媒体的场景。代价是首次要加载十几到二十几兆的 wasm,且只能跑单线程或有限多线程,重活依旧吃力。这篇就按我实际踩过的路,把选型、加载、调用、参数和坑一次讲清。
2. ffmpeg.js 的加载与初始化:wasm 从哪来、内存怎么给
2.1 先搞清楚它和原生 FFmpeg 的差别
原生 FFmpeg 是本地进程,能开多线程、能调 GPU、能读写任意路径。ffmpeg.js 跑在浏览器的沙箱里,没有文件系统,没有进程,所有输入输出都得走内存。它的工作模型是:你把一个Uint8Array塞进去,它把结果以Uint8Array吐出来。中间那些-i input.mp4 -c:v libx264 output.mp4的命令行参数基本能照抄,但路径是虚拟的,通常约定输入叫input、输出叫output。
选型上要分清两个东西:一个是@ffmpeg/ffmpeg(新版,基于 ESM,API 是ffmpeg.load()/ffmpeg.exec()),另一个是老的ffmpeg.js(单文件,暴露Module全局对象)。热词里常搜的「ffmpeg.js」多半指后者,但新项目我建议用@ffmpeg/ffmpeg,因为老版本对多线程和内存增长的处理比较糙,长时间跑容易崩。下面示例用新版 API,逻辑对老版同样成立。
2.2 加载 wasm 与创建实例
import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile, toBlobURL } from '@ffmpeg/util'; const ffmpeg = new FFmpeg(); // 加载核心 wasm,coreURL 指向 ffmpeg-core.js,wasmURL 指向 ffmpeg-core.wasm async function loadFFmpeg() { const baseURL = '/ffmpeg'; // 静态资源目录,需自己托管 await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), }); console.log('ffmpeg ready'); }toBlobURL的作用是把跨域资源转成 blob URL,绕开部分浏览器对 wasm 的 MIME 校验。coreURL是胶水 JS,wasmURL是真正的二进制,两个文件必须版本一致,混用会直接报RuntimeError: abort。如果你要开多线程,还得额外提供workerURL,并且服务器要带Cross-Origin-Opener-Policy: same-origin和Cross-Origin-Embedder-Policy: require-corp两个响应头,否则SharedArrayBuffer不可用,多线程直接退化甚至报错。
2.3 内存与线程参数怎么给
wasm 的内存是预分配的,默认上限通常够跑几十兆的媒体,但处理大文件时会在exec中途抛OOM。常见做法是在加载时通过Module配置调大INITIAL_MEMORY,或者干脆在业务层限制单文件大小。多线程版要设-threads N,但浏览器里 N 一般给 2 到 4 就够,给多了反而因为调度开销变慢。判断是否真的用上了多线程,看ffmpeg.on('log')里有没有pthread相关输出,没有就是没生效,多半是响应头没配。
提示:wasm 文件建议放 CDN 并开 gzip/brotli,二十几兆的 wasm 压完能到七八兆,首屏体验差别很大。
3. 用 ffmpeg.js 做转码与抽帧:命令怎么写、结果怎么取
3.1 把文件喂进去、把结果拿出来
async function transcode(file) { // 写入虚拟文件系统,名字随意,后面命令里对应上即可 await ffmpeg.writeFile('input.m4a', await fetchFile(file)); // 监听日志,排查参数错误时非常关键 ffmpeg.on('log', ({ message }) => console.log('[ffmpeg]', message)); // 转 mp3,128k 码率,采样率 44100 await ffmpeg.exec([ '-i', 'input.m4a', '-vn', // 丢弃视频流,纯音频场景必加 '-ar', '44100', // 采样率 '-b:a', '128k', // 音频码率 'output.mp3', ]); const data = await ffmpeg.readFile('output.mp3'); return new Blob([data.buffer], { type: 'audio/mpeg' }); }writeFile把浏览器File转成 wasm 能读的字节流,exec是同步阻塞式的(返回 Promise,但内部串行),readFile拿回结果。注意readFile返回的是Uint8Array,构造 Blob 时要用data.buffer,直接传data在某些浏览器会得到空文件,这是我早期最常翻的车。-vn在纯音频处理里一定要加,否则遇到带封面的 m4a 会尝试解视频流,白白耗时甚至报错。
3.2 抽帧和截图
async function grabFrame(file, time = '00:00:01') { await ffmpeg.writeFile('video.mp4', await fetchFile(file)); await ffmpeg.exec([ '-ss', time, // 定位到指定时间点,放在 -i 前更快 '-i', 'video.mp4', '-frames:v', '1', // 只取一帧 '-q:v', '2', // 输出质量,2 已经很高 'frame.jpg', ]); const img = await ffmpeg.readFile('frame.jpg'); return new Blob([img.buffer], { type: 'image/jpeg' }); }-ss放在-i之前是快速定位,放在之后是精确解码,抽帧场景用前者足够快。-frames:v 1保证只输出一张,不加的话会按帧率吐一堆图,内存直接爆。-q:v是 JPEG 质量,范围 2 到 31,数字越小越清晰、文件越大,做缩略图给 5 左右就够。
3.3 参数对照与常见组合
| 场景 | 关键参数 | 说明 |
|---|---|---|
| 音频转码 | -vn -ar -b:a | 丢视频流,控采样率和码率 |
| 视频压缩 | -c:v libx264 -crf 28 -preset veryfast | crf 越大越糊,preset 越快越糊 |
| 抽帧 | -ss -frames:v 1 -q:v | 定位、单帧、质量 |
| 裁剪时长 | -ss -t | 起点加持续时长 |
| 提取音频 | -vn -acodec copy | 不重编码,秒出 |
-crf是 x264 的恒定质量参数,18 到 28 是常用区间,28 以上肉眼可见糊。-preset从ultrafast到veryslow,浏览器里我一般只敢用veryfast或ultrafast,再慢用户就以为页面卡死了。-acodec copy不重编码,速度极快,但要求容器支持,m4a 提 aac 没问题,mp4 提 mp3 就会失败。
4. 避坑与排查:那些让我加班到凌晨的 ffmpeg.js 问题
4.1 报错SharedArrayBuffer is not defined
现象是加载多线程版核心时直接抛异常,页面白屏。原因是浏览器出于安全策略默认禁用SharedArrayBuffer,需要跨域隔离响应头。解决是在服务器给 wasm 和页面都加上Cross-Origin-Opener-Policy: same-origin与Cross-Origin-Embedder-Policy: require-corp,或者干脆用单线程版核心,牺牲速度换稳定。
4.2exec跑完但readFile拿到空文件
现象是命令日志显示成功,读出来却是 0 字节。原因通常是输出文件名和命令里写的不一致,或者构造 Blob 时传了Uint8Array而不是它的buffer。解决是核对writeFile/readFile的文件名,并统一用new Blob([data.buffer])。另外exec是串行的,前一个没 await 完就发下一个,虚拟文件系统会互相覆盖。
4.3 大文件跑到一半 OOM
现象是处理几十兆以上文件时中途崩溃,日志停在某个解码步骤。原因是 wasm 内存有上限,且readFile会把整个结果读进内存。解决是限制单文件大小、分片处理,或者改用流式方案(新版支持ffmpeg.exec配合FS分块读写)。我一般在前端就拦掉超过 100MB 的文件,提示用户先本地压缩。
4.4 中文文件名或路径导致失败
现象是带中文名的文件写入后命令找不到。原因是虚拟文件系统对非 ASCII 路径支持不稳。解决是写入前统一重命名为input、output这类纯英文名,处理完再在 Blob 层还原原始文件名,用户无感知。
4.5 首次加载慢、用户以为卡死
现象是首屏点按钮后十几秒没反应。原因是 wasm 体积大,且首次要实例化。解决是提前在页面空闲时预加载核心,加一个进度提示,并把 wasm 放 CDN 开压缩。别等用户点了才开始下载,那是体验杀手。
5. 进阶:把 ffmpeg.js 用稳的几个习惯
真正让 ffmpeg.js 在生产里站住脚的,不是会写几条命令,而是把「加载、执行、回收」当成一条流水线来管。我现在的做法是:页面初始化时就load()一次,实例常驻,所有转码任务走一个队列串行执行,避免并发把内存打爆。每个任务开始前writeFile,结束后主动deleteFile清掉虚拟文件系统里的临时文件,否则跑几十次之后内存只增不减,最后必然 OOM。
验证是否真的处理成功,别只看exec有没有抛错,要检查输出字节数是否大于 0,必要时用ffprobe(部分核心带)读一下时长和编码格式。我习惯在log回调里过滤error关键字,一旦出现就中断任务并提示用户换参数,而不是让它跑完再发现文件是坏的。
还有一个容易被忽略的点:-threads在单线程核心里是无效参数,写了也不报错,但会让你误以为开了多线程。判断方法就是看日志里有没有pthread。另外-preset和-crf的组合对耗时影响极大,做在线工具时我一般给用户两档:快速(ultrafast+crf 30)和标准(veryfast+crf 24),别把veryslow暴露出去,那是给离线批处理用的。
从那以后我每次接媒体处理需求,都强制先问一句「文件能不能不出浏览器」,能就上 ffmpeg.js,不能才考虑服务端。这套判断帮我省了不止一台转码服务器。希望帮到你。
本文还有配套的精品资源,点击获取