简介
FFmpeg是目前使用最广泛的开源音视频处理库,大多数的多媒体应用都会或多或少、直接或间接使用到FFmpeg。
FFmpeg是一个C写的项目,通常是被按需编译成.so,然后在代码中引用。
除了编程使用,FFmpeg项目也提供了三个特别好用的命令行工具:ffmpeg
、ffprobe
和ffplay
。这次主要是介绍其中的ffmpeg
命令。
(下文小写的ffmpeg
指的是命令行工具,开头大写的FFmpeg
指的是FFmpeg项目)
基本概念
在介绍之前,先简单介绍下一些音视频/FFmpeg的基本概念。
-
像素、图像、视频、音频。
这些都是大家所熟知的概念。
广义上来讲,所有的光学信号都可以称为图像。计算机世界的图像是离散的,因而一张图像由许多像素组成,每个像素有自己的颜色。
在时间维度上将一系列图像串联起来就形成了视频,所以原始的视频数据往往存储为连续的、一块一块的像素数据。
音频则是在时间维度上将一系列震动串联起来,所以原始的音频数据往往存储为PCM数据,也就是每一时刻的震动幅度。
-
编码(encode)和解码(decode)。
这些原始的视频和音频数据量是很大的,可以使用一些算法来利用其中的一些重复信息,从而减小数据量,这个压缩过程就是编码;反过来还原这些重复信息,得到原始的视频和音频,用于播放和处理,这个过程功能就是解码。
一些编解码算法计算量很大,很是耗费CPU,因而SOC或者显卡会使用专用的硬件单元实现一些常见算法,使用这些硬件能极大加速编解码过程,适合音视频播放等场景。
但硬件实现的可配置性较低,CPU编解码的好处在于可以对编解码的过程进行更为精细的控制。
-
封装(mux)和解封装(demux)。
有了多个媒体资源,将他们组合起来的过程通常叫做“封装”(mux);反过来的过程叫做“解封装”(demux)。我们平时接触到的各种音视频文件,往往都是多个资源“混合”之后的产物,比如一个电影文件可能包含:视频、多声道和多语言的音频、字幕,以及一些额外数据,如封面图、标题、年份等等。
“封装”的方法往往和文件格式对应,常见的比如mov文件、mp4文件、mp3文件、ogg文件等,它们都可以理解为用来存储媒体数据的容器。
注意和编解码区分开,编解码指的是对一段媒体数据的压缩、解压过程,“封装”指的是对多个媒体数据打包的过程,往往是不带压缩能力的。就比如一个mp4文件,它的视频数据有可能是h264编码的,也有可能是h265/hevc编码的;虽然编码和封装有一些常见的对应关系,但它们并非一回事。
-
过滤器(filter)。
这是FFmpeg的一个概念。
对音视频的处理往往是从文件中读取作为输入,处理之后然后输出。这个数据处理过程经常有一些惯用法,比如缩放、位移、拼接、剪切,或是调整音频音量等等,FFmpeg把这些惯用的操作做成一个个的filter,每个filter可以实现一个单一的功能,通过拼接这些filter就可以实现比较复杂的功能。
安装
FFmpeg提供了三个工具,分别是:
ffmpeg
最主要的工具,用于音视频的处理;ffplay
用于播放音视频文件;ffprobe
用于探测音视频的格式和编码等信息。
在Mac上,可以通过brew安装:brew install ffmpeg
。完成后即可使用上述三个命令。
其中ffplay
和ffprobe
比较简单,后面直接加文件名就行。例如命令行下输入ffplay 1.mp4
就可以播放一个视频,ffprobe 1.mp4
就可以查看文件的信息。
这里主要讲一下ffmpeg
命令的使用方式。
参数结构
ffmpeg的工作方式大致上是将输入的媒体文件按照指定的方式处理,然后输出新的媒体文件(这里的文件也包含网络流等)。
相应地,它的参数也主要由三部分构成:输入参数、输出参数和全局参数。
全局参数
其中,全局参数控制ffmpeg
的一些全局行为,比如日志级别、版本信息、内存限制等等。
输入参数
输入参数用来指定输入文件,以及告知ffmpeg输入文件的一些信息。FFmpeg支持几乎所有媒体格式,一般情况下,ffmpeg会通过文件的扩展名(.mp4、.mp3等)来确定文件的封装格式,进而读取内容以获取编码方式、分辨率、通道数、像素格式、采样率等信息,再基于这些信息进行解码。
比如上图的命令,指定-i input.mp4
之后,ffmpeg会把input.mp4作为输入,同时根据它的后缀.mp4得知他是一个mp4封装的文件。
ffmpeg也支持多个输入,例如我们现在有一个视频input.mp4
和一个音频input.mp3
,要将它俩合到一起,就可以同时指定两个文件作为输入ffmpeg -i input.mp4 -i input.mp3
,这样在后续的输出参数中,就可以操作和合成这两个输入。
对每一个输入文件,都有一些参数可以控制输入行为。
例如,ffmpeg -t 3s -i input.mp4 -t 2s -i input.mp3
,其中-i input.mp4
前面的参数用来控制input.mp4
这个输入文件,即-t 3s
的作用是让ffmpeg只读取该文件的前3秒;同理,-i input.mp3
前面的-t 2s
会使得ffmpeg只读取该音频文件的前两秒。
输入序列帧
如果输入并非普通文件,扩展名不正确或不能直接使用,或者是文件中不包含这些信息(如raw格式、序列帧等),就需要我们使用一些额外的输入参数,自行指定编码、宽高、像素格式、帧率等信息。
比如对于一串序列帧,名称分别为0001.png、0002.png、...0100.png,如果要将这个序列帧作为输入,可以使用通配的方法:ffmpeg -r 20 -i %04d.png
,这样的话这个序列帧就会被当作一个视频输入;序列帧自身不包含帧率信息,这行命令中的-r 20
参数就手动指定了该序列帧的帧率。
输出参数
输入参数告诉了ffmpeg如何读取和解码媒体文件,解码之后就得到了原始的音视频流;接下来需要我们告诉ffmpeg如何处理这些音视频流,以及如何输出结果,这些由输出参数指定。
输入文件需要指定-i
开关,输出文件则不用,直接在命令最后加上需要输出的文件就行,例如:ffmpeg -i input.mp4 output.mov
就可以将mp4文件转为mov文件。
ffmpeg解析输出参数的逻辑是:在所有-i xxx
结束之后,每个不带开关的参数(通常被称做Positional参数)都是输出文件;每个输入文件之前的所有开关和参数都是属于这个输入文件的。
例如,ffmpeg -i input.mp4 -i input2.mp4
-r 20 output.mov
-f mp4 output.mp4
,其中-r 20
就是output.mov的参数,用于指定该输出文件帧率为20;而-f mp4
就属于output.mp4
,用于指定该输出文件的封装格式为mp4。
一些常见的参数
了解了ffmpeg的参数结构,接下来介绍一些常用的参数;这些参数很大一部分既可以作为输入参数,也可以作为输出参数。
名称 | 作用 | 输入/输出 | 示例 |
---|---|---|---|
-r | 指定帧率 作为输入参数,重新解释输入文件的帧率,会影响播放时长; 作为输出参数,通过丢帧和复制帧控制输出帧率,但不会影响播放时长。 |
均可 | -r 20<br> 通常会用在序列帧作为输入时 |
-f | 指定封装格式 | 均可 | -f mp4 -f mov |
-c | 指定编解码器 -c:v 指定视频编解码器 -c:a 指定音频编解码器 |
均可 | -c h264<br> -c:v h264<br> -c:a aac |
-b | 指定比特率 -b:v 指定视频比特率 -b:a 指定音频比特率 -b 指定总比特率 |
仅输出 | -b 2500K<br> -b:v 2500K<br> -b:a 100K<br> 有的编码器不支持,会被忽略。 |
-t | 指定时长 | 均可 | -t 20s |
参数还有很多,可以通过man ffmpeg
或者官方文档按需查看。
Filter
除了格式、帧率这些输出参数,最特殊的参数就是-filter
和-filter_complex
,这两个参数可以用来指定该输出文件要使用的filter。
刚才简单介绍过,filter可以对音视频原始数据做一些常见的处理。
ffmpeg自带了大量的filter,比如我电脑上的5.0版本,就有470+个filter(可以通过ffmpeg -filters
来列出所有的filter)。这些filter提供了各种各样的功能,小到缩放、剪切,大到物体识别,甚至于可以在其中插入GPU代码,总之五花八门。
我们可以通过-filter
参数指定一个filter,来实现需要的功能。
例如,ffmpeg -i i.mp4 -filter scale=100x100 o.mp4
就可以将i.mp4缩放到100x100的大小。
filter的语法是这样的:filter名=参数
,其中filter名是scale这些,每个filter可以接受不同的参数,参数之间由:
分开;上面的scale=100x100
也可以写成scale=w=100:h=100
。
filter中也可以访问一些变量,例如scale=iw/2:ih/2
就可以将原视频宽高都缩小为原来的1/2,其中的iw/ih
是filter可以访问的变量,iw
指的是输入文件的宽度,ih
指的是高度。
可以通过ffmpeg -filters
命令来查看所有的filter;ffmpeg -h filter=scale
命令可以查看某个filter的详细说明,包括介绍、参数等。或者可以去官网上查看网页版本的filter文档。
-filter
参数只能指定一条“链状”的filter,而-filter_complex
参数则可以组合多个filter,形成树状,组合起来的filter一般称作filter graph
。
例如:
ffmpeg -i i.mp4 -filter_complex \
'split[v1][v2];[v1]scale=iw/2:ih/2[v1];[v2][v1]overlay' o.mp4
每个filter用;
分开;每个filter都有输入和输出,在filter后加上[abc]
可以将输出命名为abc
;在filter的前面加上[abc]
则可以将abc
作为输入;通过这种命名的方法,可以实现很复杂的功能。
上面这个命令分开来看的话有三个filter:
split``[v1][v2]
将输入文件复制成两份,分别命名为v1和v2(未指定输入的话默认选用上一个输入,在这里就是-i
选项指定的文件);[v1]``scale``=``iw/2``:``ih/2``[v1]
将v1缩放为原来的一半,同时将结果再次命名为v1;[v2][v1]``overlay
overlay用于将v1和v2堆叠起来,v1位于v2上方。
经过这个命令之后,对于这样的输入:
会得到这样的输出:
示例
图片处理
除了音视频,ffmpeg处理图片当然也不在话下,例如ffmpeg -i input.png output.webp
就可以将png转为webp。
Webp动图
处理webp的时候要注意,如果要从视频或者序列帧生成webp动图,需要指定编码为libwebp_anim
,例如ffmpeg -i i.mp4 -c libwebp_anim o.webp
,直接使用webp编码会出现播放花屏的情况。
PNG压缩
由于PNG是个无损压缩的格式,体积往往比较大,大家平时可能用到TinyPNG这个网站来压缩一些图片,它的原理是将通常每像素24位、64位的PNG图像修改为每像素8位的。
之所以能压缩成8位这么小,是因为它用了pal8这种像素格式,这是一种基于色板的像素格式,在用它对一张图片进行“编码”的时候,需要先指定一张“色板”:这张色板上只有256种颜色,每种颜色有个编号。通过这个色板对一张图片进行“编码”的时候,需要给这张图片上的每个像素在色板上找一个最相近的颜色,然后在该像素处只需要存储这个颜色对应的色板序号就行。最终在封装的时候,把色板也封装到里面,解码的时候就可以大致还原原图。这种方式适合一些色彩不是特别丰富的图像。
比如,一张图片的某一个像素是蓝色#0000FF,通常存储它需要3*8=24位,而如果色板上有这个颜色的话,存储它只需要一个字节,也就是该颜色对应的色板序号。
知道了原理,这个操作通过ffmpeg也很容易完成:
ffmpeg -i 0001.png \
-filter_complex '[0]palettegen=transparency_color=#00000000[p];[0][p]paletteuse' \
o.png
其中的关键部分在于中间这两个filter:palettegen
和paletteuse
,一个用来生成色板,一个用来使用色板。(色板其实也就是一张16x16的图片,也就是这里的[p])
通过这行命令,就可以得到一张和TinyPNG“压缩”效果差不多的图片(有时候会差很多)。
FFmpeg的色板生成算法比较简单,这方面有些人做了更为深入的探索,有兴趣的同学可以看下PngQuant这个项目。
为音乐添加封面
通常,从音乐网站或音乐app上面下载的音乐文件都会带有封面,在本地播放器播放、或者文件管理器预览时,可以看到该封面,例如使用ffplay:
ffplay with_pic.mp3
这个封面的原理是在文件中保存一个图片通道,可以通过ffprobe看到:
ffprobe with_pic.mp3
假设现在有个不带有封面的mp3音乐文件no_pic.mp3
,和一个封面图片pic.png
,那么可以通过ffmpeg向其中添加一个视频静态的通道:
ffmpeg \
-i pic.png `# 输入图片` \
-i nopic.mp3 `# 输入mp3` \
-codec copy `# 使用copy编码器,不对数据做处理` \
-map 0:v:0 `# 将第0个输入文件的视频通道,输出到第0个输出文件中` \
-map 1:a:0 `# 将第1个输入文件的音频通道,输出到第0个输出文件中` \
output.mp3
这里的关键是-map
选项,可以用它来操作文件或filter中的通道。
使用ffprobe检查output.mp3,或者使用ffplay播放该文件,就可以看到填充进去的封面了。
获取视频缩略图
通过以下命令,可以从视频中获取一帧并保存成图片:
ffmpeg -y -ss 00:10:00 -i Sherlock.S01E01.2010.mkv -frames 1 thumbnail.png
其中的关键是-ss 00:10:00
和-frames 1
两个参数:
-ss
参数用于指定输入文件的偏移时间,这里指定的是十分钟的位置。-frames 1
参数用于指定输出文件的帧数,只需要一帧。
官方文档对此也有个简单的介绍,可以参考Seeking - FFmpeg。
基于此可以为视频生成一个简单的抽帧预览的小视频:
用到的python脚本如下:
#!/usr/bin/env python3
from os import system
from pathlib import Path
Path('thumbnails/').mkdir(exist_ok=True)
for i in range(10):
min = str(i * 5).zfill(2) # 每五分钟一张图片
name = str(i).zfill(2)
system(f"ffmpeg -y -ss 00:{min}:40 -i 'Sherlock.S01E01.2010.mkv' -frames 1 'thumbnails/{name}.png'")
# 输入一秒一帧,输出时补充为25帧
system(f"ffmpeg -y -r 1 -i 'thumbnails/%02d.png' -r 25 thumbnails/preview.mp4")
Alpha视频
最后简单说一下一个比较复杂的例子,串联一下上述知识。
假设我们有一串序列帧,0001.png、0002.png、...0100.png,
现在想要把它转换成Alpha视频,也就是这样的:
RGB通道在右侧,Alpha通道按照灰度图的形式在左侧。
转换前后
首先来看下转换前后的异同。
转换前的序列帧是PNG格式的,拥有RGBA四个通道,上图魔方的其余部分都是空白的。
转换后的格式为mp4,mp4不支持Alpha通道。
转换后的视频右侧部分,其实就是原序列帧的RGB通道,同时原序列帧的Alpha通道值为0的像素要显示为黑色。
左侧部分其实是将Alpha通道单独提取出来,然后将其转成灰度图,也就是将Alpha通道的值分别赋给RGB三个通道(例如#FF123456就变成了#FFFFFFFF,完全透明的部分就变成了黑色,完全不透明的部分就变成了白色)。
实现
那我们可以使用这行命令:
ffmpeg -r 25 -i '%04d.png' \
-filter_complex 'split[rgb][mask];[mask]colorchannelmixer=0:0:0:1:0:0:0:1:0:0:0:1:0:0:0:1[mask];[rgb]split[fg][bg];[bg]drawbox=t=fill:color=black[bg];[bg][fg]overlay[rgb];[mask]pad=iw*2:ih[mask];[mask][rgb]overlay=w:0' \
o.mp4
将其中的filter拆开可以得到许多个步骤,分别介绍一下:
-
split[mask][rgb];
将输入复制成两份,名字为mask和rgb,最终分别会用作左边的遮罩部分和右边的彩色部分;
-
[mask]colorchannelmixer=0:0:0:1:0:0:0:1:0:0:0:1:0:0:0:1[mask];
colorchannelmixer
这个filter用于重组rgba通道,这里通过将rgba通道的值都设置为alpha通道的值,来得到左边的遮罩,也就是将alpha通道转化为灰度图。 -
[rgb]split[fg][bg];
把另外一份再次复制为两份,bg和fg;
-
[bg]drawbox=t=fill:color=black[bg];
bg作为右侧的背景,所以使用drawbox在其上绘制一个黑色的区域将其盖住;
-
[bg][fg]overlay[rgb];
然后通过overlay将fg盖在bg上方;
-
[mask]pad=iw*2:ih[mask];
将遮罩宽度*2,也就是给它右侧增加一块空白;
-
[mask][rgb]overlay=w:0
最后将rgb放到mask的右侧。
这里之所以先绘制一个黑色底色,是因为有的软件导出的PNG图片,在Alpha通道为0像素上,RGB三个通道的值可能是随机数;如果不做底色的话,ffmpeg会直接将这些随机数显示出来,就会导致类似于这样的图像:
经过上面几步,就可以完成这个转换。
另外,其中的-r 25
选项用来控制序列帧的帧率为25fps。
通过filter反向还原序列帧也是可行的,思路差不多。
获取帮助
如果要对ffmpeg、ffplay、ffprobe这些命令有更详细的认识,有两种方式:
- 在Mac或者Linux上使用包管理器安装FFmpeg之后,默认会带上FFmpeg的所有文档,可以通过命令行下执行
man ffmpeg
、man ffmpeg-all
、man ffplay
、man ffprobe
来查看,里面有完整的参数信息。 - 或者查看官网上面的在线文档,会更方便一些:ffmpeg Documentation。
如果在使用过程中,需要某个编解码器或者filter的文档,也随时可以通过命令行查看:
# 查看所有解码器
ffmpeg -decoders
# 查看所有编码器
ffmpeg -encoders
# 查看所有muxer
ffmpeg -muxers
# 查看所有demuxer
ffmpeg -demuxers
# 查看所有filter
ffmpeg -filters
# 获取某个解码器的文档
ffmpeg -h decoder=hevc
# 获取某个编码器的文档
ffmpeg -h encoder=hevc
# 查看某个muxer的文档
ffmpeg -h muxer=mp4
# 查看某个demuxer的文档
ffmpeg -h demuxer=mp4
# 获取某个filter的文档
ffmpeg -h filter=pad
这些帮助信息中,大多数时候包含了encoder/decoder/muxer/demuxer的可配置参数的解释,或是filter的功能、输入输出、参数类型和说明等。
同样的,也可以去官网上面查:Documentation。
例如,如果想要调节输出webp动图的质量,但是不知道怎么调节,可以使用ffmpeg -h encoder=libwebp_anim
命令来查看这个编码器支持的参数,会发现其中有一个参数,
因此可以通过这个参数来指定webp质量,例如:
ffmpeg -i i.mp4
-c libwebp_anim -quality 30
o.webp
源码
FFmpeg是一个用C写的项目,它的代码结构很清晰,整体被分成了几个大的模块:
- libavcodec 编解码器
- libavformat 封装、解封装库
- libavfilter Filter库,用于对媒体数据做中间处理
- libavdevice 用于支持在各个平台上访问媒体硬件(如摄像头、麦克风、屏幕等)
- libswscale 高性能的图像缩放和色彩空间转换库
- libswresample 高性能的音频重采样库
- libavutil 一些通用的工具函数
这些模块都可以编译成.so,如果是要在代码中使用,可以直接引用这些.so。