背景
在写技术文档的时候,为了演示方便,经常需要插入一些短小的视频资源。比如一些操作的录频、一些经典视频片段、一些科普动画等。由于展示的地方通常是网页,而如果是插入视频之类的资源,通常需要占用额外的存储,而且也需要可用的视频播放器的支持,非常不便。对于命令行操作的回放,我们可能会采用 Asciinema,但是这个方案对非终端的操作无法模拟。一个常见的方案是把需要的资源转换成网页支持的 GIF、WEBP 格式等动图。考虑到各大浏览器对 GIF 格式的支持最稳定,因此大部分情况下我们都想将待展示的短小视频转换成 GIF 格式,方便直接插入文档中。
将视频转为 GIF 并对 GIF 做调整的过程中会有很多的坑,比如经常容易出现色差严重、分辨率不合适、GIF 占用的存储过大等问题。有时我们也希望在转换前做一些剪切等。处理图片的工具其实有很多,比如 ImageMagick 的 convert 工具,或者 gifsicle 工具,甚至是一些在线的 GIF 格式转换工具(极不推荐,很难用)。但是在处理类视频的信息时,这些工具其实并不很适合。毕竟原始数据本身是视频,相比先把视频转换成 GIF 再做操作,直接在视频上做完操作最后再一步转为 GIF 造成的信息丢失会更少,效果也会更好。本文主要基于 FFmpeg 总结一下转换过程中经常用到的命令,并介绍个人在处理这些问题的一些原则。
视频预处理
在典型的场景下,我们在将视频转为图片前一般会有如下步骤:
- 视频采集
- 视频剪裁
- 视频分辨率调整
- 视频帧率调整
- 视频速率调整
- 视频压缩
下面将基于此流程,结合样例做简要介绍。本地的 ffmpeg 版本为 4.4.1 。
视频采集
如果我们能获取到视频文件,则最好。不过很多情况下还是需要我们使用一些录屏工具进行采集,不过作为系统洁癖患者,个人不建议下载一些乱七八糟来源的录频工具或者很贵但是普通人根本用不到多少功能的视频处理软件。
- 在 Windows 下,可以通过 Win G 快捷键呼出 xbox 自带的录频工具。
- 在 Mac 下,可以通过 Command Shift 5 快捷键呼出。
以上工具足够用了。
视频裁剪
无论是自有视频,还是录屏来的视频,我们经常都希望剪裁掉头尾的一些无用片段。我们当然可以使用常见的编辑工具,例如 Mac 上的 imovie 等。不过显然我们不想杀鸡用牛刀,一两条命令就可以解决。
截取从 00:00:10 开始的 10 秒的时间段的视频:
代码语言:javascript复制$ ffmpeg -i sample.mov -ss 00:00:10 -t 00:00:10 output1.mov
(其中 -ss 表示开始时间,-t 表示截取的时长)
截取从 00:00:10 到 00:00:20 时间段的视频:
代码语言:javascript复制$ ffmpeg -i sample.mov -ss 00:00:10 -to 00:00:20 output2.mov
(其中 -ss 表示开始时间,-to 表示结束时间)
确认二者相同并确认视频时长:
代码语言:javascript复制$ diff output1.mov output2.mov
$ ffprobe -v error -show_entries format=duration sample.mov
[FORMAT]
duration=10.000000
[/FORMAT]
(其中 -v error 是为了屏蔽一开始打印的版本信息)
视频分辨率调整
由于我们需要展示的视频本身是嵌入文档或网页里的,因此本身对图片分辨率的要求并不大。这时候适当的缩小分辨率无论是对文件占用的大小、还是对展示的便捷都是有好处的。采用的工具是 ffmpeg 的 scale filter graph。
确认原视频的分辨率:
代码语言:javascript复制$ ffprobe -v error -show_entries stream=width,height sample.mov
[STREAM]
width=1560
height=1148
[/STREAM]
强制比例缩放,设置长度,宽度:
代码语言:javascript复制$ ffmpeg -i sample.mov -vf scale=720:530 output1.mov
(其中 720 为宽,530为高,且,宽高均不可为奇数)
固定比例缩放,设置宽度,高度自适应:
代码语言:javascript复制$ ffmpeg -i sample.mov -vf scale=720:-1 output2.mov
固定比例缩放,设置高度,宽度自适应:
代码语言:javascript复制$ ffmpeg -i sample.mov -vf scale=-1:530 output3.mov
(将需要自适应的部分设置为-1即可,如果自适应部分再按倍缩放,则可以设置为 -2 ,-3 等)
选择自定义scale算法,可选算法可见ScalerOptions:
代码语言:javascript复制$ ffmpeg -i sample.mov -vf scale=-1:530:flags=lanczos output4.mov
(缩放算法有很多种,如果效果不好可以换几个试试。默认是 bicubic 算法)
视频帧率调整
视频的帧率一般会比较高,而我们对 GIF 的要求一般没那么高。为了减少图片的体积,我们可以手动调节下帧率,以达到图片大小和用户体验的最佳平衡点。通常视频的帧率一般是 60 fps 。对普通图片来说,20 的fps早已够用,节约点的话,10 fps ,5 fps 也凑合能看,具体就自己体验喽。
确认原视频帧率:
代码语言:javascript复制$ ffprobe -v error -show_entries stream=r_frame_rate sample.mov
[STREAM]
r_frame_rate=60/1
[/STREAM]
(原视频的帧率就是 60 fps)
调整帧率为20:
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 output1.mov
视频速率调整
对于录屏而言,可能我们的动作比较慢,希望在展示的时候稍微加快点速度以提高展示效率并减少视频体积。或者视频本身很快,我们希望做一下慢放。这些时候我们最好提前就对视频速度做一些调整。
调整视频速度变快为5倍(时长*0.2):
代码语言:javascript复制$ ffmpeg -i sample.mov -filter:v "setpts=0.2*PTS" output1.mov
调整视频速度变慢2倍(时长x2):
代码语言:javascript复制$ ffmpeg -i sample.mov -filter:v "setpts=2*PTS" output2.mov
(原理通过调整视频帧中的 PTS 展示时间戳来调整速度)
需要注意的是,调整速率后,帧率仍然保持不变。因此将视频加速再减速成原视频的速度后,与原视频相比会丢失信息。
视频压缩
其实H264视频本身的压缩率已经很高了,如果想要进一步压缩,基本只能通过一些有损的形式。我们可以通过调整 x264, x265, 以及 libvpx 中的 Constant Rate Factor 参数来进行一些有损压缩处理。该参数取值在 0 到 51 之间,值越大则有损的比例越大,压缩率越好。通常我们会取 23这个值,稍微激进一点可以调整为30 。
调整 crf 取值到30 :
代码语言:javascript复制$ ffmpeg -i sample.mov -crf 30 output1.mov
比较二者大小:
代码语言:javascript复制$ ls -lah sample.mov output1.mov
-rw-r--r-- 1 myths staff 1.5M May 1 17:31 sample.mov
-rw-r--r--@ 1 myths staff 1.0M May 1 17:51 output1.mov
可见视频的确压缩了一些,不过会发现 GIF 的质量会有一些下降。是否需要调整这个参数,需要根据实际情况进行取舍。
GIF 格式转换
ffmpeg默认支持根据输出文件的后缀名自动进行格式转换,非常方便。但是如果你以为能无脑用,那就大错特错了。
帧率问题
一个典型的错误转换方法是:
代码语言:javascript复制$ ffmpeg -i sample.mov output.gif
有什么问题呢?我们检查一下视频长度就知道了:
代码语言:javascript复制$ ffprobe -v error -show_entries format=duration sample.mov
[FORMAT]
duration=62.000000
[/FORMAT]
$ ffprobe -v error -show_entries format=duration output.gif
[FORMAT]
duration=173.680000
[/FORMAT]
我们发现转换后的 GIF 的视频长度竟然和原视频不一样。打开检测后发现的确 GIF 相比原视频要慢了许多。
再检查一下帧率就发现问题了:
代码语言:javascript复制$ ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate sample.mov
[STREAM]
r_frame_rate=60/1
avg_frame_rate=60/1
[/STREAM]
$ ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate output.gif
[STREAM]
r_frame_rate=50/1
avg_frame_rate=257/12
[/STREAM]
GIF 的实际帧率竟然变小了,难怪视频变长了。计算下比例也能对的上:
173.680000/62.000000≈(60/1)/(257/12)
既然如此,我们强制设置下帧率就好了:
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 output2.gif
$ ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate,duration output2.gif
[STREAM]
r_frame_rate=20/1
avg_frame_rate=20/1
duration=62.050000
[/STREAM]
这样时长就和原视频对上了,帧率也是我们设置的帧率。具体原因未知,不过结论就是在对视频转 GIF 时,一定要重新指定一下帧率。
调色板优化
你可能知道,和视频不同,PNG的调色盘只有256种颜色。默认情况下,这256种颜色会尽量平均分布在整个颜色空间中。这就导致对于一些色彩区分度比较小的图片,会出现颜色模糊的情况。
首先我们生成一张下未优化下的图片:
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 output-raw.gif
结果如下:
这时候需要用对图片进行一下全局调色板优化:
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" output-palette.gif
结果如下:
放大后仔细观察一下二者,你会很明显的发现原图的背景颜色变成了很多小颗粒,而使用调色版优化后,背景才真正显示了纯色。
当然,如果对图片质量要求高,也可以对每一帧单独记录调色板(代价就是图片会变大很多):
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen=stats_mode=single [p];[s1][p]paletteuse=new=1" output-palette-single.gif
如果图片的运动程度比较大,也可以修改一些防抖参数( dither = none / bayer / heckbert / floyd_steinberg / sierra2 / sierra2_4a),如果不指定,默认是 sierra2_4a 。例如:
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=none" output-palette-none.gif
$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=bayer" output-palette-bayer.gif
$ ffmpeg -i sample.mov -r 20 -vf "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=heckbert" output-palette-heckbert.gif
最后看一下添加了调色板优化后的图片大小:
代码语言:javascript复制$ ls -lah
-rw-r--r-- 1 myths staff 2.2M May 2 11:36 output-palette-bayer.gif
-rw-r--r-- 1 myths staff 2.5M May 2 11:36 output-palette-heckbert.gif
-rw-r--r-- 1 myths staff 1.9M May 2 11:40 output-palette-none.gif
-rw-r--r-- 1 myths staff 32M May 2 11:29 output-palette-single.gif
-rw-r--r--@ 1 myths staff 2.7M May 2 11:15 output-palette.gif
-rw-r--r-- 1 myths staff 1.0M May 2 11:30 output-raw.gif
-rw-r--r--@ 1 myths staff 198K May 1 22:15 sample.mov
显然,每帧记录调色板( output-palette-single.gif ) 最大;不进行调色板优化图片最小,但是质量最差( output-raw.gif );而不使用防抖策略( output-palette-none.gif ) 会使图片更小一点,也不太影响图片观感。
GIF 循环次数设置
通过 ffmpeg 还可以设置图片的循环次数。图片在播放完成后,默认会重头开始播放,如果想修改这个特性,可以通过 -loop 参数指定循环方式,也可以通过 -final_delay 参数配置间隔时间:
设置 GIF 播放完后不重头开始:
代码语言:javascript复制$ ffmpeg -i sample.mov -r 20 -loop -1 output.gif
gif muxer 支持的 -loop 和 -final_delay 参数的说明可以通过命令查看:
代码语言:javascript复制$ ffmpeg -v error -h muxer=gif
Muxer gif [CompuServe Graphics Interchange Format (GIF)]:
Common extensions: gif.
Mime type: image/gif.
Default video codec: gif.
GIF muxer AVOptions:
-loop <int> E.......... Number of times to loop the output: -1 - no loop, 0 - infinite loop (from -1 to 65535) (default 0)
-final_delay <int> E.......... Force delay (in centiseconds) after the last frame (from -1 to 65535) (default -1)
有点令人费解的是,除了无限重复(0)和不重复(-1)的值,如果你想重复 N 次,那么这个 -loop 参数就要设置为 N 1 。。。
图片大小分析
最后记录一下我的一个测试资源在顺序进行以上各种处理后的大小变化情况:
- 录频后的原视频:9.3M (mov格式)
- 不加任何参数转码后视频:1.5M(mov格式)
- 按需裁减后:1.2M (mov格式)
- 分辨率由 1560x1148 调整为 720x539 后:541K(mov格式)
- 帧率从 60 调整为 20 后:339K (mov格式)
- 速率 x2 后:235K (mov格式)
- 视频压缩 CRF 取值 30 后:198K (mov格式)
- 转换为 GIF ,使用全局调色板并取消防抖设置后:1.9M (gif格式)
GIF 格式的压缩效果和普通视频格式相比还是差很多的,不过在尽量保证图片质量的前提下,把图片大小控制在 2M 以内一般也还能够接受。
参考资料
压缩 gif 图片的方法(命令行)
Speeding up/slowing down video
CRF Guide
How do I convert a video to GIF using ffmpeg, with reasonable quality?
High quality GIF with FFmpeg
ffmpegでとにかく綺麗なGIFを作りたい