ijkplayer系列(三) —— ijkplayer網絡數據讀取線程 寫在前面 數據讀取線程 總結 寫在前面 數據讀取線程 總結

寫在前面

上一篇文章我大概跟蹤了一下ijkplayer播放器的初始化流程,然後在IjkMediaPlayer_prepareAsync的時候我們發現它創建了幾個線程:

  • 視頻顯示線程
  • 數據讀取線程
  • 消息循環處理線程

如果還不清楚的童鞋可以返回看一下。

在本篇文章中,我們將會詳細地去了解數據讀取線程。

數據讀取線程

從上一文,我們瞭解到,數據讀取線程是在stream_open()/ff_ffplayer.c函數裏面創建的,那麼我們現在就回到這個函數。

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
        frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1)
        frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1)
        packet_queue_init(&is->videoq);
        packet_queue_init(&is->audioq);
        SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout") //視頻顯示線程創建
        SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read")
}

前面幾句都是初始化隊列的操作,分爲兩個隊列:一個是音頻,一個是視頻。創建這兩個隊列的作用,相信大家都能猜到。

frame_queue_init()的第一個參數是解碼出來的隊列,第二個參數是未解碼的隊列。

後面創建了兩個線程。我們現在只分析read_thread,同樣貼出部分重要代碼:

static int read_thread(void *arg)
{

//...
   err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
  //...
    err = avformat_find_stream_info(ic, opts);
//...
   /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
    }
//...
 for (;;) {
    if (is->seek_req) {
           avformat_seek_file();
       } 
    ret = av_read_frame(ic, pkt);
    packet_queue_put(&is->audioq, pkt); 
     //如果是視頻的話 
    //packet_queue_put(&is->videoq, pkt);  
    }
}

首先如果讓我們來實現一個讀取線程,我們是不是要先判斷視頻源的格式??沒錯,avformat_open_input()/Utils.c這句話內部就是探測數據源的格式。這裏會跳轉到ffmpeg裏面了,不在本文講解的範圍內,有興趣的童鞋可以自行閱讀下源碼,其實在avformat_open_input()/Utils.c內讀取網絡數據包頭信息時調用id3v2_parse(),然後獲取到頭信息後會執行ff_id3v2_parse_apic()/id3v2.c。最後 會更改ic這個AVFormatContext型的參數。

avformat_find_stream_info()解析流並找到相應解碼器。當然解碼器在我們上一文初始化的時候註冊過了,在哪裏呢?

JNI_Load()函數裏面有一個ijkmp_global_init(),然後它會調用avcodec_register_all();這個函數其實就是註冊解碼器等。

然後對於音頻或者視頻都會調用stream_component_open()函數來進行音視頻讀取和解碼,我們現在來看看這個函數:

