IjkPlayer初始化過程

背景

最近調(diào)研做視頻秒開,使用B站開源的ijkplayer作為播放器厂捞。ijkplayer基于ffmpeg的播放器输玷。

ijkplayer使用

  1. 創(chuàng)建IjkMediaPlayer對象
  2. 通過setDataSource設(shè)置播放路徑
  3. 調(diào)用prepareAsync讓播放器開始工作

JNI_OnLoad

當(dāng)ijkplayer.so被加載時,會回調(diào)到IjkPlayer_jni.c中的JNI_OnLoad中靡馁,最主要調(diào)用ijkmp_global_init初始化

ijkPlayer播放流程

  1. 在IjkMediaPlayer的構(gòu)造函數(shù)中欲鹏,
    • 會調(diào)用loadLibrariesOnce加載libijkffmpegijksdl臭墨,ijkplayer這三個so
    • 調(diào)用native_init打印了一行日志
    • 初始化當(dāng)前線程Looper所使用的Handler
      • 如果在有Looper的子線程初始化的話赔嚎,則會在該子線程進(jìn)行消息循環(huán)
      • 如果沒有Looper的子線程,則使用主線程進(jìn)行消息循環(huán)
    • 初始化一個Native層的IjkPlayer的引用
 private void initPlayer(IjkLibLoader libLoader) {
        loadLibrariesOnce(libLoader);
        initNativeOnce();
        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }
        /*
         * Native setup requires a weak reference to our object. It's easier to
         * create it here than in C++.
         */
        native_setup(new WeakReference<IjkMediaPlayer>(this));
    }
  1. 調(diào)用__setDataSource將視頻URL傳入Native層
  2. 調(diào)用__prepareAsync告知Native層開始加載解碼
  3. Ijkplayer_jni.c是IjkMediaPlayer對應(yīng)的C文件胧弛,其中setup完成以下事情:
    • 初始化Native的IjkMediaPlayer對象尤误,在ijkmp_create函數(shù)中通過ffp_create初始化FFPlayer對象,并且將message_loop賦予該對象
static void IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    MPTRACE("%s\n", __func__);
    // 創(chuàng)建Native層的IjkMediaPlayer對象结缚,并且將message_loop對象賦與mp->msg_loop
    IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
    JNI_CHECK_GOTO(mp, env, "java/lang/OutOfMemoryError", "mpjni: native_setup: ijkmp_create() failed", LABEL_RETURN);
    // 通過JNI將上步創(chuàng)建的IjkMediaPlayer對象的指針地址保存到Java層的mNativeMediaPlayer屬性中
    // 并且釋放舊的IjkMediaPlayer
    jni_set_media_player(env, thiz, mp);
    // 重新生成IjkMediaPlayer的弱引用賦值mp->weak_thiz;
    ijkmp_set_weak_thiz(mp, (*env)->NewGlobalRef(env, weak_this));
    // 設(shè)置ffp的inject_opaque為上述弱引用
    ijkmp_set_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
    ijkmp_set_ijkio_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
    ijkmp_android_set_mediacodec_select_callback(mp, mediacodec_select_callback, ijkmp_get_weak_thiz(mp));
LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
}
  1. setDataSource最終會調(diào)用到ijkmp_set_data_source_l
    • 將原來的mp->data_source指針釋放
    • 重新將url生成char *賦值給mp->data_source
    • 將ijkmediaplayer對象的狀態(tài)修改成MP_STATE_INITIALIZED
static int ijkmp_set_data_source_l(IjkMediaPlayer *mp, const char *url)
{
    ...
    freep((void**)&mp->data_source);
    mp->data_source = strdup(url);
    if (!mp->data_source)
        return EIJK_OUT_OF_MEMORY;
    ijkmp_change_state_l(mp, MP_STATE_INITIALIZED);
    return 0;
}
  1. prepareAsync最終也會調(diào)用到ijkmp_prepare_async_l
    • 將ijkmediaplayer的狀態(tài)修改為MP_STATE_ASYNC_PREPARING
    • 初始化消息隊列&mp->ffplayer->msg_queue
    • 初始化消息處理線程损晤,線程處理function為ijk_msg_loop
    • 調(diào)用ffp_prepare_async_l調(diào)用ffmpeg中的方法開始prepare
