0.引言
本篇文章也是針對前面ijk原始碼分析系列的繼續補充,為了更好的學習ijk,下面給出了文章內容列表,可以參考學習,參考文章如下:
ijk是bilibili基於ffplay封裝的主打移動端的播放器!在業內有較高的認可度!
參考部落格地址:
https://blog.csdn.net/biezhihua/article/details/90730061
ffplay的重要函式關係調用圖:
1.ffplay再分析
本章節將以該⽂件為主線,從資料接收、⾳影片解碼、⾳影片渲染及同步這三⼤⽅⾯進⾏講解,要求讀者有基本的ffmpeg知識。當外部調⽤ prepareToPlay 啟動播放後,ijkplayer內部最終會調⽤到ff_ffplay.c中的函式ffp_prepare_async_l(FFplayer *ffp,const char * file_name)。該⽅法是啟動播放器的入口函式,在此會設定player選項,開啟audio output,最重要的是調⽤stream_open方法,其原始碼如下:
static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat) { ....../* start video display */ if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0) goto fail; if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)goto fail; if (packet_queue_init(&is->videoq) < 0 || packet_queue_init(&is->audioq) < 0 ) goto fail; ...... is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout"); ...... is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read"); ...... }
從程式碼中可以看出,stream_open主要做了以下⼏件事情:
(1)建立存放video/audio解碼前資料的videoq/audioq。
(2)建立存放video/audio解碼後資料的pictq/sampq。
(3)建立讀資料執行緒read_thread。
(4)建立影片渲染執行緒video_refresh_thread。
注意:subtitle是與video、audio平⾏的⼀個stream,ffplay中也⽀持對它的處理,即建立存放解碼前後資料的兩個queue,並且當⽂件中存在subtitle時,還會啟動subtitle的解碼執行緒,由於篇幅有限,本⽂暫時忽略對它的相關介紹。
1.1 資料讀取
資料讀取的整個過程都是由ffmpeg內部完成的,接收到⽹絡過來的資料後,ffmpeg根據其封裝格式,完成了解復⽤的動作,我們得到的,是⾳影片分離開的解碼前的資料,步驟如下:
(1)建立上下⽂結構體,這個結構體是最上層的結構體,表示輸⼊上下⽂。
ic = avformat_alloc_context();
(2)設定中斷函式,如果出錯或者退出,就可以⽴刻退出。
ic->interrupt_callback.callback = decode_interrupt_cb; ic->interrupt_callback.opaque = is;
(3)開啟⽂件,主要是探測協議型別,如果是⽹絡⽂件則建立⽹絡連結等。
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->fo rmat_opts);
(4)探測媒體型別,可得到當前⽂件的封裝格式,⾳影片編碼引數等資訊。這個介面可能是一個耗時的操作。
err = avformat_find_stream_info(ic, opts);
(5)開啟影片、⾳頻解碼器。在此會開啟相應解碼器,並建立相應的解碼執行緒。
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
(6)讀取媒體資料,得到的是⾳影片分離的解碼前資料。
ret = av_read_frame(ic, pkt);
(7)將⾳影片資料分別送⼊相應的queue中。重複6、7步,即可不斷獲取待播放的資料。
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) { packet_queue_put(&is->audioq, pkt); } else if (pkt->stream_index == is->video_stream && pkt_in_play_ra nge && !(is->video_st && (is->video_st->disposition & AV_DISPOSITI ON_ATTACHED_PIC))) { packet_queue_put(&is->videoq, pkt); ...... } else { av_packet_unref(pkt); }
2.ijk⾳影片解碼
ijkplayer在影片解碼上⽀持軟解和硬解兩種⽅式,可在起播前配置優先使⽤的解碼⽅式,播放過程中不可切換。iOS平臺上硬解使⽤VideoToolbox,Android平臺上使⽤MediaCodec。ijkplayer中的⾳頻解碼只⽀持軟解(基本很多安卓裝置都是這樣),暫不⽀持硬解。
2.1影片解碼⽅式選擇
在開啟解碼器的⽅法中,找到合適解碼器,開啟解碼器,使用硬解還是軟解,開啟解碼執行緒。這裡以影片舉例分析。
如果是硬解,開啟ffmpeg的解碼器,然後透過ffpipeline_open_video_decoder(ffp->pipeline, ffp)建立IJKFF_Pipenode。在前面的文章有介紹,在建立IJKMediaPlayer物件時,透過ffpipeline_create_from_ios建立了pipeline。
static int stream_component_open(FFPlayer *ffp, int stream_index) { ...... codec = avcodec_find_decoder(avctx->codec_id); ...... if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) { goto fail; } ...... 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 (!ffp->node_vdec) goto fail; if ((ret = decoder_start(&is->viddec, video_thread, ffp,"ff_video_dec")) < 0) goto out;...... }
(1)以ios開啟解碼器為例子。如下程式碼:
IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp) { //回撥 return pipeline->func_open_video_decoder(pipeline, ffp); }
pipeline->func_open_video_decoder(pipeline, ffp)函式指標最後指向的是ffpipeline_ios.c中的func_open_video_decoder,其程式碼如下:
static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp){ IJKFF_Pipenode* node = NULL; IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;if (ffp->videotoolbox) { node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp); if (!node) ALOGE("vtb fail!!! switch to ffmpeg decode!!!! \n"); } if (node == NULL) { node = ffpipenode_create_video_decoder_from_ffplay(ffp); ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC; opaque->is_videotoolbox_open = false; } else { ffp->stat.vdec_type = FFP_PROPV_DECODER_VIDEOTOOLBOX; opaque->is_videotoolbox_open = true; } ffp_notify_msg2(ffp, FFP_MSG_VIDEO_DECODER_OPEN, opaque->is_videotoolbox_open); return node; }
(2)如果配置了ffp->videotoolbox,會使用函式ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp)優先去嘗試開啟硬體解碼器,程式碼如下:
node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
如果硬體解碼器開啟失敗,則會⾃動切換⾄軟解。
node = ffpipenode_create_video_decoder_from_ffplay(ffp);
注意,一定要在起播前去配置,ffp->videotoolbox需要在起播前透過如下⽅法配置。如下程式碼:
ijkmp_set_option_int(_mediaPlayer, IJKMP_OPT_CATEGORY_PLAYER, "v ideotoolbox", 1);
2.2⾳影片解碼執行緒
video的解碼執行緒為video_thread,audio的解碼執行緒為audio_thread。不管影片解碼還是⾳頻解碼,其基本流程都是從解碼前的資料緩衝區中取出⼀幀資料進⾏解碼,完成後放⼊相應的解碼後的資料緩衝區。本⽂以video的軟解流程為例進⾏分析,audio的流程可對照研究(前面的文章也有講audio ,可以參考)。影片解碼執行緒如下:
static int video_thread(void *arg) { FFPlayer *ffp = (FFPlayer *)arg; int ret = 0; if (ffp->node_vdec) { ret = ffpipenode_run_sync(ffp->node_vdec); } return ret; }
上面函式ffpipenode_run_sync(ffp->node_vdec)調⽤的是IJKFF_Pipenode物件中的func_run_sync。如下程式碼:
int ffpipenode_run_sync(IJKFF_Pipenode *node) { return node->func_run_sync(node); }
func_run_sync(node)取決於播放前配置的軟硬解,假設為軟解,則呼叫ffplay_video_thread(void *arg),原始碼如下:
static int ffplay_video_thread(void *arg){ FFPlayer *ffp = arg; ...... for (;;) { //解碼 ret = get_video_frame(ffp, frame); ...... //存放解碼後的資料 ret = queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial); } return 0;}
get_video_frame(ffp, frame)呼叫了decoder_decode_frame,其實現如下:
該⽅法中從解碼前的video queue中取出⼀幀資料,送⼊decoder進⾏解碼,解碼後的資料在ffplay_video_thread中送入pictq。
static int decoder_decode_frame(FFPlayer *ffp, Decoder *d, AVFrame *frame, AVSubtitle *sub) { int got_frame = 0; do { int ret = -1; ...... if (!d->packet_pending || d->queue->serial != d->pkt_serial){ AVPacket pkt; do { ...... if (packet_queue_get_or_buffering(ffp, d->queue,&pkt, &d->pkt_serial, &d->finished) < 0) return -1;...... } while (pkt.data == flush_pkt.data || d->queue->serial != d->pkt_serial); ...... } switch (d->avctx->codec_type) { case AVMEDIA_TYPE_VIDEO: { ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp); ...... } break; } ...... } while (!got_frame && !d->finished); return got_frame; }
2.3 ⾳影片同步及渲染
(1)⾳頻輸出
ijkplayer中Android平臺使⽤OpenSL ES或AudioTrack輸出⾳頻,iOS平臺使⽤AudioQueue輸出⾳頻。audio output節點,在ffp_prepare_async_l方法中被建立。如下程式碼:
ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
函式ffpipeline_open_audio_output(ffp->pipeline, ffp)⽅法實際上調⽤的是IJKFF_Pipeline物件的函式指標func_open_audio_output,該函式指標在初始化中的ijkmp_ios_create方法中被賦值,最後指向的是func_open_audio_output,如下程式碼:
static SDL_Aout *func_open_audio_output(IJKFF_Pipeline *pipeline,FFPlayer *ffp) { return SDL_AoutIos_CreateForAudioUnit(); }
SDL_AoutIos_CreateForAudioUnit()定義如下,主要完成的是建立SDL_Aout物件。從這裡可以看出使用外面註冊的音訊解碼函式進行解碼。
SDL_Aout *SDL_AoutIos_CreateForAudioUnit(){ SDL_Aout *aout = SDL_Aout_CreateInternal(sizeof(SDL_Aout_Opaque)); if (!aout) return NULL; // SDL_Aout_Opaque *opaque = aout->opaque; aout->free_l = aout_free_l; //使用ios第三方音訊解碼器 aout->open_audio = aout_open_audio; aout->pause_audio = aout_pause_audio; aout->flush_audio = aout_flush_audio; aout->close_audio = aout_close_audio; aout->func_set_playback_rate = aout_set_playback_rate; aout->func_set_playback_volume = aout_set_playback_volume;aout->func_get_latency_seconds = auout_get_latency_seconds; aout->func_get_audio_persecond_callbacks = aout_get_persecond_callbacks; return aout; }
回到ffplay.c中,如果發現待播放的⽂件中含有⾳頻,那麼在調⽤ stream_component_open 開啟解碼器時,該⽅法⾥⾯也調⽤ audio_open 打開了audio output裝置。這裡主要是以win為主。原始碼如下:
static int audio_open(FFPlayer *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params){ FFPlayer *ffp = opaque;VideoState *is = ffp->is; //配置音訊輸出相關的引數SDL_AudioSpec wanted_spec, spec; ...... wanted_nb_channels = av_get_channel_layout_nb_channels(wanted_channel_layout); wanted_spec.channels = wanted_nb_channels; wanted_spec.freq = wanted_sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.silence = 0; wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AoutGetAudioPerSecondCallBacks(ffp->aout)));wanted_spec.callback = sdl_audio_callback; wanted_spec.userdata = opaque; //這裡就可以開啟指定的音訊輸出,ios就是AudioQueue,win就是sdl,這都是由上面的函式指標指向的具體實現決定 while (SDL_AoutOpenAudio(ffp->aout, &wanted_spec, &spec) < 0){ ..... } ...... return spec.size; }
在 audio_open 中配置了⾳頻輸出的相關引數 SDL_AudioSpec wanted_spec, spec ,並透過設定給了Audio Output, iOS平臺上即為AudioQueue。AudioQueue模組在⼯作過程中,透過不斷的callback來獲取pcm資料進⾏播放。有關AudioQueue更加詳細的內容可以參考其它文章。
2.4 影片渲染
iOS平臺上採⽤OpenGL渲染解碼後的YUV影象,渲染執行緒為video_refresh_thread,最後渲染影象的⽅法為video_image_display2,定義如下:
1 static void video_image_display(FFPlayer *ffp) { VideoState *is = ffp->is; Frame *vp; Frame *sp = NULL; vp = frame_queue_peek_last(&is->pictq); ...... SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp); ...... }
從上面程式碼可以看出,該執行緒的主要工作就是如下:
(1)呼叫frame_queue_peek_last(&is->pictq),從is->pictq讀取當前需要顯示影片幀。
(2)調⽤SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp)進⾏繪製。繪製的原始碼如下:
int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay) { if (vout && overlay && vout->display_overlay) return vout->display_overlay(vout, overlay); return -1; }
上面函式實際呼叫的是vout->display_overlay(vout, overlay),在前⾯初始化流程有介紹過,指向的是如下函式,原始碼如下:
SDL_Vout *SDL_VoutIos_CreateForGLES2()
被賦值為 vout_display_overlay ,該⽅法本質就是調⽤OpengGL繪製圖像。
2.5⾳影片同步
對於播放器來說,⾳影片同步是⼀個關鍵點,同時也是⼀個難點,同步效果的好壞,直接決定著播放器的質量。通常⾳影片同步的解決⽅案就是選擇⼀個參考時鐘,播放時讀取⾳影片幀上的時間戳,同時參考當前時鐘參考時鐘上的時間來安排播放。
參考時間戳的做法:
如果⾳影片幀的播放時間⼤於當前參考時鐘上的時間,則不急於播放該幀,直到參考時鐘達到該幀的時間戳。如果⾳影片幀的時間戳⼩於當前參考時鐘上的時間,則需要“儘快”播放該幀或丟棄,以便播放進度追上參考時鐘。
參考時鐘的選擇也有多種⽅式:
(1)選取影片時間戳作為參考時鐘源
(2)選取⾳頻時間戳作為參考時鐘源
(3)選取外部時間作為參考時鐘源
考慮⼈對影片、和⾳頻的敏感度,在存在⾳頻的情況下,優先選擇⾳頻作為主時鐘源。ijkplayer在預設情況下也是使⽤⾳頻作為參考時鐘源,處理同步的過程主要在影片渲染video_refresh_thread的執行緒中,如下原始碼:
static int video_refresh_thread(void *arg) { FFPlayer *ffp = arg; VideoState *is = ffp->is; double remaining_time = 0.0; while (!is->abort_request) { if (remaining_time > 0.0) av_usleep((int)(int64_t)(remaining_time * 1000000.0)); remaining_time = REFRESH_RATE; if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)) //重新整理影片幀video_refresh(ffp, &remaining_time); } return 0; }
從上述實現可以看出,該⽅法中主要迴圈做兩件事情。
(4). 休眠等待, remaining_time 的計算在 video_refresh 中
(5). 調⽤ video_refresh ⽅法,重新整理影片幀
可⻅同步的重點是在 video_refresh 中,下⾯著重分析該⽅法,原始碼如下:
lastvp = frame_queue_peek_last(&is->pictq); vp = frame_queue_peek(&is->pictq); ...... /* compute nominal last_duration */last_duration = vp_duration(is, lastvp, vp); delay = compute_target_delay(ffp, last_duration, is);
lastvp是上⼀幀,vp是當前幀,last_duration則是根據當前幀和上⼀幀的pts,計算出來上⼀幀的顯示時間,經過 compute_target_delay ⽅法,計算出顯示當前幀需要等待的時間,計算方法的原始碼如下:
static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is) { double sync_threshold, diff = 0; /* update delay to follow master synchronisation source */ if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) { /* if video is slave, we try to correct big delays by duplicating or deleting a frame */ //計算影片時鐘與主時鐘的差值 diff = get_clock(&is->vidclk) - get_master_clock(is); /* skip or repeat frame. We take into account the delay to compute the threshold. I still don't know if it is the best guess */ sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay)); /* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */ if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) { //影片時鐘落後主時鐘 if (diff <= -sync_threshold) delay = FFMAX(0, delay + diff); //影片時鐘超前主時鐘 else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) delay = delay + diff;else if (diff >= sync_threshold) //加倍延時的策略 delay = 2 * delay; } } ..... return delay; }
在 compute_target_delay ⽅法中,diff = get_clock(&is->vidclk) - get_master_clock(is),這個函式是表示如果發現當前主時鐘源不是video,則計算當前影片時鐘與主時鐘的差值。
(1)如果當前影片幀落後於主時鐘源,則需要減⼩下⼀幀畫⾯的等待時間。一般可能就要drop。也就是程式碼中這一段,if (diff <= -sync_threshold) delay = FFMAX(0, delay + diff);
(2)如果影片幀超前,並且該幀的顯示時間⼤於顯示更新⻔檻,則顯示下⼀幀的時間為超前的時間差加上上⼀幀的顯示時間。也就是程式碼中這一段,else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)delay = delay + diff;一般可能會重複上一幀顯示或顯示的時間更長。
(3)如果影片幀超前,並且上⼀幀的顯示時間⼩於顯示更新⻔檻(也就是上一幀很快就顯示完了),則採取加倍延時的策略。一般可能會重複上一幀顯示。也就是程式碼中這一段,else if (diff >= sync_threshold) delay = 2 * delay;
繼續分析video_refresh。原始碼如下:
time= av_gettime_relative()/1000000.0; if (isnan(is->frame_timer) || time < is->frame_timer) is->frame_timer = time; if (time < is->frame_timer + delay) { *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display; }
frame_timer 實際上就是上⼀幀的播放時間,⽽ frame_timer + delay 實際上就是當前這⼀幀的播放時間,如果系統時間還沒有到當前這⼀幀的播放時間,直接跳轉⾄display,⽽此時 is->force_refresh 變數為0,不顯示當前幀,進⼊ video_refresh_thread 中下⼀次迴圈,並睡眠等待。
is->frame_timer += delay;//系統時間超前上一幀音訊顯示的時間 if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) is->frame_timer = time;//將當前幀的顯示時間修改為系統時間SDL_LockMutex(is->pictq.mutex); if (!isnan(vp->pts))update_video_pts(is, vp->pts, vp->pos, vp->serial);SDL_UnlockMutex(is->pictq.mutex);if (frame_queue_nb_remaining(&is->pictq) > 1) {Frame *nextvp = frame_queue_peek_next(&is->pictq);duration = vp_duration(is, vp, nextvp); if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop &&get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {//緩衝區有更多的資料,就需要丟幀。 frame_queue_next(&is->pictq); goto retry; }}
如果當前這⼀幀的播放時間已經過了,並且其和當前系統時間的差值超過了AV_SYNC_THRESHOLD_MAX ,對應這一段程式碼if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX),則將當前這⼀幀的播放時間改為系統時間,並在後續判斷是否需要丟幀,其⽬的是為後⾯幀的播放時間重新調整frame_timer,如果緩衝區中有更多的資料,並且當前的時間已經⼤於當前幀的持續顯示時間,則丟棄當前幀,嘗試顯示下⼀幀。這一些邏輯是比較複雜,需要反覆去琢磨,才能夠 理解。繼續看原始碼。
{frame_queue_next(&is->pictq); //這個標誌位是正常顯示或丟幀就會被置為1 is->force_refresh = 1; SDL_LockMutex(ffp->is->play_mutex); ...... display: /* display picture */ if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown) video_display2(ffp);
如果不是上述的情況,就進⼊正常顯示當前幀的流程,調⽤ video_display2 開始渲染。
3.事件處理
在播放過程中,某些⾏為的完成或者變化,如prepare完成,開始渲染等,需要以事件形式通知到外部,以便上層作出具體的業務處理。ijkplayer⽀持的事件⽐較多,具體定義在ijkplayer/ijkmedia/ijkplayer/ff_ffmsg.h中,如下原始碼:
#define FFP_MSG_FLUSH 0 #define FFP_MSG_ERROR 100 /* arg1 = error */ #define FFP_MSG_PREPARED 200 #define FFP_MSG_COMPLETED 300 #define FFP_MSG_VIDEO_SIZE_CHANGED 400 /* arg1 = width, arg2 = height */ #define FFP_MSG_SAR_CHANGED 401 /* arg1 = sar.num, arg2 = sar.den */ #define FFP_MSG_VIDEO_RENDERING_START 402 #define FFP_MSG_AUDIO_RENDERING_START 403 #define FFP_MSG_VIDEO_ROTATION_CHANGED 404 /* arg1 = degree */ #define FFP_MSG_BUFFERING_START 500 #define FFP_MSG_BUFFERING_END 501 #define FFP_MSG_BUFFERING_UPDATE 502 /* arg1 = buffering head position in time, arg2 = minimum percent in time or bytes */#define FFP_MSG_BUFFERING_BYTES_UPDATE 503 /* arg1 = cached data in bytes, arg2 = high water mark */#define FFP_MSG_BUFFERING_TIME_UPDATE 504 /* arg1 = cached duration in milliseconds, arg2 = high water mark */ #define FFP_MSG_SEEK_COMPLETE 600 /* arg1 = seek position, arg2 = error */ #define FFP_MSG_PLAYBACK_STATE_CHANGED 700 #define FFP_MSG_TIMED_TEXT 800 #define FFP_MSG_VIDEO_DECODER_OPEN 10001
3.1訊息上報初始化
在IJKFFMoviePlayerController的初始化⽅法中,原始碼如下:
- (id)initWithContentURLString:(NSString *)aUrlString withOptions:(IJKFFOptions *)options { ...... // init player _mediaPlayer = ijkmp_ios_create(media_player_msg_loop); ...... }
以ios為例子,可以看到在建立播放器時, media_player_msg_loop 函式地址作為引數傳⼊了ijkmp_ios_create ,繼續跟蹤程式碼,可以發現,該函式地址最終被賦值給了IjkMediaPlayer中的msg_loop 函式指標,如下原始碼:
IjkMediaPlayer *ijkmp_create(int (*msg_loop)(void*)) { ...... mp->msg_loop = msg_loop;...... }
開始播放時,會啟動⼀個訊息執行緒,原始碼如下:
static int ijkmp_prepare_async_l(IjkMediaPlayer *mp) { ...... mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_ms g_loop, mp, "ff_msg_loop"); ...... }
ijkmp_msg_loop ⽅法中調⽤的即是 mp->msg_loop 。⾄此已經完成了播放訊息傳送的準備⼯作。
3.2 訊息上報處理
播放器底層上報事件時,實際上就是將待發送的訊息放⼊訊息佇列,另外有⼀個執行緒會不斷從佇列中取出訊息,上報給外部。以prepare完成事件為例,看看程式碼中事件上報的具體流程。ff_ffplay.c中上報PREPARED完成時調⽤。
ffp_notify_msg1(ffp, FFP_MSG_PREPARED);
ffp_notify_msg1 ⽅法實現原始碼如下:
inline static void ffp_notify_msg1(FFPlayer *ffp, int what) { msg_queue_put_simple3(&ffp->msg_queue, what, 0, 0); }
msg_queue_put_simple3 中將事件及其引數封裝成了AVMessge物件,原始碼如下:
inline static void msg_queue_put_simple3(MessageQueue *q, int what, int arg1, int arg2) { AVMessage msg; msg_init_msg(&msg); msg.what = what; msg.arg1 = arg1; msg.arg2 = arg2; msg_queue_put(q, &msg); }
繼續跟蹤程式碼,發現是在函式msg_queue_put_private(MessageQueue *q, AVMessage*msg)中, 把訊息物件被放在了訊息佇列⾥。從哪裡讀取佇列的訊息呢?在建立播放器時,會傳入media_player_msg_loop 函式地址,作為⼀個單獨的執行緒運⾏,現在來看⼀下media_player_msg_loop ⽅法的原始碼實現:
int media_player_msg_loop(void* arg) { @autoreleasepool { IjkMediaPlayer *mp = (IjkMediaPlayer*)arg; __weak IJKFFMoviePlayerController *ffpController = ffplayerRetain(ijkmp_set_weak_thiz(mp, NULL)); while (ffpController) {@autoreleasepool { IJKFFMoviePlayerMessage *msg = [ffpController obtainMessage]; if (!msg) break; int retval = ijkmp_get_msg(mp, &msg->_msg, 1); if (retval < 0) break; // block-get should never return 0 assert(retval > 0); [ffpController performSelectorOnMainThread:@selector(postEvent:) withObject:msg waitUntilDone:NO]; } } // retained in prepare_async, before SDL_CreateThreadEx ijkmp_dec_ref_p(&mp); return 0; } }
從原始碼中可以看出,最後是透過ijkmp_get_msg(mp, &msg->_msg, 1)去讀取訊息的。這裡在部落格上找了一張圖片,可以便於理解和分析!
4.總結