ffplay.c函数结构简单分析

整理来自雷霄骅

总体结构图

FFplay的总体函数调用结构图

主要函数分别解析

main()

main()是FFplay的主函数,调用了如下函数

  1. av_register_all():注册所有编码器和解码器。
  2. show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。
  3. parse_options():解析输入的命令。
  4. SDL_Init():SDL初始化。
  5. stream_open ():打开输入媒体。
  6. event_loop():处理各种消息,不停地循环下去。

show_banner()

下图红框中的内容即为show_banner()的输出结果:


show_banner

parse_options()

parse_options()解析全部输入选项。
即将输入命令“ffplay -f h264 test.264”中的“-f”这样的命令解析出来。
其函数调用结构如下图所示:


parse_options

parse_options()调用了如下函数:
parse_option():解析一个输入选项。具体的解析步骤不再赘述。
parse_options()会循环调用parse_option()直到所有选项解析完毕。
FFmpeg的每一个选项信息存储在一个OptionDef结构体中。定义如下:

typedef struct OptionDef {
    const char *name;
    int flags;
#define HAS_ARG    0x0001
#define OPT_BOOL   0x0002
#define OPT_EXPERT 0x0004
#define OPT_STRING 0x0008
#define OPT_VIDEO  0x0010
#define OPT_AUDIO  0x0020
#define OPT_INT    0x0080
#define OPT_FLOAT  0x0100
#define OPT_SUBTITLE 0x0200
#define OPT_INT64  0x0400
#define OPT_EXIT   0x0800
#define OPT_DATA   0x1000
#define OPT_PERFILE  0x2000     /* the option is per-file (currently ffmpeg-only).
         implied by OPT_OFFSET or OPT_SPEC */
#define OPT_OFFSET 0x4000       /* option is specified as an offset in a passed optctx */
#define OPT_SPEC   0x8000       /* option is to be stored in an array of SpecifierOpt.
         Implies OPT_OFFSET. Next element after the offset is
         an int containing element count in the array. */
#define OPT_TIME  0x10000
#define OPT_DOUBLE 0x20000
     union {
        void *dst_ptr;
        int (*func_arg)(void *, const char *, const char *);
        size_t off;
    } u;
    const char *help;
    const char *argname;
} OptionDef;