static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
    ...
    ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
    // 向&mp->ffplayer->msg_queue中丟入一個FFP_MSG_FLUSH消息
    // 最終目的只是初始化Native的MessageQueue
    msg_queue_start(&mp->ffplayer->msg_queue);

    // released in msg_loop
    ijkmp_inc_ref(mp);
    // 創(chuàng)建處理消息隊列的線程,
    // 向Java層IjkMediaPlayer的postEventFromNative回調(diào)
    // 該回調(diào)會回調(diào)到EventHandler中红竭,而隊列中只有FFP_MSG_FLUSH這個消息尤勋,這只是一個NOP的測試消息
    mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_msg_loop, mp, "ff_msg_loop");
    // msg_thread is detached inside msg_loop
    // TODO: 9 release weak_thiz if pthread_create() failed;

    int retval = ffp_prepare_async_l(mp->ffplayer, mp->data_source);
    if (retval < 0) {
        ijkmp_change_state_l(mp, MP_STATE_ERROR);
        return retval;
    }

    return 0;
}
  1. ffp_prepare_async_l中真正調(diào)用ffmpeg開始準(zhǔn)備播放
    • 判斷url協(xié)議是否為rtmp或者rtsp喘落,如果是則取消timeout參數(shù)
    • 如果url長度大于1024,則加入ijklongurl參數(shù)
    • 調(diào)用stream_open打開視頻流最冰,使用FFPlayer播放
int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)
{
    ...
    if (av_stristart(file_name, "rtmp", NULL) ||
        av_stristart(file_name, "rtsp", NULL)) {
        // There is total different meaning for 'timeout' option in rtmp
        av_log(ffp, AV_LOG_WARNING, "remove 'timeout' option for rtmp.\n");
        av_dict_set(&ffp->format_opts, "timeout", NULL, 0);
    }

    /* there is a length limit in avformat */
    if (strlen(file_name) + 1 > 1024) {
        av_log(ffp, AV_LOG_ERROR, "%s too long url\n", __func__);
        if (avio_find_protocol_name("ijklongurl:")) {
            av_dict_set(&ffp->format_opts, "ijklongurl-url", file_name, 0);
            file_name = "ijklongurl:";
        }
    }
    ...
    av_opt_set_dict(ffp, &ffp->player_opts);
    if (!ffp->aout) {
        ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
        if (!ffp->aout)
            return -1;
    }

#if CONFIG_AVFILTER
    if (ffp->vfilter0) {
        GROW_ARRAY(ffp->vfilters_list, ffp->nb_vfilters);
        ffp->vfilters_list[ffp->nb_vfilters - 1] = ffp->vfilter0;
    }
#endif
    //  打開視頻流
    VideoState *is = stream_open(ffp, file_name, NULL);
    if (!is) {
        av_log(NULL, AV_LOG_WARNING, "ffp_prepare_async_l: stream_open failed OOM");
        return EIJK_OUT_OF_MEMORY;
    }

    ffp->is = is;
    ffp->input_filename = av_strdup(file_name);
    return 0;
}
  1. stream_open開始打開視頻流瘦棋,其中VideoState也就代表著視頻當(dāng)前的狀態(tài),包括幀锌奴,數(shù)據(jù)兽狭,解碼器等等
    • 初始化幀隊列:&is->pictq&is->subpq鹿蜀,&is->sampq箕慧,Queue大小為16
    • 初始化數(shù)據(jù)包隊列:&is->videoq&is->audioq茴恰,&is->subtitleq
    • 初始化時針:&is->vidclk颠焦,&is->audclk&is->extclk往枣,代表當(dāng)前視頻video伐庭,audio的時刻
    • 創(chuàng)建視頻刷新的線程is->video_refresh_tid
    • 創(chuàng)建視頻讀取線程is->read_tid
    • 調(diào)用decoder_init初始化視頻解碼器
