ijkplayer框架深入剖析

隨著互聯網技術的飛速發展,移動端播放視頻的需求如日中天,由此也催生了一批開源/閉源的播放器,但是無論這個播放器功能是否強大、兼容性是否優秀,它的基本模塊通常都是由以下部分組成:事務處理、數據的接收和解復用、音視頻解碼以及渲染,其基本框架如下圖所示:


播放器基本框圖.png

針對各種鋪天蓋地的播放器項目,我們選取了比較出眾的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平臺的差異主要表現在視頻硬件解碼以及音視頻渲染方面,兩者實現的載體區別如下表所示:

Platform Hardware Codec Video Render Audio Output
iOS VideoToolBox OpenGL ES AudioQueue
Android MediaCodec OpenGL ES、MediaCodec OpenSL ES、AudioTrack

二、初始化流程

初始化完成的主要工作就是創建播放器對象,打開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;
}

在該方法中主要完成了三個動作:

  1. 創建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播放器初始化的相關流程,簡單來說,就是創建播放器對象,完成音視頻解碼、渲染的準備工作。在下一章節中,會重點介紹播放的核心代碼。

三、核心代碼剖析

ijkplayer實際上是基于ffplay.c實現的,本章節將以該文件為主線,從數據接收、音視頻解碼、音視頻渲染及同步這三大方面進行講解,要求讀者有基本的ffmpeg知識。

ffplay.c中主要的代碼調用流程如下圖所示:

ffplay代碼調用流程圖.png

當外部調用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根據其封裝格式,完成了解復用的動作,我們得到的,是音視頻分離開的解碼前的數據,步驟如下:

  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->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

不管視頻解碼還是音頻解碼,其基本流程都是從解碼前的數據緩沖區中取出一幀數據進行解碼,完成后放入相應的解碼后的數據緩沖區,如下圖所示:

音視頻解碼示意圖.png

本文以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);
    ......
}

從代碼實現上可以看出,該線程的主要工作為:

  1. 調用frame_queue_peek_last從pictq中讀取當前需要顯示視頻幀

  2. 調用SDL_VoutDisplayYUVOverlay進行繪制

    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函數指針在前面初始化流程有介紹過,它在

    SDL_Vout *SDL_VoutIos_CreateForGLES2()
    

    方法中被賦值為vout_display_overlay,該方法就是調用OpengGL繪制圖像。

3.4.3 音視頻同步

對于播放器來說,音視頻同步是一個關鍵點,同時也是一個難點,同步效果的好壞,直接決定著播放器的質量。通常音視頻同步的解決方案就是選擇一個參考時鐘,播放時讀取音視頻幀上的時間戳,同時參考當前時鐘參考時鐘上的時間來安排播放。如下圖所示:

音視頻同步示意圖.png

如果音視頻幀的播放時間大于當前參考時鐘上的時間,則不急于播放該幀,直到參考時鐘達到該幀的時間戳;如果音視頻幀的時間戳小于當前參考時鐘上的時間,則需要“盡快”播放該幀或丟棄,以便播放進度追上參考時鐘。

參考時鐘的選擇也有多種方式:

  • 選取視頻時間戳作為參考時鐘源
  • 選取音頻時間戳作為參考時鐘源
  • 選取外部時間作為參考時鐘源

考慮人對視頻、和音頻的敏感度,在存在音頻的情況下,優先選擇音頻作為主時鐘源。

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;
}

從上述實現可以看出,該方法中主要循環做兩件事情:

  1. 休眠等待,remaining_time的計算在video_refresh
  2. 調用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 消息上報處理

播放器底層上報事件時,實際上就是將待發送的消息放入消息隊列,另外有一個線程會不斷從隊列中取出消息,上報給外部,其代碼流程大致如下圖所示:

消息上報代碼調用流程圖.png

我們以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上層。

五、結束語

本文只是粗略的分析了ijkplayer的關鍵代碼部分,平臺相關的解碼、渲染以及用戶事務處理部分,都沒有具體分析到,大家可以參考代碼自行分析,也歡迎加QQ群討論。

轉載請注明:
作者金山視頻云,首發簡書 Jianshu.com


后續我們還會陸續推出其他有關音視頻方面的文章,請大家關注。

也歡迎大家使用我們的多媒體SDK:

https://github.com/ksvc

金山云多媒體SDK相關的QQ交流群:

  • 視頻云技術交流群:574179720
  • 視頻云iOS技術交流:621137661
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,732評論 0 3
  • 這是一個跨平臺的播放器ijkplayer,iOS上集成看【如何快速的開發一個完整的iOS直播app】(原理篇)。 ...
    FindCrt閱讀 7,097評論 2 46
  • 2015年可謂是中國企業的跌宕起伏之年,互聯網概念此起彼伏,門口的野蠻人前赴后繼,所有人都在互聯網思維的泥潭中掙扎...
    慕容隨風閱讀 495評論 0 2