static int stream_component_open(FFPlayer *ffp, int stream_index)
{
  //...
    codec = avcodec_find_decoder(avctx->codec_id);
    switch (avctx->codec_type) {
        case AVMEDIA_TYPE_AUDIO   : is->last_audio_stream    = stream_index; forced_codec_name = ffp->audio_codec_name; break;
        // FFP_MERGE: case AVMEDIA_TYPE_SUBTITLE:
        case AVMEDIA_TYPE_VIDEO   : is->last_video_stream    = stream_index; forced_codec_name = ffp->video_codec_name; break;
        default: break;
    }
//...
    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        /* prepare audio output */
        if ((ret = audio_open(ffp, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
            goto fail;
      //...
        decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
      //...
        if ((ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec")) < 0)
            goto fail;
        SDL_AoutPauseAudio(ffp->aout, 0);
        break;
    case AVMEDIA_TYPE_VIDEO:
   //...
        decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
        ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
   //...
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto fail;
       //...
        break;
    }
fail:
    av_dict_free(&opts);

    return ret;
}

type == audio
首先我們看到如果是audio的話,這個函數裏面會調用audio_open(),這個函數會接着調用aout->open_audio(aout, pause_on);這個open_audio()其實也是我們之前初始化的時候設置過了,最終會調用aout_open_audio_n(),最後會:

SDL_CreateThreadEx(&opaque->_audio_tid, aout_thread);

aout_thread線程從解碼好的音頻幀隊列sampq中,循環取數據推入AudioTrack中播放。這裏又創建了一個線程==。就叫音頻播放線程吧。

audio_open()裏面還有個需要注意的地方,就是有這麼一句,後面分析播放流程的時候會用到:

    wanted_spec.callback = sdl_audio_callback;

這裏繼續返回調用stream_component_open()的地方,然後接着後面有一個decoder_start();這個函數裏面會創建一個線程:

SDL_CreateThreadEx(&is->_audio_tid, audio_thread, ffp, "ff_audio_dec");

音頻解碼線程,從audioq隊列中獲取音頻包,解碼並加入sampq音頻幀列表中。入口函數是audio_thread()

type == video
這裏其實和上面的流程差不多。
先在stream_component_open()裏面創建了一個線程:

SDL_CreateThreadEx(&is->_video_tid, video_thread, ffp, "ff_video_dec")

視頻解碼線程,這個線程裏面分爲硬解碼和軟解碼。

  • 軟解:從videoq隊列中獲取視頻包,解碼視頻幀放入pictq列表中。
  • 硬解:從videoq隊列中獲取視頻包,推送MediaCodec解碼,獲取解碼outputbuffer index並存儲在pictq列表中。

接着在stream_component_open()後有一個無限for循環,作用是循環讀取數據,然後把數據放入相應的audio隊列或者video隊列中。

然後在for循環裏面,會先判斷是否暫停讀取數據,因爲有時隊列滿了。還會判斷是否拖動了快進或者後退操作。

在for循環的最後:

if (is->audio_stream >= 0) {
                    packet_queue_flush(&is->audioq);
                    packet_queue_put(&is->audioq, &flush_pkt);
                }
#ifdef FFP_MERGE
                if (is->subtitle_stream >= 0) {
                    packet_queue_flush(&is->subtitleq);
                    packet_queue_put(&is->subtitleq, &flush_pkt);
                }
#endif
                if (is->video_stream >= 0) {
                    if (ffp->node_vdec) {
                        ffpipenode_flush(ffp->node_vdec);
                    }
                    packet_queue_flush(&is->videoq);
                    packet_queue_put(&is->videoq, &flush_pkt);
                }

循環地向幾個隊列中put數據,這裏看起來有3個隊列,分別是音頻,視頻,字幕。然而作者前面似乎把字幕屏蔽過了。

總結

  • 從上面的流程可以看出,在讀取數據的時候,當queue滿了的時候,會delay。然而並不會斷開連接。
  • 在讀取的時候,有很多操作,這些操作都是受java層界面的影響,比如pause和resume操作,seek操作等。如果界面按了暫停什麼的,都會反饋到這裏,然後這裏無限for循環的時候會相應作出各種操作。
  • 這裏會不斷讀取音頻和視頻,然後放入到相應隊列中。
  • read_thread線程裏面會創建兩個解碼線程,一個音頻播放線程。

數據讀取線程大概完成了。如果有缺漏或者錯誤,歡迎拍磚!_

寫在前面

上一篇文章我大概跟蹤了一下ijkplayer播放器的初始化流程,然後在IjkMediaPlayer_prepareAsync的時候我們發現它創建了幾個線程:

  • 視頻顯示線程
  • 數據讀取線程
  • 消息循環處理線程

如果還不清楚的童鞋可以返回看一下。

在本篇文章中,我們將會詳細地去了解數據讀取線程。

數據讀取線程

從上一文,我們瞭解到,數據讀取線程是在stream_open()/ff_ffplayer.c函數裏面創建的,那麼我們現在就回到這個函數。

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
        frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1)
        frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1)
        packet_queue_init(&is->videoq);
        packet_queue_init(&is->audioq);
        SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout") //視頻顯示線程創建
        SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read")
}

前面幾句都是初始化隊列的操作,分爲兩個隊列:一個是音頻,一個是視頻。創建這兩個隊列的作用,相信大家都能猜到。

frame_queue_init()的第一個參數是解碼出來的隊列,第二個參數是未解碼的隊列。

後面創建了兩個線程。我們現在只分析read_thread,同樣貼出部分重要代碼:

static int read_thread(void *arg)
{

//...
   err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
  //...
    err = avformat_find_stream_info(ic, opts);
//...
   /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
    }
//...
 for (;;) {
    if (is->seek_req) {
           avformat_seek_file();
       } 
    ret = av_read_frame(ic, pkt);
    packet_queue_put(&is->audioq, pkt); 
     //如果是視頻的話 
    //packet_queue_put(&is->videoq, pkt);  
    }
}

首先如果讓我們來實現一個讀取線程,我們是不是要先判斷視頻源的格式??沒錯,avformat_open_input()/Utils.c這句話內部就是探測數據源的格式。這裏會跳轉到ffmpeg裏面了,不在本文講解的範圍內,有興趣的童鞋可以自行閱讀下源碼,其實在avformat_open_input()/Utils.c內讀取網絡數據包頭信息時調用id3v2_parse(),然後獲取到頭信息後會執行ff_id3v2_parse_apic()/id3v2.c。最後 會更改ic這個AVFormatContext型的參數。

avformat_find_stream_info()解析流並找到相應解碼器。當然解碼器在我們上一文初始化的時候註冊過了,在哪裏呢?