其中的重要字段:

  1. name:用于存储选项的名称。例如“i”,“f”,“codec”等等。
  2. flags:存储选项值的类型。例如:HAS_ARG(包含选项值),OPT_STRING(选项值为字符串类型),OPT_TIME(选项值为时间类型。
  3. u:存储该选项的处理函数。
  4. help:选项的说明信息。

FFmpeg使用一个名称为options,类型为OptionDef的数组存储所有的选项。有一部分通用选项存储在cmdutils_common_opts.h中。这些选项对于FFmpeg,FFplay以及FFprobe都试用。
cmdutils_common_opts.h内容如下:

    { "L"          , OPT_EXIT, {(void*)show_license},      "show license" },
    { "h"          , OPT_EXIT, {(void*) show_help},         "show help", "topic" },
    { "?"          , OPT_EXIT, {(void*)show_help},         "show help", "topic" },
    { "help"       , OPT_EXIT, {(void*)show_help},         "show help", "topic" },
    { "-help"      , OPT_EXIT, {(void*)show_help},         "show help", "topic" },
    { "version"    , OPT_EXIT, {(void*)show_version},      "show version" },
    { "formats"    , OPT_EXIT, {(void*)show_formats  },    "show available formats" },
    { "codecs"     , OPT_EXIT, {(void*)show_codecs   },    "show available codecs" },
    { "decoders"   , OPT_EXIT, {(void*)show_decoders },    "show available decoders" },
    { "encoders"   , OPT_EXIT, {(void*)show_encoders },    "show available encoders" },
    { "bsfs"       , OPT_EXIT, {(void*)show_bsfs     },    "show available bit stream filters" },
    { "protocols"  , OPT_EXIT, {(void*)show_protocols},    "show available protocols" },
    { "filters"    , OPT_EXIT, {(void*)show_filters  },    "show available filters" },
    { "pix_fmts"   , OPT_EXIT, {(void*)show_pix_fmts },    "show available pixel formats" },
    { "layouts"    , OPT_EXIT, {(void*)show_layouts  },    "show standard channel layouts" },
    { "sample_fmts", OPT_EXIT, {(void*)show_sample_fmts }, "show available audio sample formats" },
    { "loglevel"   , HAS_ARG,  {(void*)opt_loglevel},      "set libav* logging level", "loglevel" },
    { "v",           HAS_ARG,  {(void*)opt_loglevel},      "set libav* logging level", "loglevel" },
    { "debug"      , HAS_ARG,  {(void*)opt_codec_debug},   "set debug flags", "flags" },
    { "fdebug"     , HAS_ARG,  {(void*)opt_codec_debug},   "set debug flags", "flags" },
    { "report"     , 0,        {(void*)opt_report}, "generate a report" },
    { "max_alloc"  , HAS_ARG,  {(void*) opt_max_alloc},     "set maximum size of a single allocated block", "bytes" },
    { "cpuflags"   , HAS_ARG | OPT_EXPERT, {(void*) opt_cpuflags}, "force specific cpu flags", "flags" },

options数组的定义位于ffplay.c中,如下所示:

static const OptionDef options[] = {
#include "cmdutils_common_opts.h"//包含进来
    { "x", HAS_ARG, { (void*) opt_width }, "force displayed width", "width" },
    { "y", HAS_ARG, { (void*) opt_height }, "force displayed height", "height" },
    { "s", HAS_ARG | OPT_VIDEO, { (void*) opt_frame_size }, "set frame size (WxH or abbreviation)", "size" },
    { "fs", OPT_BOOL, { &is_full_screen }, "force full screen" },
    { "an", OPT_BOOL, { &audio_disable }, "disable audio" },
    { "vn", OPT_BOOL, { &video_disable }, "disable video" },
    { "ast", OPT_INT | HAS_ARG | OPT_EXPERT, { &wanted_stream[AVMEDIA_TYPE_AUDIO] }, "select desired audio stream", "stream_number" },
    { "vst", OPT_INT | HAS_ARG | OPT_EXPERT, { &wanted_stream[AVMEDIA_TYPE_VIDEO] }, "select desired video stream", "stream_number" },
    { "sst", OPT_INT | HAS_ARG | OPT_EXPERT, { &wanted_stream[AVMEDIA_TYPE_SUBTITLE] }, "select desired subtitle stream", "stream_number" },
    { "ss", HAS_ARG, { (void*) opt_seek }, "seek to a given position in seconds", "pos" },
    { "t", HAS_ARG, { (void*) opt_duration }, "play  \"duration\" seconds of audio/video", "duration" },
    //选项众多,不再一一列出…
};

选项众多,简单举几个例子:
强行设置设置屏幕的宽度选项(“-x”选项):

{ "x", HAS_ARG, { (void*) opt_width }, "force displayed width", "width" }

从代码中可以看出,“-x”选项包含选项值(HAS_ARG),选项处理函数是opt_width()。选项说明是"force displayed width"。opt_width()的内容如下:

static int opt_width(void *optctx, const char *opt, const char *arg)
{
    screen_width = parse_number_or_die(opt, arg, OPT_INT64, 1, INT_MAX);
    return 0;
}

可以看出其作用是解析输入的字符串为整数并赋值给全局变量screen_width。
全屏(“-fs”选项)

{ "fs", OPT_BOOL, { &is_full_screen }, "force full screen" }

从代码中可以看出,“-fs”选项包含布尔型选项值(OPT_BOOL),并绑定了全局变量is_full_screen。选项说明是"force full screen"。

SDL_Init()

SDL_Init()用于初始化SDL。FFplay中视频的显示和声音的播放都用到了SDL。

stream_open()

stream_open()的作用是打开输入的媒体。这个函数还是比较复杂的,包含了FFplay中各种线程的创建。它的函数调用结构如下图所示。


image

stream_open()调用了如下函数:

  1. packet_queue_init():初始化各个PacketQueue(视频/音频/字幕)
  2. read_thread():读取媒体信息线程。

read_thread()

read_thread()调用了如下函数:

  1. avformat_open_input():打开媒体。
  2. avformat_find_stream_info():获得媒体信息。
  3. av_dump_format():输出媒体信息到控制台。
  4. stream_component_open():分别打开视频/音频/字幕解码线程。
  5. refresh_thread():视频刷新线程。
  6. av_read_frame():获取一帧压缩编码数据(即一个AVPacket)。
  7. packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的PacketQueue中。

refresh_thread()

refresh_thread()调用了如下函数:

  1. SDL_PushEvent(FF_REFRESH_EVENT):发送FF_REFRESH_EVENT的SDL_Event
  2. av_usleep():每两次发送之间,间隔一段时间。

stream_component_open()

stream_component_open()用于打开视频/音频/字幕解码的线程。其函数调用关系如下图所示。


image

stream_component_open()调用了如下函数:
avcodec_find_decoder():获得解码器。
avcodec_open2():打开解码器。
audio_open():打开音频解码。
SDL_PauseAudio(0):SDL中播放音频的函数。
video_thread():创建视频解码线程。
subtitle_thread():创建字幕解码线程。
packet_queue_start():初始化PacketQueue。

audio_open()调用了如下函数
SDL_OpenAudio():SDL中打开音频设备的函数。注意它是根据SDL_AudioSpec参数打开音频设备。SDL_AudioSpec中的callback字段指定了音频播放的回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。

下面来看一下SDL_AudioSpec中指定的回调函数sdl_audio_callback()。
sdl_audio_callback()调用了如下函数
audio_decode_frame():解码音频数据。
update_sample_display():当不显示视频图像,而是显示音频波形的时候,调用此函数。

audio_decode_frame()调用了如下函数
packet_queue_get():获取音频压缩编码数据(一个AVPacket)。
avcodec_decode_audio4():解码音频压缩编码数据(得到一个AVFrame)。
swr_init():初始化libswresample中的SwrContext。libswresample用于音频采样采样数据(PCM)的转换。
swr_convert():转换音频采样率到适合系统播放的格式。
swr_free():释放SwrContext。

video_thread()调用了如下函数
avcodec_alloc_frame():初始化一个AVFrame。
get_video_frame():获取一个存储解码后数据的AVFrame。
queue_picture():

get_video_frame()调用了如下函数
packet_queue_get():获取视频压缩编码数据(一个AVPacket)。
avcodec_decode_video2():解码视频压缩编码数据(得到一个AVFrame)。

queue_picture()调用了如下函数
SDL_LockYUVOverlay():锁定一个SDL_Overlay。
sws_getCachedContext():初始化libswscale中的SwsContext。Libswscale用于图像的Raw格式数据(YUV,RGB)之间的转换。注意sws_getCachedContext()和sws_getContext()功能是一致的。
sws_scale():转换图像数据到适合系统播放的格式。
SDL_UnlockYUVOverlay():解锁一个SDL_Overlay。

subtitle_thread()调用了如下函数
packet_queue_get():获取字幕压缩编码数据(一个AVPacket)。
avcodec_decode_subtitle2():解码字幕压缩编码数据。

event_loop()

FFplay再打开媒体之后,便会进入event_loop()函数,永远不停的循环下去。该函数用于接收并处理各种各样的消息。有点像Windows的消息循环机制。
PS:该循环确实是无止尽的,其形式为如下

SDL_Event event;
for (;;) {
    SDL_WaitEvent(&event);
        switch (event.type) {
        case SDLK_ESCAPE:
        case SDLK_q:
                do_exit(cur_stream);
                break;
        case SDLK_f:
        …
        …
        }
}

根据event_loop()中SDL_WaitEvent()接收到的SDL_Event类型的不同,会调用不同的函数进行处理(从编程的角度来说就是一个switch()语法)。图中仅仅列举了几个例子:

  1. SDLK_ESCAPE(按下“ESC”键):do_exit()。退出程序。
  2. SDLK_f(按下“f”键):toggle_full_screen()。切换全屏显示。
  3. SDLK_SPACE(按下“空格”键):toggle_pause()。切换“暂停”。
  4. SDLK_DOWN(按下鼠标键):stream_seek()。跳转到指定的时间点播放。
  5. SDL_VIDEORESIZE(窗口大小发生变化):SDL_SetVideoMode()。重新设置宽高。
  6. FF_REFRESH_EVENT(视频刷新事件(自定义事件)):video_refresh()。刷新视频。

下面分析一下do_exit()函数。该函数用于退出程序。函数的调用关系如下图所示。

do_exit()函数调用了以下函数
stream_close():关闭打开的媒体。
SDL_Quit():关闭SDL。

stream_close()函数调用了以下函数
packet_queue_destroy():释放PacketQueue。
SDL_FreeYUVOverlay():释放SDL_Overlay。
sws_freeContext():释放SwsContext。

下面重点分析video_refresh()函数。该函数用于将图像显示到显示器上。函数的调用关系如下图所示。

video_refresh()函数调用了以下函数
video_display():显示像素数据到屏幕上。
show_status:这算不上是一个函数,但是是一个独立的功能模块,因此列了出来。该部分打印输出播放的状态至屏幕上。如下图所示。

image

video_display()函数调用了以下函数
video_open():初始化的时候调用,打开播放窗口。
video_audio_display():显示音频波形图(或者频谱图)的时候调用。里面包含了不少画图操作。
video_image_display():显示视频画面的时候调用。

video_open()函数调用了以下函数
SDL_SetVideoMode():设置SDL_Surface(即SDL最基础的黑色的框)的大小等信息。
SDL_WM_SetCaption():设置SDL_Surface对应窗口的标题文字。

video_audio_display()函数调用了以下函数
SDL_MapRGB():获得指定(R,G,B)以及SDL_PixelFormat的颜色数值。例如获得黑色的值,作为背景。(R,G,B)为(0x00,0x00,0x00)。
fill_rectangle():将指定颜色显示到屏幕上。
SDL_UpdateRect():更新屏幕。

video_image_display()函数调用了以下函数
calculate_display_rect():计算显示画面的位置。当拉伸了SDL的窗口的时候,可以让其中的视频保持纵横比。
SDL_DisplayYUVOverlay():显示画面至屏幕。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容