前言
本专题之所以说是「整体性学习」,是因为最好的学习的过程,是先看到大体的轮廓,再逐步探索细节。就像渐进式jpeg图像加载,从模糊的全局图到清晰的全局图。
先有个简单的全局认知,再有深入细致的认知
前面两篇文章都是概念性的解释,这篇文章,我们要下场翻代码了,Let's Do it!
kamuel:刻意练习FFmpeg系列:音视频基础概念格式和编码zhuanlan.zhihu.com
kamuel:刻意练习FFmpeg系列:颜色和像素zhuanlan.zhihu.com
本文将用一个最简单的图像格式来从整体性地探讨FFmpeg的流程和框架。在开始后面的阅读之前,先带着个疑问:FFmpeg怎么知道应该用哪个格式解封装器Demuxer的?
什么是PGM灰度图
PGM是一个可以用文本直接描述灰度图的图像格式,是Netpbm图像处理开源库中使用并定义的几种图形格式之一,Netpbm还包括PPM(可以是RGB彩色图)和PBM等几种,有时也统称PNM。
直接举个例子就明白了,下面这个文本,复制保存为demo.pgm,就可以是一张图像了。
P2 # 表明文件类型的Magic Number
# 可以加注释,这张图就是现实 "KAMU"四个字母
25 7 # 宽度和高度
15 # 色彩范围
# 下面是图像的像素,每个数字是一个像素
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 9 0 0 9 0 0 0 7 7 0 0 0 11 11 11 11 11 0 0 15 0 0 15 0
0 9 0 9 0 0 0 7 0 0 7 0 0 11 0 11 0 11 0 0 15 0 0 15 0
0 9 9 0 0 0 0 7 7 7 7 0 0 11 0 11 0 11 0 0 15 0 0 15 0
0 9 0 9 0 0 0 7 0 0 7 0 0 11 0 11 0 11 0 0 15 0 0 15 0
0 9 0 0 9 0 0 7 0 0 7 0 0 11 0 11 0 11 0 0 15 15 15 15 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
可以明显看到,PGM文件分为几部分,第一部分是文件头,第二部分是灰度的像素信息。
文件头包括:P2是表明这是PGM图像,25和7是图像尺寸的宽和高,15 是灰度色彩范围。
文件头之后是图像内容,是一系列的像素,每个数字代表一个像素,数字越大,越靠近白色,数字越小,越靠近黑色。
PNM系列图像还可以添加用#
符号添加注释,非常方便理解。
如果用Mac OS,previewer可以直接打开,效果是这样:
用预览打开pgm灰度图
尺寸太小了,放大效果是这样
放大的pgm灰度图
FFmpeg的下载和编译
FFmpeg的编译指南很多,本文只简单介绍,方便没有编译过的同学直接编译,因为后面我们需要进行一些调试工作。
首先用git下载
git clone -b n4.2.2 --depth=1 https://git.ffmpeg.org/ffmpeg.git
然后执行 configure 和 make,这里开启调试,并为了方便禁用OpenSSL。
./configure --enable-debug=3 --disable-optimizations
--disable-stripping --disable-openssl
make -j 4 # 4个线程并行编译
make doc/examples/avio_reading #编译示例
LLDB调试的简单入门
lldb启动调试
lldb -- ffplay_g ~/Works/media/images/demo.pgm
添加断点
(lldb) b main
Breakpoint 1: 24 locations.
控制运行
(lldb) r #运行 run
(lldb) n #下一行代码 next
(lldb) s #深入调用函数 step in
查看当前代码
(lldb) list
68
69 if (argc != 2) {
70 fprintf(stderr, "usage: %s input_filen"
71 "API example program to show how to read from a custom buffer "
72 "accessed through AVIOContext.n", argv[0]);
73 return 1;
74 }
输出变量
(lldb) p argc
(int) $0 = 2
准备工作做完,可以开始调试FFmpeg是怎么打开一个图像格式的了。
FFmpeg打开PGM图像过程
解封装器Demuxer
之前我们介绍到,FFmpeg打开一个格式,是使用demuxer,也就是AVInputFromat
。它的关键结构是这样的:
typedef struct AVInputFormat {
...
int(* read_probe )(const AVProbeData *)
int(* read_header )(struct AVFormatContext *)
int(* read_packet )(struct AVFormatContext *, AVPacket *pkt)
int(* read_close )(struct AVFormatContext *)
...
}
最主要的是这几种函数
- 试探文件格式
read_probe()
- 读取文件头
read_header()
- 读取数据包
read_packet()
- 关闭读取
read_close()
- 还有其他函数,暂时不关心
一个demuxer的具体例子
// 见 libavformat/mpeg.c
AVInputFormat ff_vobsub_demuxer = {
.name = "vobsub",
.long_name = NULL_IF_CONFIG_SMALL("VobSub subtitle format"),
.priv_data_size = sizeof(MpegDemuxContext),
.read_probe = vobsub_probe,
.read_header = vobsub_read_header,
.read_packet = vobsub_read_packet,
.read_seek2 = vobsub_read_seek,
.read_close = vobsub_read_close,
.flags = AVFMT_SHOW_IDS,
.extensions = "idx",
.priv_class = &vobsub_demuxer_class,
};
PGM Demuxer
其实判断是什么格式,主要就是通过read_probe()
来实现的。那么PGM图像的demuxer在哪里呢?我们可以直接搜索pgm。
➜ ffmpeg4.2.2 git:(4.2.2) grep --include="*.c" pgm libavformat -rin
...
libavformat/img2dec.c:955:static int pgm_probe(const AVProbeData *p)
...
可以看到有pgm_probe()
函数,就是它了。
开始调试
如果使用ffmpeg来把pgm转换为bmp,可以使用下面的代码。如果你的电脑不支持预览pgm,转换为bmp后,你就可以预览了。而且这是没任何失真的。
./ffmpeg_g -i /Works/media/images/demo.pgm /tmp/demo.bmp
然后可以用lldb调试这个过程
➜ ffmpeg4.2.2 git:(4.2.2) lldb -- ffmpeg_g -i /Works/media/images/demo.pgm /tmp/demo.bmp
(lldb) target create "ffmpeg_g"
Current executable set to '/Works/cpp/ffmpeg4.2.2/ffmpeg_g' (x86_64).
(lldb) settings set -- target.run-args "-i" "/Works/media/images/demo.pgm" "/tmp/demo.bmp"
直接设置断点pgm_probe()
(lldb) b pgm_probe
Breakpoint 1: where = ffmpeg_g`pgm_probe + 12 at img2dec.c:957:26, address = 0x000000010043decc
用r
执行,我们就能进入到pgm_probe()
函数了
(lldb) r
Process 7766 launched: '/Works/cpp/ffmpeg4.2.2/ffmpeg_g' (x86_64
...
Process 7766 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: . ffmpeg_g`pgm_probe(p=.) at img2dec.c:957:26
954
955 static int pgm_probe(const AVProbeData *p)
956 {
-> 957 int ret = pgmx_probe(p);
958 return ret && !av_match_ext(p->filename, "pgmyuv") ? ret : 0;
959 }
960
Target 0: (ffmpeg_g) stopped
用bt
看看调用栈
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: . ffmpeg_g`pgm_probe(p=.) at img2dec.c:957:26
frame #1: . ffmpeg_g`av_probe_input_format3(pd=., is_opened=1, score_ret=.) at format.c:165:21
frame #2: . ffmpeg_g`av_probe_input_format2(pd=., is_opened=1, score_max=.) at format.c:208:37
frame #3: . ffmpeg_g`av_probe_input_buffer2(pb=., fmt=., filename="/images/demo.pgm", logctx=., offset=0, max_probe_size=1048576) at format.c:280:16
frame #4: . ffmpeg_g`init_input(s=., filename="/images/demo.pgm", options=.) at utils.c:443:12
frame #5: . ffmpeg_g`avformat_open_input(ps=., filename="/images/demo.pgm", fmt=., options=.) at utils.c:573:16
frame #6: . ffmpeg_g`open_input_file(o=., filename="/images/demo.pgm") at ffmpeg_opt.c:1104:11
frame #7: . ffmpeg_g`open_files(l=., inout="input", open_file=(ffmpeg_g`open_input_file at ffmpeg_opt.c:998)) at ffmpeg_opt.c:3275:15
frame #8: . ffmpeg_g`ffmpeg_parse_options(argc=4, argv=.) at ffmpeg_opt.c:3315:11
frame #9: . ffmpeg_g`main(argc=4, argv=.) at ffmpeg.c:4872:11
frame #10: . libdyld.dylib`start + 1
frame #11: . libdyld.dylib`start + 1
这里只需要留意av_probe_input_format3()
,代码是如下。FFmpeg通过av_demuxer_iterate()
把所有的demuxer
解封装器都列出来了,然后循环逐个试探格式得分fmt1->read_probe()
,得分最高的解封装器会被返回。
ff_const59 AVInputFormat *av_probe_input_format3(ff_const59 AVProbeData *pd, int is_opened, int *score_ret)
{
...
while ((fmt1 = av_demuxer_iterate(&i))) {
...
score = 0;
if (fmt1->read_probe) {
score = fmt1->read_probe(&lpd);
...
}
if (score > score_max) {
score_max = score;
fmt = (AVInputFormat*) fmt1;
} else if (score == score_max)
fmt = NULL;
}
...
return fmt;
}
继续看pgm_probe()
,因为这里给出了PGM图像的试探得分。主要就是通过Magic Number来判断的,也就是看有无P2
P3
P4
等开头的两个魔术字符,
static int pgm_probe(const AVProbeData *p)
{
int ret = pgmx_probe(p);
return ret && !av_match_ext(p->filename, "pgmyuv") ? ret : 0;
}
static inline int pgmx_probe(const AVProbeData *p)
{
return pnm_magic_check(p, 2) || pnm_magic_check(p, 5) ? pnm_probe(p) : 0;
}
static int pnm_magic_check(const AVProbeData *p, int magic)
{
const uint8_t *b = p->buf;
return b[0] == 'P' && b[1] == magic + '0';
}
通过更改lldb的调试堆栈frame,可以向上找到调用pgm_probe()
的调用方 av_probe_input_format3()
(lldb) up
frame #1: . ffmpeg_g`av_probe_input_format3(pd=., is_opened=1, score_ret=.) at format.c:165:21
162 continue;
163 score = 0;
164 if (fmt1->read_probe) {
-> 165 score = fmt1->read_probe(&lpd);
166 if (score)
167 av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%dn", fmt1->name, score, lpd.buf_size);
168 if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
这时候试着把fmt1的名字打印出来,可以看到pgm_pipe
(lldb) p fmt1->name
(const char *const) $4 = ... "pgm_pipe"
用pgm_pipe
在libavformat/allformats.c
搜索,可以看到demuxer的命名是ff_image_pgm_pipe_demuxer
extern AVInputFormat ff_image_pgm_pipe_demuxer;
啊哈,原来FFmpeg是通过这个方式来判断文件格式的!
最后,让我们把ff_image_pgm_pipe_demuxer
的定义找出来,其实是在 libavformat/img2dec.c
// libavformat/img2dec.c
#define IMAGEAUTO_DEMUXER(imgname, codecid)
static const AVClass imgname ## _class = {
.class_name = AV_STRINGIFY(imgname) " demuxer",
.item_name = av_default_item_name,
.option = ff_img2pipe_options,
.version = LIBAVUTIL_VERSION_INT,
};
AVInputFormat ff_image_ ## imgname ## _pipe_demuxer = {
.name = AV_STRINGIFY(imgname) "_pipe",
.long_name = NULL_IF_CONFIG_SMALL("piped " AV_STRINGIFY(imgname) " sequence"),
.priv_data_size = sizeof(VideoDemuxData),
.read_probe = imgname ## _probe,
.read_header = ff_img_read_header,
.read_packet = ff_img_read_packet,
.priv_class = & imgname ## _class,
.flags = AVFMT_GENERIC_INDEX,
.raw_codec_id = codecid,
};
IMAGEAUTO_DEMUXER(pgm, AV_CODEC_ID_PGM)
通过探索代码,我们终于窥探到了FFmpeg处理多媒体文件的思路。
小结:
本文通过详细介绍FFmpeg打开PGM图像的流程,解答了FFmpeg是如何判断文件的「格式Format」这个疑问。
下一篇,我们将继续通过PGM图像来介绍FFmpeg的解码过程,不要错过哦。