背景說明
FFmpeg是一個開源,免費,跨平臺的視頻和音頻流方案,它提供了一套完整的錄製、轉換以及流化音視頻的解決方案。而ffplay是有ffmpeg官方提供的一個基於ffmpeg的簡單播放器。學習ffplay對於播放器流程、ffmpeg的調用等等是一個非常好的例子。本文就是對ffplay的一個基本的流程剖析,很多細節內容還需要繼續鑽研。
注:本文師基於ffmpeg-2.0版本進行分析,具體代碼行還請對號入座,謝謝!
主框架流程
下圖是一個使用“gcc+eygpt+graphviz+手工調整”生成的一個ffplay函數基本調用關係圖,其中只保留了視頻部分,去除了音頻處理、字幕處理以及一些細節處理部分。
注:圖中的數字表示了播放中的一次基本調用流程,X?序號表示退出流程。
從上圖中我們可以瞭解到以下幾種信息:
- 三個線程:主流程用於視頻圖像顯示和刷新、read_thread用於讀取數據、video_thread用於解碼處理;
- 視頻數據處理:由read_thread讀取原始數據解複用後,按照packet的方式放入到隊列中;由video_thread從packet隊列中讀取packet解碼後,按照picture的方式放入到隊列中;由主流程從picture隊列中依次取picture進行顯示;
- 啓動流程:啓動流程如上圖中的數字部分
- 退出流程:退出流程如上圖中的X?序號部分
下面將對三個線程分別加以詳細描述。
read_thread線程
從read_thread開始說起而不是從main線程,主要原因是考慮按照視頻數據轉換的方式比較好理解。
read_thread的創建是在main-->stream_open函數中:
is->read_tid = SDL_CreateThread(read_thread, is); |
read_thread線程主要分爲三部分:
- 初始化部分:主要包括SDL_mutex信號量創建、AVFormatContext創建、打開輸入文件、解析碼流信息、查找音視頻數據流並打開對應的數據流。對應ffplay.c文件中的2693-2810行代碼;
- 循環讀取數據部分:主要包括pause和resume操作處理、seek操作處理、packet隊列寫入失敗處理、讀數據結束處理、然後是讀數據並寫入到對應的音視頻隊列中。對應ffplay.c文件中的2812-2946行代碼;
- 反初始化部分:主要包括退出前的等待、關閉音視頻流、關閉avformat、給主線程發送FF_QUIT_EVENT消息以及銷燬SDL_mutex信號量。對應ffplay.c文件中的2947-2972行代碼;
初始化部分
主要包括SDL_mutex信號量創建、創建avformat上下文、打開輸入文件、解析碼流信息、查找音視頻數據流並打開對應的數據流。
創建wait_mutex互斥量
SDL_mutex *wait_mutex = SDL_CreateMutex(); |
該互斥量主要用於在對(VideoState *)is->continue_read_thread操作時加保護,如2887行和2925行:
//代碼段一 /* if the queue are full, no need to read more */ if (infinite_buffer<1 && ……) { /* wait 10 ms */ SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); <-- line 2887 SDL_UnlockMutex(wait_mutex); continue; }
//代碼段二 ret = av_read_frame(ic, pkt); if (ret < 0) { if (ret == AVERROR_EOF || url_feof(ic->pb)) eof = 1; if (ic->pb && ic->pb->error) break; SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); <-- line 2925 SDL_UnlockMutex(wait_mutex); continue; } |
而continue_read_thread從其名字上來看,是一個控制read_thread線程是否繼續阻塞的信號量,上面兩次阻塞的地方分別是:packet隊列已滿,需要等待一會(即超時10ms)或者收到信號重新循環;讀數據失敗,但是並不是IO錯誤(ic->pb->error),如讀取網絡實時數據時取不到數據,此時也需要等待或者收到信號重新循環。
注:seek操作時(L1216)和音頻隊列爲空(L2327)時,會發送continue_read_thread信號。
AVFormatContext創建
(AVFormatContext *)ic = avformat_alloc_context(); |
此處創建的avformat上下文,類似於一個句柄,後續所有avformat相關的函數調用第一個參數都是該上下文指針,如avformat_open_input、avformat_find_stream_info以及一些和av相關的函數接口第一個參數也是該指針,如av_find_best_stream、av_read_frame等等。
打開輸入文件
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts); |
創建好avformat上下文後,就打開is->filename指定的文件(或流),其中第三個和第四個參數可以傳NULL,由ffmpeg自動偵測待輸入流的文件格式,也可以通過is->iformat手動指定,format_opts參數表示設置的特殊屬性。
通過調用avformat_open_input函數,我們可以得到輸入流的一個基本信息。我們可以通過調用av_dump_format(ic, 0, is->filename, 0);來輸出解析後的碼流信息,可以得到如下數據:
Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0 Duration: N/A, bitrate: N/A Program 1 Stream #0:0[0x68]:Video:h264 ([27][0][0][0] / 0x001B), 90k tbn Stream #0:1[0x67]:Audio:aac([15][0][0][0] / 0x000F), 0 channels |
即,可以解析出
² 封裝格式是mpegts,包含兩路數據流
² 流1的PID是0x68,類型是視頻,編碼格式是H264
² 流2的PID是0x67,類型是音頻,編碼格式是AAC
但是隻有這些信息可定無法解碼,比如視頻的寬高比、圖像編碼格式(YUV or RGB …)、音頻採樣率、音頻聲道數量等等,以及Duration、bitrate等信息。這些信息都需要通過其他函數來解析。
解析碼流信息
err = avformat_find_stream_info(ic, opts); |
因爲avformat_open_input函數只能解析出一些基本的碼流信息,不足以滿足解碼的要求,因此我們調用avformat_find_stream_info函數來儘量的解析出所有的和輸入流相關的信息。
解析碼流的內部實現我們不在此處討論,先看一看調用後該函數後解析出來的信息(同樣採用av_dump_format來輸出):
Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0 Duration: 00:02:53.73, start: 2051.276989, bitrate: 1983 kb/s Program 1 Stream #0:0[0x68]: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuv420p, 1280x720, 30 tbr, 90k tbn,180k tbc Stream #0:1[0x67]: Audio: aac ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp,72 kb/s |
對比上一步獲取的信息,我們可以看到新解析出來的信息:
² 碼流信息;節目時長00:02:53.73,開始播放時間2051.276989,碼率1983 kb/s
² 視頻信息:色彩空間YUV420p,分辨率1280x720,幀率30,文件層的時間精度90k,視頻層的時間精度180K
² 音頻信息:採樣率48000,立體聲stereo,音頻採樣格式fltp(float, planar),音頻比特率72 kb/s
需要注意的是,該函數是一個阻塞操作,即默認情況下會在該函數中阻塞5s。具體的實現是在avformat_open_input函數中有一個for(;;) 循環,其中的一個break條件如下:
if (t >= ic->max_analyze_duration) { av_log(ic, AV_LOG_VERBOSE, "max_analyze_duration %d reached at %"PRId64" microseconds\n", ic->max_analyze_duration, t); break; } |
而ic->max_analyze_duration的默認值定義在options_table.h文件中,即默認的參數表:
{"analyzeduration", "specify how many microseconds are analyzed to probe the input", OFFSET(max_analyze_duration), AV_OPT_TYPE_INT, {.i64 = 5*AV_TIME_BASE }, 0, INT_MAX, D},
#define AV_TIME_BASE 1000000 <--file: avutil.h, line: 229 |
如果覺得這個默認的5s阻塞時間太長,或者甚至覺得完全沒有必要,即我們可以手動的設置各種解碼的參數,那麼可以通過下面的方法將ic->max_analyze_duration的值修改爲1s:
ic = avformat_alloc_context(); ic->interrupt_callback.callback = decode_interrupt_cb; ic->interrupt_callback.opaque = is; //add by Nfer ic->max_analyze_duration =1*1000*1000; av_log(NULL, AV_LOG_ERROR, "ic->max_analyze_duration %d.\n", ic->max_analyze_duration); err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts); |
注:紅色部分爲添加的代碼
查找音視頻數據流
if (!video_disable) st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, wanted_stream[AVMEDIA_TYPE_VIDEO], -1, NULL, 0); |
av_find_best_stream函數主要就做了一件事:找符合條件的數據流。其簡單實現可以參考ffmpeg-tutorial項目中tutorial01.c的代碼:
// Find the first video stream videoStream=-1; for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) { videoStream=i; break; } if(videoStream==-1) return -1; // Didn't find a video stream |
注:ffmpeg-tutorial項目是對Stephen Dranger寫的7個ffmpeg tutorial做的一個update。
打開對應的數據流
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]); } |
通過最開始的主框架流程圖,我們可以大概的看到stream_component_open函數中最主要的動作就是調用packet_queue_start和創建video_thread線程。當然在這之前還有一些處理,其中包括:
查找解碼器
avctx = ic->streams[stream_index]->codec; codec = avcodec_find_decoder(avctx->codec_id); |
如果啓動ffplay時通過vcodec參數指定了解碼器名稱,那麼在通過codec_id查找到解碼器後,再使用forced_codec_name查找解碼avcodec_find_decoder_by_name。但是注意,如果通過解碼器名稱查找後會覆蓋之前通過codec_id查找到解碼器,即如果在參數中指定了錯誤的解碼器會導致無法正常播放的。
設置解碼參數
opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec); if (!av_dict_get(opts, "threads", NULL, 0)) av_dict_set(&opts, "threads", "auto", 0); if (avctx->lowres) av_dict_set(&opts, "lowres", av_asprintf("%d", avctx->lowres), AV_DICT_DONT_STRDUP_VAL); if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO) av_dict_set(&opts, "refcounted_frames", "1", 0); |
打開解碼器
if (avcodec_open2(avctx, codec, &opts) < 0) return -1; |
啓動packet隊列
packet_queue_start(&is->videoq); |
啓動packet隊列時,會向隊列中先放置一個flush_pkt,其中詳細緣由後面再講。
創建video_thread線程
is->video_stream = stream_index; is->video_st = ic->streams[stream_index]; is->video_tid = SDL_CreateThread(video_thread, is); is->queue_attachments_req = 1; |
注:上述分析過程中沒有考慮音頻和字幕處理的部分,後續有機會再詳解。
循環讀取數據部分
該部分是一個for (;;)循環,循環中主要包括pause和resume操作處理、seek操作處理、packet隊列寫入失敗處理、讀數據結束處理、然後是讀數據並寫入到對應的音視頻隊列中。
for循環跳出條件
有兩處是break處理的:
//代碼段一 if (is->abort_request) break; <-- Line 2814
//代碼段二 ret = av_read_frame(ic, pkt); if (ret < 0) { if (ic->pb && ic->pb->error) break; <-- Line 2923 } |
其中條件一是調用do_exit --> stream_close中將is->abort_request置爲1的,代碼中有多個地方是判斷該條件進行exit處理的;條件二很清晰,就是當遇到讀數據失敗並且是IO錯誤時,會退出。
pause和resume操作處理
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); } |
在ffplay中暫停和恢復的按鍵操作時p鍵(SDLK_p)和space鍵(SDLK_SPACE),會調用toggle_pause--> stream_toggle_pause來修改is->paused標記變量,然後在read_thread線程中通過對is->paused標記變量的判斷進行pause和resum(play)的處理。
seek操作處理
if (is->seek_req) { ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags); if (is->video_stream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); } is->seek_req = 0; } |
注:上述代碼有所刪減,只保留了和視頻相關的部分
同上面pause和resume的處理,is->seek_req是在按鍵操作(SDLK_PAGEUP、SDLK_PAGEDOWN、SDLK_LEFT、SDLK_RIGHT、SDLK_UP和SDLK_DOWN)時,調用stream_seek函數來修改is->seek_req標記變量,然後在read_thread線程中根據is->seek_req標記變量來進行處理。
具體處理除了調用ffmpeg的avformat_seek_file接口外,還向packet隊列中放置了一個flush_pkt,這個在video_thread中的處理中會解決seek操作的花屏效果。
packet隊列寫入失敗處理
/* if the queue are full, no need to read more */ if (infinite_buffer<1 && (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE || ( (is->audioq .nb_packets > MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request) && (is->videoq .nb_packets > MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request || (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) && (is->subtitleq.nb_packets > MIN_FRAMES || is->subtitle_stream < 0 || is->subtitleq.abort_request)))) { /* wait 10 ms */ SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); continue; } |
此處的各種判斷條件不詳細解釋,重點是在播放器處理中,寫數據失敗時需要wait and continue的處理。
讀數據結束處理
if (eof) { if (is->video_stream >= 0) { av_init_packet(pkt); pkt->data = NULL; pkt->size = 0; pkt->stream_index = is->video_stream; packet_queue_put(&is->videoq, pkt); } SDL_Delay(10); if (is->audioq.size + is->videoq.size + is->subtitleq.size == 0) { if (loop != 1 && (!loop || --loop)) { stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0); } else if (autoexit) { ret = AVERROR_EOF; goto fail; } } eof=0; continue; } |
當遇到eof,即end of file時,做一下幾個步驟:
- 向packet隊列中放置一個null packet,此處用於loop時使用
- 判斷是否是loop操作,如果是就seek到開始位置重新播放
- 如果是autoexit模式,就goto fail退出
注意,在讀數據eof時,讀數據部分還有些滯後,即if (is->audioq.size + is->videoq.size + is->subtitleq.size== 0)判斷不一定爲true,引起在判斷前先delay了10ms(SDL_Delay(10););但是仍然不一定爲true,因此需要continue。當然下一步av_read_frame失敗也會返回AVERROR_EOF,eof會重新賦值爲1。即,eof退出會wait到真正的播放完畢。
讀數據並寫入到對應的音視頻隊列
ret = av_read_frame(ic, pkt); if (pkt->stream_index == is->video_stream && pkt_in_play_range && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) { packet_queue_put(&is->videoq, pkt); } |
注:上述代碼有所刪減,只保留了和視頻相關的部分
此處的處理實際上比較簡單,就是av_read_frame和packet_queue_put,不詳解。
反初始化部分
主要包括退出前的等待、關閉音視頻流、關閉avformat、給主線程發送FF_QUIT_EVENT消息以及銷燬SDL_mutex信號量。
退出前的等待
/* wait until the end */ while (!is->abort_request) { SDL_Delay(100); } |
因爲之前for循環跳出條件中說明了只有兩種情況下才會break出來,其一就是is->abort_request爲true,其二直接就goto到fail了,因此兩種情況下該while循環都不會判斷爲true,直接略過。具體代碼原因不明。
關閉音視頻流
if (is->video_stream >= 0) stream_component_close(is, is->video_stream); |
注:上述代碼有所刪減,只保留了和視頻相關的部分
其中stream_component_close關閉視頻流做了以下處理:
- 終止packet隊列:packet_queue_abort(&is->videoq);
- 發送信號給video_thread,避免繼續解碼阻塞:SDL_CondSignal(is->pictq_cond);
- 等待vide_thread線程退出:SDL_WaitThread(is->video_tid, NULL);
- 清空packet隊列:packet_queue_flush(&is->videoq);
給主線程發送FF_QUIT_EVENT
if (ret != 0) { SDL_Event event; event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); } |
在主線程會接收到FF_QUIT_EVENT消息,從而會調用do_exit函數來做退出處理。
銷燬SDL_mutex信號量
SDL_DestroyMutex(wait_mutex); |
read_thread基本就分析到這裏,下面描述以下video_thread。
video_thread線程
從主框架流程中可以看出,video_thread線程是在read_thread--> stream_component_open中創建的,負責從packet隊列中讀取packet並解碼爲picture,然後存儲到picture隊列中供主線程讀取並刷新顯示。
video_thread的創建是在read_thread --> stream_component_open函數中:
is->video_tid = SDL_CreateThread(video_thread, is); |
read_thread線程同樣分爲三部分:
- 初始化部分:主要包括AVFrame創建和AVFilterGraph創建。對應ffplay.c文件中的1881-1895行代碼;
- 循環解碼部分:主要包括pause和resume操作處理、讀取packet處理、AVFILTER處理、然後是將picture寫入視頻隊列中以及每次解碼後的清理動作。對應ffplay.c文件中的1897-1966行代碼;
- 反初始化部分:主要包括刷新codec中的數據、釋放AVFilterGraph、釋放AVPacket以及釋放AVFrame。對應ffplay.c文件中的1972-1978行代碼;
初始化部分
該線程的初始化就是創建了AVFrame和AVFilterGraph,其中AVFilterGraph還是和編譯宏包含,如果沒有打開CONFIG_AVFILTER可以直接省略。
is->video_tid = SDL_CreateThread(video_thread, is); … … AVFrame *frame = av_frame_alloc(); #if CONFIG_AVFILTER AVFilterGraph *graph = avfilter_graph_alloc(); #endif |
循環解碼部分
主要包括pause和resume操作處理、讀取packet處理、AVFILTER處理、然後是將picture寫入視頻隊列中以及每次解碼後的清理動作。
pause和resume操作處理
video_thread中的關於pause和resume的處理比較簡單,就是如果是pause狀態就delay(線程sleep):
while (is->paused && !is->videoq.abort_request) SDL_Delay(10); |
讀取packet處理
avcodec_get_frame_defaults(frame); av_free_packet(&pkt); ret = get_video_frame(is, frame, &pkt, &serial); //關於frame的一些處理 av_frame_unref(frame); |
從上述代碼中可以看出,一個frame(和packet)的完整生命流程。
在ffmpeg-tutorial項目中tutorial01.c中的例子是使用avcodec_alloc_frame()來申請並設置default value的操作,但是在這裏就分成了兩步:av_frame_alloc()然後avcodec_get_frame_defaults(frame)。
av_free_packet實際上清空上一次get_video_frame中獲取的packet數據,函數本身是有異常處理的,所以連續調用兩次av_free_packet是沒有問題的。
get_video_frame函數中主要部分是packet_queue_get然後avcodec_decode_video2,即從packet隊列中讀取數據然後進行解碼,具體內容有機會另開文章進行講解。
AVFILTER處理
AVFILTER處理是一個比較模塊化很高的處理部分,大致流程包括以下幾步:
- 釋放舊的AVFilterGraph並創建一個新的:avfilter_graph_free()和avfilter_graph_alloc()
- 配置video filters:configure_video_filters
- 向buffersrc中添加frame:av_buffersrc_add_frame
- 情況原有的frame和packet:av_frame_unref、avcodec_get_frame_defaults和av_free_packet
- 從buffersink中讀取處理後的frame:av_buffersink_get_frame_flags
簡單的理解就是:
將picture寫入視頻隊列
如果需要avfilter處理,那麼處理完後或者不需要avfilter處理,解碼完成後的frame會調用queue_picture寫入到picture隊列中。具體細節不詳解。
解碼後的清理動作
使用完packet後,必須從frame中釋放出來:av_frame_unref。如api說明:Unreference allthe buffers referenced by frame and reset the frame fields.
for循環跳出條件
有以下幾種情況下會break出for循環:
- get_video_frame讀數據失敗,並且返回<0:該函數失敗條件和read_thread其實是一致的,即當q->abort_request爲true時;
- configure_video_filters配置filter失敗:該函數失敗的情況下,我遇到的一種就是avfilter_graph_create_filter創建crop filter時失敗,原因在於在configureffmpeg時沒有把filter配置打開,導致只有默認的幾個filter,其他一些特性filter都沒有添加進行;
- av_buffersrc_add_frame添加frame失敗:該函數屬於api,不詳解;
- queue_picture保存picture失敗:該函數的失敗條件是當is->videoq.abort_request爲true時;
即正常情況下,有兩種退出模式:
- 正常播放完成後退出,此時會通過get_video_frame讀數據失敗退出
- 如果是按ESCAPE和Q鍵退出,會直接退出,則不會等到,直接在queue_picture函數失敗
反初始化部分
反初始化部分比較簡單,就是先通知avcodec進行flush數據,然後依次釋放AVFilterGraph、AVPacket和AVFrame。
video_thread講解的比較粗糙,主要原因還是由於個人瞭解的知識有所欠缺,後續有機會會補上。
主線程
主流程用於視頻圖像顯示和刷新,實際上還主線程是一個事件驅動的,就是一個wait_event然後switch處理,然後繼續for循環。
refresh_loop_wait_event處理
該函數會從event隊列中讀取出event,SDL_PumpEvents、SDL_PeepEvents。同時會調用video_refresh來進行視頻刷新和顯示。此處會有大量和SDL API相關的操作,由於個人能力有限暫不分析。
event的switch處理
該event的處理分爲以下幾類:
- SDL_KEYDOWN鍵盤按鍵事件
- SDL_VIDEOEXPOSE屏幕重畫事件
- SDL_MOUSEBUTTONDOWN鼠標按下事件,如果啓動ffplay時有exitonmousedown參數,會相應鼠標按下事件,然後退出播放;
- SDL_MOUSEMOTION鼠標移動事件,主要seek操作
- SDL_VIDEORESIZE視頻大小變化事件,比如視頻中間會出現大小變化,會觸發該事件
- SDL_QUIT、FF_QUIT_EVENT退出事件,如read_thread中出現各種異常會發送該消息
- FF_ALLOC_EVENT事件比較特殊,如代碼中的註釋“ifthe queue is aborted, we have to pop the pending ALLOC event or wait for theallocation to complete”,該消息是video_thread中的發出的消息
總結
由於時間有限,文章有些虎頭蛇尾,還請各位諒解。有多個方面沒有詳細分析,如音頻處理和字幕處理部分,音視頻同步,SDL顯示等等很多很多有關的知識,這些知識對於我來說大部分也還是全新的東西,後續有機會還會繼續學習和各位分享。