標籤:ffplay 原始碼 函數 ffmpeg
最近重溫了一下FFplay的原始碼。FFplay是FFmpeg項目提供的播放器樣本。儘管FFplay只是一個簡單的播放器樣本,它的原始碼的量也是不少的。之前看代碼,主要是集中於某一個“點”進行研究,而沒有從總體結構上進行分析。本文就打算彌補之前學習的不足,從總體結構上分析一下FFplay的原始碼,畫圖理一下它的結構。其中還有諸多不足,以後有機會慢慢完善。
說明一下自己畫的結構圖的規則:圖中僅畫出了比較重要的函數之間的調用關係。粉紅色的函數是FFmpeg編解碼類庫(libavcodec,libavformat等)的API。紫色的函數是SDL的API。其他不算很重要的函數就不再列出了。
在看ffplay.c的代碼之前,最好先看一下簡單的代碼瞭解FFmpeg播放一個視頻的核心代碼:
100行代碼實現最簡單的基於FFMPEG+SDL的視頻播放器
最簡單的基於FFmpeg+SDL的音頻播放器
總體結構圖
FFplay的總體函數調用結構圖如所示。
所示本是一張高清大圖。但是頁面顯示不下。因此上傳了一份:
http://my.csdn.net/leixiaohua1020/album/detail/1788077
上面地址的那張圖儲存下來的話就是一張清晰的圖片了。
下文對主要函數分別解析。
main()main()是FFplay的主函數。
調用了如下函數
av_register_all():註冊所有編碼器和解碼器。
show_banner():列印輸出FFmpeg版本資訊(編譯時間,編譯選項,類庫資訊等)。
parse_options():解析輸入的命令。
SDL_Init():SDL初始化。
stream_open ():開啟輸入媒體。
event_loop():處理各種訊息,不停地迴圈下去。
紅框中的內容即為show_banner()的輸出結果。
parse_options()parse_options()解析全部輸入選項。即將輸入命令“ffplay -f h264 test.264”中的“-f”這樣的命令解析出來。其函數調用結構如所示。需要注意的是,FFplay(ffplay.c)的parse_options()和FFmpeg(ffmpeg.c)中的parse_options()實際上是一樣的。因此本部分的內容和《ffmpeg.c函數結構簡單分析(畫圖)》中的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;
其中的重要欄位:
name:用於儲存選項的名稱。例如“i”,“f”,“codec”等等。
flags:儲存選項值的類型。例如:HAS_ARG(包含選項值),OPT_STRING(選項值為字串類型),OPT_TIME(選項值為時間類型。
u:儲存該選項的處理函數。
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中各種線程的建立。它的函數調用結構如所示。
stream_open()調用了如下函數:
packet_queue_init():初始化各個PacketQueue(視頻/音頻/字幕)
read_thread():讀取媒體資訊線程。
read_thread()
read_thread()調用了如下函數:
avformat_open_input():開啟媒體。
avformat_find_stream_info():獲得媒體資訊。
av_dump_format():輸出媒體資訊到控制台。
stream_component_open():分別開啟視頻/音頻/字幕解碼線程。
refresh_thread():視頻重新整理線程。
av_read_frame():擷取一幀壓縮編碼資料(即一個AVPacket)。
packet_queue_put():根據壓縮編碼資料類型的不同(視頻/音頻/字幕),放到不同的PacketQueue中。
refresh_thread()
refresh_thread()調用了如下函數:
SDL_PushEvent(FF_REFRESH_EVENT):發送FF_REFRESH_EVENT的SDL_Event
av_usleep():每兩次發送之間,間隔一段時間。
stream_component_open()
stream_component_open()用於開啟視頻/音頻/字幕解碼的線程。其函數調用關係如所示。
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()函數調用關係如下所示。
根據event_loop()中SDL_WaitEvent()接收到的SDL_Event類型的不同,會調用不同的函數進行處理(從編程的角度來說就是一個switch()文法)。圖中僅僅列舉了幾個例子:
SDLK_ESCAPE(按下“ESC”鍵):do_exit()。退出程式。
SDLK_f(按下“f”鍵):toggle_full_screen()。切換全螢幕顯示。
SDLK_SPACE(按下“空格”鍵):toggle_pause()。切換“暫停”。
SDLK_DOWN(按下滑鼠鍵):stream_seek()。跳轉到指定的時間點播放。
SDL_VIDEORESIZE(視窗大小發生變化):SDL_SetVideoMode()。重新設定寬高。
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:這算不上是一個函數,但是是一個獨立的功能模組,因此列了出來。該部分列印輸出播放的狀態至螢幕上。如所示。
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():顯示畫面至螢幕。
ffplay.c函數結構簡單分析(畫圖)