JNI_Load()函數裏面有一個ijkmp_global_init(),然後它會調用avcodec_register_all();這個函數其實就是註冊解碼器等。

然後對於音頻或者視頻都會調用stream_component_open()函數來進行音視頻讀取和解碼,我們現在來看看這個函數:

static int stream_component_open(FFPlayer *ffp, int stream_index)
{
  //...
    codec = avcodec_find_decoder(avctx->codec_id);
    switch (avctx->codec_type) {
        case AVMEDIA_TYPE_AUDIO   : is->last_audio_stream    = stream_index; forced_codec_name = ffp->audio_codec_name; break;
        // FFP_MERGE: case AVMEDIA_TYPE_SUBTITLE:
        case AVMEDIA_TYPE_VIDEO   : is->last_video_stream    = stream_index; forced_codec_name = ffp->video_codec_name; break;
        default: break;
    }
//...
    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        /* prepare audio output */
        if ((ret = audio_open(ffp, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
            goto fail;
      //...
        decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
      //...
        if ((ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec")) < 0)
            goto fail;
        SDL_AoutPauseAudio(ffp->aout, 0);
        break;
    case AVMEDIA_TYPE_VIDEO:
   //...
        decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
        ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
   //...
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto fail;
       //...
        break;
    }
fail:
    av_dict_free(&opts);

    return ret;
}

type == audio
首先我們看到如果是audio的話,這個函數裏面會調用audio_open(),這個函數會接着調用aout->open_audio(aout, pause_on);這個open_audio()其實也是我們之前初始化的時候設置過了,最終會調用aout_open_audio_n(),最後會:

SDL_CreateThreadEx(&opaque->_audio_tid, aout_thread);

aout_thread線程從解碼好的音頻幀隊列sampq中,循環取數據推入AudioTrack中播放。這裏又創建了一個線程==。就叫音頻播放線程吧。

audio_open()裏面還有個需要注意的地方,就是有這麼一句,後面分析播放流程的時候會用到:

    wanted_spec.callback = sdl_audio_callback;

這裏繼續返回調用stream_component_open()的地方,然後接着後面有一個decoder_start();這個函數裏面會創建一個線程:

SDL_CreateThreadEx(&is->_audio_tid, audio_thread, ffp, "ff_audio_dec");

音頻解碼線程,從audioq隊列中獲取音頻包,解碼並加入sampq音頻幀列表中。入口函數是audio_thread()

type == video
這裏其實和上面的流程差不多。
先在stream_component_open()裏面創建了一個線程:

SDL_CreateThreadEx(&is->_video_tid, video_thread, ffp, "ff_video_dec")

視頻解碼線程,這個線程裏面分爲硬解碼和軟解碼。

  • 軟解:從videoq隊列中獲取視頻包,解碼視頻幀放入pictq列表中。
  • 硬解:從videoq隊列中獲取視頻包,推送MediaCodec解碼,獲取解碼outputbuffer index並存儲在pictq列表中。

接着在stream_component_open()後有一個無限for循環,作用是循環讀取數據,然後把數據放入相應的audio隊列或者video隊列中。

然後在for循環裏面,會先判斷是否暫停讀取數據,因爲有時隊列滿了。還會判斷是否拖動了快進或者後退操作。

在for循環的最後:

if (is->audio_stream >= 0) {
                    packet_queue_flush(&is->audioq);
                    packet_queue_put(&is->audioq, &flush_pkt);
                }
#ifdef FFP_MERGE
                if (is->subtitle_stream >= 0) {
                    packet_queue_flush(&is->subtitleq);
                    packet_queue_put(&is->subtitleq, &flush_pkt);
                }
#endif
                if (is->video_stream >= 0) {
                    if (ffp->node_vdec) {
                        ffpipenode_flush(ffp->node_vdec);
                    }
                    packet_queue_flush(&is->videoq);
                    packet_queue_put(&is->videoq, &flush_pkt);
                }

循環地向幾個隊列中put數據,這裏看起來有3個隊列,分別是音頻,視頻,字幕。然而作者前面似乎把字幕屏蔽過了。

總結

  • 從上面的流程可以看出,在讀取數據的時候,當queue滿了的時候,會delay。然而並不會斷開連接。
  • 在讀取的時候,有很多操作,這些操作都是受java層界面的影響,比如pause和resume操作,seek操作等。如果界面按了暫停什麼的,都會反饋到這裏,然後這裏無限for循環的時候會相應作出各種操作。
  • 這裏會不斷讀取音頻和視頻,然後放入到相應隊列中。
  • read_thread線程裏面會創建兩個解碼線程,一個音頻播放線程。

數據讀取線程大概完成了。如果有缺漏或者錯誤,歡迎拍磚!_

** 如果大家還想了解ijkplayer的工作流程的話,可以關注下android下的ijkplayer。**

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