FFPlay視頻播放流程

背景說明

FFmpeg是一個開源,免費,跨平臺的視頻和音頻流方案,它提供了一套完整的錄製、轉換以及流化音視頻的解決方案。而ffplay是有ffmpeg官方提供的一個基於ffmpeg的簡單播放器。學習ffplay對於播放器流程、ffmpeg的調用等等是一個非常好的例子。本文就是對ffplay的一個基本的流程剖析,很多細節內容還需要繼續鑽研。

注:本文師基於ffmpeg-2.0版本進行分析,具體代碼行還請對號入座,謝謝!

主框架流程

下圖是一個使用“gcc+eygpt+graphviz+手工調整”生成的一個ffplay函數基本調用關係圖,其中只保留了視頻部分,去除了音頻處理、字幕處理以及一些細節處理部分。

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), yuv420p1280x72030 tbr, 90k tbn,180k tbc

    Stream #0:1[0x67]: Audio: aac ([15][0][0][0] / 0x000F), 48000 Hzstereofltp,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處理是一個比較模塊化很高的處理部分,大致流程包括以下幾步:

  1. 釋放舊的AVFilterGraph並創建一個新的:avfilter_graph_free()和avfilter_graph_alloc()
  2. 配置video filters:configure_video_filters
  3. 向buffersrc中添加frame:av_buffersrc_add_frame
  4. 情況原有的frame和packet:av_frame_unref、avcodec_get_frame_defaults和av_free_packet
  5. 從buffersink中讀取處理後的frame:av_buffersink_get_frame_flags

簡單的理解就是:

AVFILTER使用簡要流程

將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時;

即正常情況下,有兩種退出模式:

  1. 正常播放完成後退出,此時會通過get_video_frame讀數據失敗退出
  2. 如果是按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顯示等等很多很多有關的知識,這些知識對於我來說大部分也還是全新的東西,後續有機會還會繼續學習和各位分享。

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