开篇废话
这篇文章还是对 ugoira.huggy.moe 加功能后留下的笔记。
起因是因为这个破站 mp4 转 gif 功能在服务端太占资源了,CPU + 硬盘非常满,就有了尝试在浏览器里面跑 ffmpeg 糟蹋用户资源的想法。
WebAssembly(以下简称 Wasm) 技术到目前已经算很成熟了,在 Can I use 可以得知: Wasm 在 Chrome 57+ / Firefox 52+ / WebKit 11+ 被正式支持,经过几年的沉淀,用户浏览器升级得差不多了,问题也应该都修得差不多了(其中甚至有相关函数会被幽灵漏洞影响),特别是听到叔叔的站甚至软解 HEVC / AV1 来省流的新闻后,我觉得我也可以用了!
内容多倾向于编译部分,使用还是很简单的(毕竟都封装好了只要调用下)
记念下人生第一次写(改)Makefile
注意本文讲的是 ffmpeg.js 而不是 ffmpeg.wasm
找现成的库
首先是在 Github 找有没有现成的库,太菜了不会自己用 Emscripten 重新编译调用,一开始找的就是上文提到的 ffmpeg.wasm 实践了一波发现还是有很多问题,最终放弃:
- 二进制文件太大
- 提供的编译方法花了一星期没有编译成功过
- 使用了 worker 需要在用 SharedArrayBuffer 要强制开启跨域隔离策略 (这点非常要命,跨域被强制限定死了,不单是 Google Adsense 拒绝加载,而且需要所有引用的跨域资源都加头,感觉十分不划算)
此部分还是看个人取舍,如果你的玩具可以接受这些的话那也可以用 ffmpeg.wasm 项目。
于是我放弃了 ffmpeg.wasm 于是最后还是用了 ffmpeg.js 来安排。
使用
这部分教程参考 readme.md 可能更好点,很简单的调用,这里不作重点介绍。
这里不想用多线程,没有使用 worker 版本,需要多判断很多,好麻烦的样子。
传入的文件需要用 Uint8Array 编码,部分代码参考:
const mp4Filename = '1.mp4' // 这里文件名就没什么必要了,大概就这样
if (mp4_u8 instanceof Blob) {// 如果是 blob 对象
mp4_u8 = new Uint8Array(await mp4_u8.arrayBuffer())
}
let r0 = ffmpeg({
MEMFS: [{ name: 'mp4Filename', data: mp4_u8 }],
arguments: ['-y', '-i', mp4Filename, '233.gif'],
})
console.log(r0) // 会输出成品文件[]
不过官方提供的两个成品文件缺少对应的 codec 并不足以完成 mp4 转成 gif 的操作:
Unable to find a suitable output format

所以接下来要自己编译带 xxx codec 的 ffmpeg
编译自己的 ffmpeg.js
请注意版权,带 mp4 的 ffmpeg 为 GPL2 协议授权,带 webm 的为 LGPL2 协议授权,请在修改完后开源!
GPL2 为 X264 污染
建议先参考官方教程 以及 ffmpeg.js#build-instructions 提供的教程,这个部分更多的是我踩坑记录,也就是错误的时候要怎么修改参数,具体编译过程不会介绍得太详细。
编译原始成品
首先我们要能通过 ffmpeg.js 能编译出可用的成品才能再去修改其中的编译参数,最简易编译得出成品的办法就是按照 ffmpeg.js#build-instructions 提供的 docker 环境里面编译,当然我使用 podman 也一样能跑。
git clone https://github.com/Kagami/ffmpeg.js.git --recurse-submodules
docker run --rm -it -v /path/to/ffmpeg.js:/mnt -w /opt kagamihi/ffmpeg.js
# cp -a /mnt/{.git,build,Makefile} . && source /root/emsdk/emsdk_env.sh && make && cp ffmpeg*.js /mnt
然后就有成品了
建议拿线程撕裂者来编译 手上 Ryzen5 2400G 从零开始编译要4分钟左右。
编译参数
这里我们要简单知道下 ffmpeg 是怎么组合的:
ffmpeg 支持自由组合 编码器(encoder/decoder)、合成器(demuxer)、过滤器(filter) 从而减少二进制文件大小(依赖)
支持的列表可以在下面搜索到参数:
然后在 ffmpeg.js/Makefile 里面还是能较为流畅地修改编译参数的。
基本思路就是,先添加依赖到能用后,再精简到不能再精简为止。
不要想尝试一步到位,天下没有改一次就成功的 Makefile
不一定需要直接编译成 wasm 版的,可以手动换成 make 取代 emcc cli 先编译 && 测试通过后再编译成 Wasm 版本,这样测试的压力也许会小点。
ffmpeg.js 也自带测试 /test,不过要根据自己的情况改下参数才能更好用(使用 Node.js)
切换 ffmpeg 版本
在 ffmpeg.js 里面还是能切换 ffmpeg 版本的,我试了 4.x 5.x 都可以正常编译:
cd build/ffmpeg-mp4
git checkout n4.4
就是 git 换个分支(tag),不再过多介绍。
palettegen
我们首先看下要用到的 ffmpeg 命令:
ffmpeg -y -i '1.mp4' -vf "fps=24,scale=iw*min(1\\,min(${width}/iw\\,${height}/ih)):-2:flags=lanczos,palettegen" palette-1.png
可以看到 -vf 里面有许多参数,这里的 -vf 就是 video filter 的意思,就是要使用把过滤器参数传进去。
那么我们需要以下过滤器:
- fps
- scale
- lanczos
- palettegen
(基本上都能在 ffmpeg-filters 找到)
于是编译参数就是
COMMON_FILTERS = aresample scale crop overlay hstack vstack palettegen fps lanczos paletteuse
了,经过测试 lanczos 应该是自带的,这里不需要再加了加了也没用
实际测试还是会出错误,没有图片输出,接下文 png 部分
重新编译需要
make clean一下,清理掉编译缓存。
png
这次我们将 loglevel 调到 verbose 看看用 palettegen filter 生成 .png 用了些什么库:
ffmpeg -y -i '1.mp4' -vf "palettegen" palette-1.png -loglevel verbose