static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
    VideoState *is;  //  視頻內(nèi)容以及狀態(tài)
    ...
    /* start video display */
    if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
        goto fail;
    if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 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 ||
        packet_queue_init(&is->subtitleq) < 0)
        goto fail;

    if (!(is->continue_read_thread = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        goto fail;
    }

    if (!(is->video_accurate_seek_cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        ffp->enable_accurate_seek = 0;
    }

    if (!(is->audio_accurate_seek_cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        ffp->enable_accurate_seek = 0;
    }

    init_clock(&is->vidclk, &is->videoq.serial);
    init_clock(&is->audclk, &is->audioq.serial);
    init_clock(&is->extclk, &is->extclk.serial);
    is->audio_clock_serial = -1;
    ...
    is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
    if (!is->video_refresh_tid) {
        av_freep(&ffp->is);
        return NULL;
    }

    is->initialized_decoder = 0;
    is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
    if (!is->read_tid) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
        goto fail;
    }

    if (ffp->async_init_decoder && !ffp->video_disable && ffp->video_mime_type && strlen(ffp->video_mime_type) > 0
                    && ffp->mediacodec_default_name && strlen(ffp->mediacodec_default_name) > 0) {
        if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2) {
            decoder_init(&is->viddec, NULL, &is->videoq, is->continue_read_thread);
            ffp->node_vdec = ffpipeline_init_video_decoder(ffp->pipeline, ffp);
        }
    }
    is->initialized_decoder = 1;

    return is;
fail:
    is->initialized_decoder = 1;
    is->abort_request = true;
    if (is->video_refresh_tid)
        SDL_WaitThread(is->video_refresh_tid, NULL);
    stream_close(ffp);
    return NULL;
}

FFMpeg模塊分布

encode/decode模塊

  • 用于音視頻的編碼和解碼,存放在libavcodec子目錄中

muxer/demuxer模塊

  • 用于音頻和視頻的合并與分離(也稱混合器模塊),存放在libavformat目錄中

內(nèi)存等常用模塊

  • 存放于libavutil目錄中

總結(jié)

  1. IjkPlayer在Java層初始化主線程/當(dāng)前線程的EventHandler用于處理從Native層回調(diào)的消息
  2. 在Native層初始化IjkMediaPlayer對象
    • message_loop函數(shù)指針賦值分冈,以指定Native層的消息
    • 通過ffp_create創(chuàng)建FFPlayer對象
    • 初始化IjkMediaPlayer中的Mutex圾另,以及ref_count自增
    • 創(chuàng)建SDK_Vout用于圖形渲染
    • 根據(jù)平臺特性創(chuàng)建各平臺的IJKFF_Pipeline,PipeLine中包括了視頻解碼雕沉、音頻輸出等功能
  3. 將NativeMediaPlayer的指針地址賦值給Java層的mNativeMediaPlayer
  4. 當(dāng)調(diào)用prepareAsync時集乔,Native層會從Java層獲取之前保存的mNativeMediaPlayer指針地址來獲取Native層的IjkMediaPlayer
  5. 初始化&mp->ffplayer->msg_queue
  6. 啟動消息線程&mp->_msg_thread以及消息循環(huán)函數(shù)ijkmp_msg_loop

參考資料

ijkplayer框架深入剖析
在線短視頻秒播優(yōu)化之視頻文件格式之MP4文件Moov box的位置
FFmpeg學(xué)習(xí)1:視頻解碼
MP4文件格式的解析,以及MP4文件的分割算法
ijkplayer整理筆記(三)——AVFormatContext類圖詳解類圖詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坡椒,一起剝皮案震驚了整個濱河市扰路,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌倔叼,老刑警劉巖汗唱,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異丈攒,居然都是意外死亡哩罪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門巡验,熙熙樓的掌柜王于貴愁眉苦臉地迎上來识椰,“玉大人,你說我怎么就攤上這事深碱「桂模” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵敷硅,是天一觀的道長功咒。 經(jīng)常有香客問我愉阎,道長,這世上最難降的妖魔是什么力奋? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任榜旦,我火速辦了婚禮,結(jié)果婚禮上景殷,老公的妹妹穿的比我還像新娘溅呢。我一直安慰自己,他們只是感情好猿挚,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布咐旧。 她就那樣靜靜地躺著,像睡著了一般绩蜻。 火紅的嫁衣襯著肌膚如雪铣墨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天办绝,我揣著相機(jī)與錄音伊约,去河邊找鬼。 笑死孕蝉,一個胖子當(dāng)著我的面吹牛屡律,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播降淮,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼超埋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了骤肛?” 一聲冷哼從身側(cè)響起纳本,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤窍蓝,失蹤者是張志新(化名)和其女友劉穎腋颠,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吓笙,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡淑玫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了面睛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片絮蒿。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情遮咖,我是刑警寧澤军熏,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站艾恼,受9級特大地震影響桃序,放射性物質(zhì)發(fā)生泄漏裆站。R本人自食惡果不足惜蜡饵,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一弹渔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧溯祸,春花似錦肢专、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氨鹏,卻和暖如春欧募,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仆抵。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工跟继, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人镣丑。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓舔糖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親莺匠。 傳聞我的和親對象是個殘疾皇子金吗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容