至簡播放器ffplay工作原理

下載,編譯及運行

參考博文 http://blog.csdn.net/ericbar/article/details/79382783 即可完成ffplay的基本測試。
如果要進行GDB調試,需要先編譯一個帶GDB信息的執行文件,編譯前修改配置選項,

./configure --prefix=ffout --disable-stripping --disable-optimizations

保留GDB信息,以GDB方式運行ffplay,注意播放片源等參數是在運行命令 r 之後,

gdb ffplay
r 1080p.avi

main函數

C語言的代碼入口在main()函數裏,下面是main函數內部主要函數介紹:
init_dynload
動態加載的初始化,這是Windows平臺的dll庫相關處理;
av_log_set_flags
設置打印的標記,AV_LOG_SKIP_REPEATED表示對於重複打印的語句,不重複輸出;
avdevice_register_all
註冊音視頻相關的設備,主要是和操作系統底層相關的圖像,聲音設備註冊;
avfilter_register_all
註冊音視頻處理相關的資源,filter在FFmpeg裏主要是做各種變換的概念;
av_register_all
註冊複用/解複用相關的資源;
avformat_network_init
初始化網絡資源;
init_opts
初始化選項,通過av_dict_set設置字典,字典就是key-value對鍵值的集合,這在JAVA等高級語言裏是很常見的類,比如map,由於FFmpeg是C語言實現的,所以設立了一個字典的集合;
signal(SIGINT , sigterm_handler);
signal(SIGTERM, sigterm_handler);
註冊了2個信號,分別對應程序終止(interrupt)信號和程序結束(terminate)信號,用於異常處理;
show_banner
顯示FFmpeg和ffplay程序相關的庫版本等信息,類似如下:

ffplay version 3.4.2 Copyright (c) 2003-2018 the FFmpeg developers
  built with gcc 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.4) 20160609
  configuration: --prefix=ffout --disable-stripping
  libavutil      55. 78.100 / 55. 78.100
  libavcodec     57.107.100 / 57.107.100
  libavformat    57. 83.100 / 57. 83.100
  libavdevice    57. 10.100 / 57. 10.100
  libavfilter     6.107.100 /  6.107.100
  libswscale      4.  8.100 /  4.  8.100
  libswresample   2.  9.100 /  2.  9.100

parse_options
解析ffplay程序啓動的所有命令行參數,並存儲到對應的變量(options有默認值),如獲取要播放的媒體文件名,通過註冊的回調函數opt_input_file,將最終的媒體文件名存儲在變量input_filename中;
display_disable
video_disable
表示是否關閉視頻顯示;
audio_disable
表示是否關閉音頻的播放
以上兩個參數決定了SDL初始化時的flag標記;
SDL_Init
SDL初始化,完成顯示和音頻的初始化;
av_lockmgr_register
註冊解碼和解複用兩部分的互斥量,這是一個平臺適配接口,滿足互斥量的實現;
av_init_packet
初始化flush_pkt結構體,flush_pkt作爲連續包隊列中的標記,用於判斷是否連續的包;
SDL_CreateWindow
SDL_CreateRenderer
創建視頻顯示的圖層,SDL_GetRendererInfo獲取相關畫布信息;
stream_open
打開媒體文件播放,所有核心的播放流程代碼都由此函數實現;
event_loop
ffplay播放程序的按鍵和事件處理,包括快進快退,暫停,退出等;

stream_open函數

VideoState
在函數入口,將全局信息存儲在此結構體中,類似於上下文context;
frame_queue_init
初始化了3個幀隊列,分別保存解碼後的視頻,音頻以及字幕;
packet_queue_init
初始化了3個包隊列,分別存儲解碼前的視頻包,音頻包以及字幕包;
init_clock
完成視頻,音頻以及外部時鐘的初始化;
startup_volume
起播時的音量值,需要映射到SDL的音量範圍;
av_sync_type
爲音視頻同步的類型,一般以音頻時鐘爲同步基準;
SDL_CreateThread
SDL創建線程的函數,核心工作在read_thread線程中完成;

read_thread函數

