網(wǎng)上有很多關(guān)于pos機(jī)框架,ijkplayer框架深入剖析的知識(shí),也有很多人為大家解答關(guān)于pos機(jī)框架的問題,今天pos機(jī)之家(www.afbey.com)為大家整理了關(guān)于這方面的知識(shí),讓我們一起來看下吧!
本文目錄一覽:
pos機(jī)框架
隨著互聯(lián)網(wǎng)技術(shù)的飛速發(fā)展,移動(dòng)端播放視頻的需求如日中天,由此也催生了一批開源/閉源的播放器,但是無(wú)論這個(gè)播放器功能是否強(qiáng)大、兼容性是否優(yōu)秀,它的基本模塊通常都是由以下部分組成:事務(wù)處理、數(shù)據(jù)的接收和解復(fù)用、音視頻解碼以及渲染,其基本框架如下圖所示:
播放器基本框圖
針對(duì)各種鋪天蓋地的播放器項(xiàng)目,我們選取了比較出眾的ijkplayer進(jìn)行源碼剖析。它是一個(gè)基于FFPlay的輕量級(jí)Android/iOS視頻播放器,實(shí)現(xiàn)了跨平臺(tái)的功能,API易于集成;編譯配置可裁剪,方便控制安裝包大小。
本文基于k0.7.6版本的ijkplayer,重點(diǎn)分析其C語(yǔ)言實(shí)現(xiàn)的核心代碼,涉及到不同平臺(tái)下的封裝接口或處理方式時(shí),均以iOS平臺(tái)為例,Android平臺(tái)大同小異,請(qǐng)大家自行查閱研究。
一、總體說明打開ijkplayer,可看到其主要目錄結(jié)構(gòu)如下:tool - 初始化項(xiàng)目工程腳本
config - 編譯ffmpeg使用的配置文件
extra - 存放編譯ijkplayer所需的依賴源文件, 如ffmpeg、openssl等
ijkmedia - 核心代碼
ijkplayer - 播放器數(shù)據(jù)下載及解碼相關(guān)
ijksdl - 音視頻數(shù)據(jù)渲染相關(guān)
ios - iOS平臺(tái)上的上層接口封裝以及平臺(tái)相關(guān)方法
android - android平臺(tái)上的上層接口封裝以及平臺(tái)相關(guān)方法
在功能的具體實(shí)現(xiàn)上,iOS和Android平臺(tái)的差異主要表現(xiàn)在視頻硬件解碼以及音視頻渲染方面,兩者實(shí)現(xiàn)的載體區(qū)別如下表所示:
C++音視頻開發(fā)學(xué)習(xí)資料:點(diǎn)擊領(lǐng)取→音視頻開發(fā)(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
二、初始化流程初始化完成的主要工作就是創(chuàng)建播放器對(duì)象,打開ijkplayer/ios/IJKMediaDemo/IJKMediaDemo.xcodeproj工程,可看到IJKMoviePlayerViewController類中viewDidLoad方法中創(chuàng)建了IJKFFMoviePlayerController對(duì)象,即iOS平臺(tái)上的播放器對(duì)象。
- (void)viewDidLoad{ ...... self.player = [[IJKFFMoviePlayerController alloc] initWithContentURL:self.url withOptions:options]; ......}
查看ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer/IJKFFMoviePlayerController.m文件,其初始化方法具體實(shí)現(xiàn)如下:
- (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;}
可發(fā)現(xiàn)在此創(chuàng)建了IjkMediaPlayer結(jié)構(gòu)體實(shí)例_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;}
在該方法中主要完成了三個(gè)動(dòng)作:
創(chuàng)建IJKMediaPlayer對(duì)象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方法創(chuàng)建了FFPlayer對(duì)象,并設(shè)置消息處理函數(shù)。創(chuàng)建圖像渲染對(duì)象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;}
創(chuàng)建平臺(tái)相關(guān)的IJKFF_Pipeline對(duì)象,包括視頻解碼以及音頻輸出部分
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;}
至此已經(jīng)完成了ijkplayer播放器初始化的相關(guān)流程,簡(jiǎn)單來說,就是創(chuàng)建播放器對(duì)象,完成音視頻解碼、渲染的準(zhǔn)備工作。在下一章節(jié)中,會(huì)重點(diǎn)介紹播放的核心代碼。
C++音視頻開發(fā)學(xué)習(xí)資料:點(diǎn)擊領(lǐng)取→音視頻開發(fā)(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
三、核心代碼剖析ijkplayer實(shí)際上是基于ffplay.c實(shí)現(xiàn)的,本章節(jié)將以該文件為主線,從數(shù)據(jù)接收、音視頻解碼、音視頻渲染及同步這三大方面進(jìn)行講解,要求讀者有基本的ffmpeg知識(shí)。
ffplay.c中主要的代碼調(diào)用流程如下圖所示:
ffplay代碼調(diào)用流程圖
當(dāng)外部調(diào)用prepareToPlay啟動(dòng)播放后,ijkplayer內(nèi)部最終會(huì)調(diào)用到ffplay.c中的
int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)
方法,該方法是啟動(dòng)播放器的入口函數(shù),在此會(huì)設(shè)置player選項(xiàng),打開audio output,最重要的是調(diào)用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主要做了以下幾件事情:
創(chuàng)建存放video/audio解碼前數(shù)據(jù)的videoq/audioq創(chuàng)建存放video/audio解碼后數(shù)據(jù)的pictq/sampq創(chuàng)建讀數(shù)據(jù)線程read_thread創(chuàng)建視頻渲染線程video_refresh_thread說明:subtitle是與video、audio平行的一個(gè)stream,ffplay中也支持對(duì)它的處理,即創(chuàng)建存放解碼前后數(shù)據(jù)的兩個(gè)queue,并且當(dāng)文件中存在subtitle時(shí),還會(huì)啟動(dòng)subtitle的解碼線程,由于篇幅有限,本文暫時(shí)忽略對(duì)它的相關(guān)介紹。
3.1 數(shù)據(jù)讀取數(shù)據(jù)讀取的整個(gè)過程都是由ffmpeg內(nèi)部完成的,接收到網(wǎng)絡(luò)過來的數(shù)據(jù)后,ffmpeg根據(jù)其封裝格式,完成了解復(fù)用的動(dòng)作,我們得到的,是音視頻分離開的解碼前的數(shù)據(jù),步驟如下:
創(chuàng)建上下文結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體是最上層的結(jié)構(gòu)體,表示輸入上下文ic = avformat_alloc_context();
2.設(shè)置中斷函數(shù),如果出錯(cuò)或者退出,就可以立刻退出
ic->interrupt_callback.callback = decode_interrupt_cb;ic->interrupt_callback.opaque = is;
3.打開文件,主要是探測(cè)協(xié)議類型,如果是網(wǎng)絡(luò)文件則創(chuàng)建網(wǎng)絡(luò)鏈接等
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
4.探測(cè)媒體類型,可得到當(dāng)前文件的封裝格式,音視頻編碼參數(shù)等信息
err = avformat_find_stream_info(ic, opts);
5.打開視頻、音頻解碼器。在此會(huì)打開相應(yīng)解碼器,并創(chuàng)建相應(yīng)的解碼線程。
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
6.讀取媒體數(shù)據(jù),得到的是音視頻分離的解碼前數(shù)據(jù)
ret = av_read_frame(ic, pkt);
7.將音視頻數(shù)據(jù)分別送入相應(yīng)的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);}
重復(fù)6、7步,即可不斷獲取待播放的數(shù)據(jù)。
3.2 音視頻解碼ijkplayer在視頻解碼上支持軟解和硬解兩種方式,可在起播前配置優(yōu)先使用的解碼方式,播放過程中不可切換。iOS平臺(tái)上硬解使用VideoToolbox,Android平臺(tái)上使用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; ......}
首先會(huì)打開ffmpeg的解碼器,然后通過ffpipeline_open_video_decoder創(chuàng)建IJKFF_Pipenode。
第二章節(jié)中有介紹,在創(chuàng)建IJKMediaPlayer對(duì)象時(shí),通過ffpipeline_create_from_ios創(chuàng)建了pipeline,則
IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp){ return pipeline->func_open_video_decoder(pipeline, ffp);}
func_open_video_decoder函數(shù)指針最后指向的是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!!!! \"); } 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,會(huì)優(yōu)先去嘗試打開硬件解碼器,
node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
如果硬件解碼器打開失敗,則會(huì)自動(dòng)切換至軟解
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。
不管視頻解碼還是音頻解碼,其基本流程都是從解碼前的數(shù)據(jù)緩沖區(qū)中取出一幀數(shù)據(jù)進(jìn)行解碼,完成后放入相應(yīng)的解碼后的數(shù)據(jù)緩沖區(qū),如下圖所示:
音視頻解碼示意圖.
本文以video的軟解流程為例進(jìn)行分析,audio的流程可對(duì)照研究。
視頻解碼線程
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中調(diào)用的是IJKFF_Pipenode對(duì)象中的func_run_sync
int ffpipenode_run_sync(IJKFF_Pipenode *node){ return node->func_run_sync(node);}
func_run_sync取決于播放前配置的軟硬解,假設(shè)為軟解,則調(diào)用
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中調(diào)用了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中取出一幀數(shù)據(jù),送入decoder進(jìn)行解碼,解碼后的數(shù)據(jù)在ffplay_video_thread中送入pictq。
3.3 音視頻渲染及同步3.3.1 音頻輸出ijkplayer中Android平臺(tái)使用OpenSL ES或AudioTrack輸出音頻,iOS平臺(tái)使用AudioQueue輸出音頻。
audio output節(jié)點(diǎn),在ffp_prepare_async_l方法中被創(chuàng)建:
ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
ffpipeline_open_audio_output方法實(shí)際上調(diào)用的是IJKFF_Pipeline對(duì)象的函數(shù)指針func_open_audio_utput,該函數(shù)指針在初始化中的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定義如下,主要完成的是創(chuàng)建SDL_Aout對(duì)象
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中,如果發(fā)現(xiàn)待播放的文件中含有音頻,那么在調(diào)用stream_component_open打開解碼器時(shí),該方法里面也調(diào)用audio_open打開了audio output設(shè)備。
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中配置了音頻輸出的相關(guān)參數(shù)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;}
設(shè)置給了Audio Output, iOS平臺(tái)上即為AudioQueue。
AudioQueue模塊在工作過程中,通過不斷的callback來獲取pcm數(shù)據(jù)進(jìn)行播放。
有關(guān)AudioQueue的具體內(nèi)容此處不再介紹。
3.3.2 視頻渲染iOS平臺(tái)上采用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); ......}
從代碼實(shí)現(xiàn)上可以看出,該線程的主要工作為:
調(diào)用frame_queue_peek_last從pictq中讀取當(dāng)前需要顯示視頻幀調(diào)用SDL_VoutDisplayYUVOverlay進(jìn)行繪制int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay){ if (vout && overlay && vout->display_overlay) return vout->display_overlay(vout, overlay); return -1;}
display_overlay函數(shù)指針在前面初始化流程有介紹過,它在
SDL_Vout *SDL_VoutIos_CreateForGLES2()方法中被賦值為vout_display_overlay,該方法就是調(diào)用OpengGL繪制圖像。3.4.3 音視頻同步
對(duì)于播放器來說,音視頻同步是一個(gè)關(guān)鍵點(diǎn),同時(shí)也是一個(gè)難點(diǎn),同步效果的好壞,直接決定著播放器的質(zhì)量。通常音視頻同步的解決方案就是選擇一個(gè)參考時(shí)鐘,播放時(shí)讀取音視頻幀上的時(shí)間戳,同時(shí)參考當(dāng)前時(shí)鐘參考時(shí)鐘上的時(shí)間來安排播放。如下圖所示:
音視頻同步示意圖.
如果音視頻幀的播放時(shí)間大于當(dāng)前參考時(shí)鐘上的時(shí)間,則不急于播放該幀,直到參考時(shí)鐘達(dá)到該幀的時(shí)間戳;如果音視頻幀的時(shí)間戳小于當(dāng)前參考時(shí)鐘上的時(shí)間,則需要“盡快”播放該幀或丟棄,以便播放進(jìn)度追上參考時(shí)鐘。
參考時(shí)鐘的選擇也有多種方式:
選取視頻時(shí)間戳作為參考時(shí)鐘源選取音頻時(shí)間戳作為參考時(shí)鐘源選取外部時(shí)間作為參考時(shí)鐘源考慮人對(duì)視頻、和音頻的敏感度,在存在音頻的情況下,優(yōu)先選擇音頻作為主時(shí)鐘源。
ijkplayer在默認(rèn)情況下也是使用音頻作為參考時(shí)鐘源,處理同步的過程主要在視頻渲染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;}
從上述實(shí)現(xiàn)可以看出,該方法中主要循環(huán)做兩件事情:
休眠等待,remaining_time的計(jì)算在video_refresh中調(diào)用video_refresh方法,刷新視頻幀可見同步的重點(diǎn)是在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是當(dāng)前幀,last_duration則是根據(jù)當(dāng)前幀和上一幀的pts,計(jì)算出來上一幀的顯示時(shí)間,經(jīng)過compute_target_delay方法,計(jì)算出顯示當(dāng)前幀需要等待的時(shí)間。
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方法中,如果發(fā)現(xiàn)當(dāng)前主時(shí)鐘源不是video,則計(jì)算當(dāng)前視頻時(shí)鐘與主時(shí)鐘的差值:
如果當(dāng)前視頻幀落后于主時(shí)鐘源,則需要減小下一幀畫面的等待時(shí)間;如果視頻幀超前,并且該幀的顯示時(shí)間大于顯示更新門檻,則顯示下一幀的時(shí)間為超前的時(shí)間差加上上一幀的顯示時(shí)間如果視頻幀超前,并且上一幀的顯示時(shí)間小于顯示更新門檻,則采取加倍延時(shí)的策略。回到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實(shí)際上就是上一幀的播放時(shí)間,而frame_timer + delay實(shí)際上就是當(dāng)前這一幀的播放時(shí)間,如果系統(tǒng)時(shí)間還沒有到當(dāng)前這一幀的播放時(shí)間,直接跳轉(zhuǎn)至display,而此時(shí)is->force_refresh變量為0,不顯示當(dāng)前幀,進(jìn)入video_refresh_thread中下一次循環(huán),并睡眠等待。
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; } }
如果當(dāng)前這一幀的播放時(shí)間已經(jīng)過了,并且其和當(dāng)前系統(tǒng)時(shí)間的差值超過了AV_SYNC_THRESHOLD_MAX,則將當(dāng)前這一幀的播放時(shí)間改為系統(tǒng)時(shí)間,并在后續(xù)判斷是否需要丟幀,其目的是為后面幀的播放時(shí)間重新調(diào)整frame_timer,如果緩沖區(qū)中有更多的數(shù)據(jù),并且當(dāng)前的時(shí)間已經(jīng)大于當(dāng)前幀的持續(xù)顯示時(shí)間,則丟棄當(dāng)前幀,嘗試顯示下一幀。
{ 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);
否則進(jìn)入正常顯示當(dāng)前幀的流程,調(diào)用video_display2開始渲染。
C++音視頻開發(fā)學(xué)習(xí)資料:點(diǎn)擊領(lǐng)取→音視頻開發(fā)(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
四、事件處理在播放過程中,某些行為的完成或者變化,如prepare完成,開始渲染等,需要以事件形式通知到外部,以便上層作出具體的業(yè)務(wù)處理。
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="360px",height="auto" />
4.1 消息上報(bào)初始化在IJKFFMoviePlayerController的初始化方法中:
- (id)initWithContentURLString:(NSString *)aUrlString withOptions:(IJKFFOptions *)options{ ...... // init player _mediaPlayer = ijkmp_ios_create(media_player_msg_loop); ...... }
可以看到在創(chuàng)建播放器時(shí),media_player_msg_loop函數(shù)地址作為參數(shù)傳入了ijkmp_ios_create,繼續(xù)跟蹤代碼,可以發(fā)現(xiàn),該函數(shù)地址最終被賦值給了IjkMediaPlayer中的msg_loop函數(shù)指針
IjkMediaPlayer *ijkmp_create(int (*msg_loop)(void*)){ ...... mp->msg_loop = msg_loop; ......}
開始播放時(shí),會(huì)啟動(dòng)一個(gè)消息線程,
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方法中調(diào)用的即是mp->msg_loop。
至此已經(jīng)完成了播放消息發(fā)送的準(zhǔn)備工作。
4.2 消息上報(bào)處理播放器底層上報(bào)事件時(shí),實(shí)際上就是將待發(fā)送的消息放入消息隊(duì)列,另外有一個(gè)線程會(huì)不斷從隊(duì)列中取出消息,上報(bào)給外部,其代碼流程大致如下圖所示:
消息上報(bào)代碼調(diào)用流程圖.
我們以prepare完成事件為例,看看代碼中事件上報(bào)的具體流程。
ffplay.c中上報(bào)PREPARED完成時(shí)調(diào)用:
ffp_notify_msg1(ffp, FFP_MSG_PREPARED);
ffp_notify_msg1方法實(shí)現(xiàn)如下:
inline static void ffp_notify_msg1(FFPlayer *ffp, int what) { msg_queue_put_simple3(&ffp->msg_queue, what, 0, 0);}
msg_queue_put_simple3中將事件及其參數(shù)封裝成了AVMessge對(duì)象,
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);}
繼續(xù)跟蹤代碼,可以發(fā)現(xiàn)最后在
inline static int msg_queue_put_private(MessageQueue *q, AVMessage *msg)
方法中,消息對(duì)象被放在了消息隊(duì)列里。但是哪里讀取的隊(duì)列里的消息呢?在4.1節(jié)中,我們有提到在創(chuàng)建播放器時(shí),會(huì)傳入media_player_msg_loop函數(shù)地址,最后作為一個(gè)單獨(dú)的線程運(yùn)行,現(xiàn)在來看一下media_player_msg_loop方法的實(shí)現(xiàn):
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上層。
五、結(jié)束語(yǔ)本文只是粗略的分析了ijkplayer的關(guān)鍵代碼部分,平臺(tái)相關(guān)的解碼、渲染以及用戶事務(wù)處理部分,都沒有具體分析到,大家可以參考代碼自行分析
以上就是關(guān)于pos機(jī)框架,ijkplayer框架深入剖析的知識(shí),后面我們會(huì)繼續(xù)為大家整理關(guān)于pos機(jī)框架的知識(shí),希望能夠幫助到大家!