我们发现 .png 也是要编码器的,最后测试还需要参数 --enable-parser=png 才能正常生成图片,这部分没有找到相关资料介绍,所以也不知道 parser 部分代表什么。
当然 .jpg 也同理,自行参考(我就不测试了):

zlib
这部分是精简思路
在去掉 mp3lame 依赖后,zlib 库(libz.so)感觉变得没有地方引用了,需要手动产生 libz.so
如果要 mp4 那么 mp3 依赖也是必不可少的,如果尝试精简掉 mp3,那么会直接无法读取 mp4 文件,不过我们可以少编译 libmp3lame 来减少大小。
而 png 等需要 zlib 库,所以也不能放弃 zlib 的支持 当然是测试过后才知道这件事
于是我照着抄了个 Makefile-zlib 出来
zlib:
cd build/zlib && \
emconfigure ./configure \
--prefix="$$(pwd)/dist" \
&& \
emmake make -j && \
emmake make install
然后在编译的时候把这个库引入,参考 Makefile#124-125
如果后面 make 显示 dup symbol 那么还需要执行一遍 make clean 就可以(因为之前编译 libmp3lame 已经自带了 zlib 所以这部分被缓存了,需要清除编译时候的中间件才行)

OOM
在浏览器以坑爹 Safari 为主运行的时候可能会出现 OOM (Out Of Memory)情况,我们需要在 emcc 参数里面手动限制下内存占用。

我这里配了最大 256M 内存暂时解决(感觉也够用了,所以没有再去折腾)
动态载入
最后编译出了 gzip 后 1.5M 大小的文件,不过还是需要动态载入一下,仅在需要使用的时候加载此部分 JS 这里我用了最简单的办法来动态载入:
if (!pageCache.ffmpeg) {
// 这里最好再加个 toast 弹窗一下
pageCache.ffmpeg = (await import('@ugoira/ffmpeg.js/ffmpeg-mp4')).default
}
let r0 = pageCache.ffmpeg({})
这样弄了个最简单的变量来存放 ffmpeg.js ,当然也可以通过先进点的 Cache 或者 Service Workers 来安排,这里就偷懒用这个方法了。
放心,都支持 Wasm 了,基本也可以直接使用 await 了,思维要放开
浏览器兼容性
这里我建议还是留下 fallback 机制,如果检测到不兼容的浏览器(比如浏览器禁用了 Wasm)情况下要能提醒用户开启,以及留下服务端渲染的 API 来。
有些浏览器的增强隐私保护机制会直接禁用掉 Wasm,参考 clip-1#edge-浏览器有些页面无法正常加载
伪代码参考:
export let supportWasm = typeof WebAssembly === 'object' && ((
// When user = Android and AndroidVersion >= 9, use wasm
// Android version < 9 performance is poor
(navigator.userAgent.includes('Android') && getAndroidVersion() >= 9) ||
// When user = iOS and iOSVersion >= 12, use wasm
// iOS version < 12 performance is poor
((navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad') || navigator.userAgent.includes('iPod')) && getiOSVersion() >= 12)
// bypass Windows
) || !isMobile)
在这里,我首先判断 WebAssembly 是否存在,然后系统版本会不会太旧了(能上 Android 9+ / iOS 12+ 的性能应该都还 ok)最后判断是否有条件使用上本地 ffmpeg 如果不支持那么就继续使用原来的方案,由服务端来渲染 gif 然后再下载。
后记
在这里表示佩服能维护 && 提交给这种大项目的贡献者,我这样的新手就连编译都前前后后花了几星期时间才基本理清套路,如果不是对这些熟悉点的话连编译都不成功 >_<
虽然文章里面各种方法是轻描淡写走过的,不过想必还是有人连引入包都卡住了,在表示真的没法写这部分,大家的环境都各不相同,这里也只能粗略介绍一下了,希望你能成功地与 webpack (或者其它脚手架)斗智斗勇,成功引用依赖并且调用~
完
参考资料
堆 栈 溢 出:
- What encoders/decoders/muxers/demuxers/parsers do I need to enable in FFMpeg for converting an mp4 video to a gif?
- ffmpeg (avcodeclib) & png support
gist:
- ffmpeg_compile.sh (让我知道了还有
--enable-parser=png的存在) - update-ffmpeg-rpi.sh