avformat_alloc_context
分配一個avformat的上下文,context中的interrupt_callback是註冊到底層的回調函數,主要用於中斷退出,比如退出網絡阻塞。退出的條件由decode_interrupt_cb函數實現。
avformat_open_input
打開播放文件,得到context相關信息;
avformat_find_stream_info
分析媒體流的相關信息;
start_time變量
起播時跳轉到的時間戳位置;
realtime變量
是否是實時播放的節目,比如rtsp,udp,rtp節目;
av_dump_format
顯示媒體格式相關的內容,主要由dump_metadata和dump_stream_format函數完成,類似如下格式打印;

Input #0, avi, from '../1080p.avi':0KB vq=    0KB sq=    0B f=0/0   
  Metadata:
    encoder         : Lavf52.64.2
  Duration: 00:00:40.20, start: 0.000000, bitrate: 8204 kb/s
    Stream #0:0: Video: h264 (Main) (H264 / 0x34363248), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 7918 kb/s, 23.98 fps, 23.98 tbr, 23.98 tbn, 47.96 tbc
Stream #0:1: Audio: mp3 (U[0][0][0] / 0x0055), 48000 Hz, stereo, s16p, 320 kb/s

av_find_best_stream
查找適合的流進行播放;
av_guess_sample_aspect_ratio
獲取待解碼視頻的寬高比,從而得到視頻窗口的初始大小;
stream_component_open
打開視頻,音頻和字幕流;

if (is->paused != is->last_paused) {
            is->last_paused = is->paused;
            if (is->paused)
                is->read_pause_return = av_read_pause(ic);
            else
                av_read_play(ic);
        }

暫停狀態如果發生變化,執行暫停或者恢復播放;
if (is->seek_req)
如有seek請求,則執行seek,avformat_seek_file定位到目標位置,並清除解碼前的數據隊列,packet_queue_flush清除隊列,packet_queue_put存儲一個flush標記包flush_pkt;
stream_has_enough_packets
讀取隊列中是否還有足夠的數據包,如果已經足夠,本輪循環延遲10毫秒;
loop變量
表示循環播放的次數,播放到頭時,重新通過stream_seek函數定位到start_time的位置;
av_read_frame
讀取一個數據包(解碼前),這是數據讀取的核心函數,根據包的類型(視頻,音頻,字幕),通過packet_queue_put函數將讀取到的包,存儲到隊列裏;

fail:
    if (ic && !is->ic)
        avformat_close_input(&ic);

    if (ret != 0) {
        SDL_Event event;

        event.type = FF_QUIT_EVENT;
        event.user.data1 = is;
        SDL_PushEvent(&event);
    }
SDL_DestroyMutex(wait_mutex);

失敗時,關閉文件播放上下文,退出SDL事件管理,銷燬互斥量。

小結

上述過程,很好的解釋了怎樣打開一個媒體文件,然後怎樣進行數據包的讀取,那麼解碼以及顯示的工作在哪裏完成呢?其實是在stream_component_open函數中完成的,此函數根據不同的流類型,最後通過decoder_start各自啓動一個線程負責視頻,音頻及字幕的解碼。這部分我們放到後面討論。

主要函數流圖

下面是ffplay的主要函數調用關係圖
ffplay主要函數關係圖
從上圖可以看出,整個程序有如下4個線程:
read_thread
讀數據包線程,主要是通過函數av_read_frame從媒體文件中讀取包;
audio_thread
video_thread
subtitle_thread
上述3個線程分別負責音頻,視頻,字幕的解碼;
event_loop
其實我們也可以理解爲一個線程循環,這是主線程的while循環函數,除了顧名思義做事件相關的處理外,其實它還通過refresh_loop_wait_event函數中的video_refresh函數,完成視頻顯示和刷新。視頻顯示的時候,需要參考PTS時鐘信息進行同步。
而音頻的播放,由於音頻數據的讀取是由 SDL音頻管理器自動進行的,sdl_audio_callback函數由SDL主動回調並請求數據,我們只要在此回調函數裏完成音頻數據的解碼,並注入SDL音頻buffer即可。

總結

通過前面的介紹,大概瞭解了ffplay播放的主線,介紹了播放器打開文件,讀取數據包,解碼和顯示的主要函數,但是由於每一塊功能的實現原理都很複雜,並有很多基本理論需要了解,所以我們將在接下來的文章中,逐一展開分析,最終達到完整的掌握FFmpeg基本原理和實現機制的目的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章