寫在前面
上一篇文章我大概跟蹤了一下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。**