随着網際網路技術的飛速發展,移動端播放視訊的需求如日中天,由此也催生了一批開源/閉源的播放器,但是無論這個播放器功能是否強大、相容性是否優秀,它的基本子產品通常都是由以下部分組成:事務處理、資料的接收和解複用、音視訊解碼以及渲染,其基本架構如下圖所示:
針對各種鋪天蓋地的播放器項目,我們選取了比較出衆的ijkplayer進行源碼剖析。它是一個基于FFPlay的輕量級Android/iOS視訊播放器,實作了跨平台的功能,API易于內建;編譯配置可裁剪,友善控制安裝包大小。
本文基于k0.7.6版本的ijkplayer,重點分析其C語言實作的核心代碼,涉及到不同平台下的封裝接口或處理方式時,均以iOS平台為例,Android平台大同小異,請大家自行查閱研究。
一、總體說明
打開ijkplayer,可看到其主要目錄結構如下:
tool - 初始化項目工程腳本
config - 編譯ffmpeg使用的配置檔案
extra - 存放編譯ijkplayer所需的依賴源檔案, 如ffmpeg、openssl等
ijkmedia - 核心代碼
ijkplayer - 播放器資料下載下傳及解碼相關
ijksdl - 音視訊資料渲染相關
ios - iOS平台上的上層接口封裝以及平台相關方法
android - android平台上的上層接口封裝以及平台相關方法
在功能的具體實作上,iOS和Android平台的差異主要表現在視訊硬體解碼以及音視訊渲染方面,兩者實作的載體差別如下表所示:
二、初始化流程
初始化完成的主要工作就是建立播放器對象,打開ijkplayer/ios/IJKMediaDemo/IJKMediaDemo.xcodeproj工程,可看到IJKMoviePlayerViewController類中viewDidLoad方法中建立了IJKFFMoviePlayerController對象,即iOS平台上的播放器對象。
- (void)viewDidLoad
{
......
self.player = [[IJKFFMoviePlayerController alloc] initWithContentURL:self.url withOptions:options];
......
}
檢視ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer/IJKFFMoviePlayerController.m檔案,其初始化方法具體實作如下:
- (id)initWithContentURL:(NSURL *)aUrl
withOptions:(IJKFFOptions *)options
{
if (aUrl == nil)
return nil;
// Detect if URL is file path and return proper string for it
NSString *aUrlString = [aUrl isFileURL] ? [aUrl path] : [aUrl absoluteString];
return [self initWithContentURLString:aUrlString
withOptions:options];
}
- (id)initWithContentURLString:(NSString *)aUrlString
withOptions:(IJKFFOptions *)options
{
if (aUrlString == nil)
return nil;
self = [super init];
if (self) {
......
// init player
_mediaPlayer = ijkmp_ios_create(media_player_msg_loop);
......
}
return self;
}
可發現在此建立了IjkMediaPlayer結構體執行個體_mediaPlayer:
IjkMediaPlayer *ijkmp_ios_create(int (*msg_loop)(void*))
{
IjkMediaPlayer *mp = ijkmp_create(msg_loop);
if (!mp)
goto fail;
mp->ffplayer->vout = SDL_VoutIos_CreateForGLES2();
if (!mp->ffplayer->vout)
goto fail;
mp->ffplayer->pipeline = ffpipeline_create_from_ios(mp->ffplayer);
if (!mp->ffplayer->pipeline)
goto fail;
return mp;
fail:
ijkmp_dec_ref_p(&mp);
return NULL;
}
在該方法中主要完成了三個動作:
- 建立IJKMediaPlayer對象
IjkMediaPlayer *ijkmp_create(int (*msg_loop)(void*))
{
IjkMediaPlayer *mp = (IjkMediaPlayer *) mallocz(sizeof(IjkMediaPlayer));
......
mp->ffplayer = ffp_create();
......
mp->msg_loop = msg_loop;
......
return mp;
}
通過ffp_create方法建立了FFPlayer對象,并設定消息處理函數
2.建立圖像渲染對象SDL_Vout
SDL_Vout *SDL_VoutIos_CreateForGLES2()
{
SDL_Vout *vout = SDL_Vout_CreateInternal(sizeof(SDL_Vout_Opaque));
if (!vout)
return NULL;
SDL_Vout_Opaque *opaque = vout->opaque;
opaque->gl_view = nil;
vout->create_overlay = vout_create_overlay;
vout->free_l = vout_free_l;
vout->display_overlay = vout_display_overlay;
return vout;
}
3.建立平台相關的IJKFF_Pipeline對象,包括視訊解碼以及音頻輸出部分
IJKFF_Pipeline *ffpipeline_create_from_ios(FFPlayer *ffp)
{
IJKFF_Pipeline *pipeline = ffpipeline_alloc(&g_pipeline_class, sizeof(IJKFF_Pipeline_Opaque));
if (!pipeline)
return pipeline;
IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
opaque->ffp = ffp;
pipeline->func_destroy = func_destroy;
pipeline->func_open_video_decoder = func_open_video_decoder;
pipeline->func_open_audio_output = func_open_audio_output;
return pipeline;
}
至此已經完成了ijkplayer播放器初始化的相關流程,簡單來說,就是建立播放器對象,完成音視訊解碼、渲染的準備工作。在下一章節中,會重點介紹播放的核心代碼。
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓
三、核心代碼剖析
ijkplayer實際上是基于ffplay.c實作的,本章節将以該檔案為主線,從資料接收、音視訊解碼、音視訊渲染及同步這三大方面進行講解,要求讀者有基本的ffmpeg知識。
ffplay.c中主要的代碼調用流程如下圖所示:
當外部調用
prepareToPlay
啟動播放後,ijkplayer内部最終會調用到ffplay.c中的
int 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主要做了以下幾件事情:
- 建立存放video/audio解碼前資料的videoq/audioq
- 建立存放video/audio解碼後資料的pictq/sampq
- 建立讀資料線程
read_thread
- 建立視訊渲染線程
video_refresh_thread
說明:subtitle是與video、audio平行的一個stream,ffplay中也支援對它的處理,即建立存放解碼前後資料的兩個queue,并且當檔案中存在subtitle時,還會啟動subtitle的解碼線程,由于篇幅有限,本文暫時忽略對它的相關介紹。
3.1 資料讀取
資料讀取的整個過程都是由ffmpeg内部完成的,接收到網絡過來的資料後,ffmpeg根據其封裝格式,完成了解複用的動作,我們得到的,是音視訊分離開的解碼前的資料,步驟如下:
- 建立上下文結構體,這個結構體是最上層的結構體,表示輸入上下文
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->format_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中
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_range && !(is->video_st && (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))) {
packet_queue_put(&is->videoq, pkt);
......
} else {
av_packet_unref(pkt);
}
重複6、7步,即可不斷擷取待播放的資料。
3.2 音視訊解碼
ijkplayer在視訊解碼上支援軟解和硬解兩種方式,可在起播前配置優先使用的解碼方式,播放過程中不可切換。iOS平台上硬解使用VideoToolbox,Android平台上使用MediaCodec。ijkplayer中的音頻解碼隻支援軟解,暫不支援硬解。
3.2.1 視訊解碼方式選擇
在打開解碼器的方法中:
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;
......
}
首先會打開ffmpeg的解碼器,然後通過
ffpipeline_open_video_decoder
建立IJKFF_Pipenode。
第二章節中有介紹,在建立IJKMediaPlayer對象時,通過
ffpipeline_create_from_ios
建立了pipeline,則
IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
return pipeline->func_open_video_decoder(pipeline, ffp);
}
func_open_video_decoder
函數指針最後指向的是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;
}
如果配置了ffp->videotoolbox,會優先去嘗試打開硬體解碼器,
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, "videotoolbox", 1);
3.2.2 音視訊解碼
video的解碼線程為
video_thread
,audio的解碼線程為
audio_thread
。
不管視訊解碼還是音頻解碼,其基本流程都是從解碼前的資料緩沖區中取出一幀資料進行解碼,完成後放入相應的解碼後的資料緩沖區,如下圖所示:
本文以video的軟解流程為例進行分析,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
中調用的是IJKFF_Pipenode對象中的
func_run_sync
int ffpipenode_run_sync(IJKFF_Pipenode *node)
{
return node->func_run_sync(node);
}
func_run_sync
取決于播放前配置的軟硬解,假設為軟解,則調用
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中
調用了
decoder_decode_frame
,其定義如下:
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;
}
該方法中從解碼前的video queue中取出一幀資料,送入decoder進行解碼,解碼後的資料在
ffplay_video_thread
中送入pictq。
3.3 音視訊渲染及同步
3.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
方法實際上調用的是IJKFF_Pipeline對象的函數指針
func_open_audio_utput
,該函數指針在初始化中的
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;
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裝置。
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;
while (SDL_AoutOpenAudio(ffp->aout, &wanted_spec, &spec) < 0) {
.....
}
......
return spec.size;
}
在
audio_open
中配置了音頻輸出的相關參數
SDL_AudioSpec
,并通過
int SDL_AoutOpenAudio(SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained)
{
if (aout && desired && aout->open_audio)
return aout->open_audio(aout, desired, obtained);
return -1;
}
設定給了Audio Output, iOS平台上即為AudioQueue。
AudioQueue子產品在工作過程中,通過不斷的callback來擷取pcm資料進行播放。
有關AudioQueue的具體内容此處不再介紹。
3.3.2 視訊渲染
iOS平台上采用OpenGL渲染解碼後的YUV圖像,渲染線程為
video_refresh_thread
,最後渲染圖像的方法為
video_image_display2
,定義如下:
static void video_image_display2(FFPlayer *ffp)
{
VideoState *is = ffp->is;
Frame *vp;
Frame *sp = NULL;
vp = frame_queue_peek_last(&is->pictq);
......
SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);
......
}
從代碼實作上可以看出,該線程的主要工作為:
- 調用
從pictq中讀取目前需要顯示視訊幀frame_queue_peek_last
- 調用
進行繪制SDL_VoutDisplayYUVOverlay
-
display_overlay函數指針在前面初始化流程有介紹過,它在int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay) { if (vout && overlay && vout->display_overlay) return vout->display_overlay(vout, overlay); return -1; }
SDL_Vout *SDL_VoutIos_CreateForGLES2()
方法中被指派為vout_display_overlay,該方法就是調用OpengGL繪制圖像。
3.4.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;
}
從上述實作可以看出,該方法中主要循環做兩件事情:
- 休眠等待,
的計算在remaining_time
中video_refresh
- 調用
方法,重新整理視訊幀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
方法中,如果發現目前主時鐘源不是video,則計算目前視訊時鐘與主時鐘的內插補點:
- 如果目前視訊幀落後于主時鐘源,則需要減小下一幀畫面的等待時間;
- 如果視訊幀超前,并且該幀的顯示時間大于顯示更新門檻,則顯示下一幀的時間為超前的時間差加上上一幀的顯示時間
- 如果視訊幀超前,并且上一幀的顯示時間小于顯示更新門檻,則采取加倍延時的政策。
回到
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
,則将目前這一幀的播放時間改為系統時間,并在後續判斷是否需要丢幀,其目的是為後面幀的播放時間重新調整frame_timer,如果緩沖區中有更多的資料,并且目前的時間已經大于目前幀的持續顯示時間,則丢棄目前幀,嘗試顯示下一幀。
{
frame_queue_next(&is->pictq);
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
開始渲染。
四、事件處理
在播放過程中,某些行為的完成或者變化,如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
4.1 消息上報初始化
在IJKFFMoviePlayerController的初始化方法中:
- (id)initWithContentURLString:(NSString *)aUrlString
withOptions:(IJKFFOptions *)options
{
......
// init player
_mediaPlayer = ijkmp_ios_create(media_player_msg_loop);
......
}
可以看到在建立播放器時,
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_msg_loop, mp, "ff_msg_loop");
......
}
ijkmp_msg_loop
方法中調用的即是
mp->msg_loop
。
至此已經完成了播放消息發送的準備工作。
4.2 消息上報處理
播放器底層上報事件時,實際上就是将待發送的消息放入消息隊列,另外有一個線程會不斷從隊列中取出消息,上報給外部,其代碼流程大緻如下圖所示:
我們以prepare完成事件為例,看看代碼中事件上報的具體流程。
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);
}
繼續跟蹤代碼,可以發現最後在
inline static int msg_queue_put_private(MessageQueue *q, AVMessage *msg)
方法中,消息對象被放在了消息隊列裡。但是哪裡讀取的隊列裡的消息呢?在4.1節中,我們有提到在建立播放器時,會傳入
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;
}
}
由此可以看出,最後是在該方法中讀取消息,并采用notification通知到APP上層。
本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