ijkplayer系列(二) —— ijkplayer初始化流程

上一篇文章我們簡單地了解了ijkplayer的使用方法,本文準備分析一下ijkplayer初始化的流程。

在這里我還是建議大家clone下來源碼同步分析,因為ijkplayer文件太多,我開始看的時候是越看越亂,到最后重新看一點,然后用筆記記一點,才有了頭緒。

我們上一篇文章有一個示例代碼,為了方便,我再貼一下:

        // init player
        IjkMediaPlayer.loadLibrariesOnce(null);
        IjkMediaPlayer.native_profileBegin("libijkplayer.so");
        videoView.setVideoURI(Uri.parse("http://106.36.45.36/live.aishang.ctlcdn.com/00000110240001_1/encoder/1/playlist.m3u8"));

        videoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(IMediaPlayer mp) {
                videoView.start();
            }
        });

OK,現在我們就從初始化的第一句分析起,在loadLibrariesOnce(null)中加載了我們編譯出來的幾個so文件,如下:

libLoader.loadLibrary("ijkffmpeg");
libLoader.loadLibrary("ijksdl");
libLoader.loadLibrary("ijkplayer");

這里直接把libLoader看成System.loadLibrary(libname)就行,雖然看起來沒什么,挺正常的,但是分析到后面發現,java層調用的函數在c層找不到0.0。之后google了下發現:

JNI在加載時,會調用JNI_OnLoad(),而卸載時會調用JNI_UnLoad(),我們可以利用這兩個方法來動態方式實現JNI。

然后趕快找找C代碼中有沒有JNI_OnLoad()方法。

果然,在ijkplayer_jni.c中找到了JNI_OnLoad()方法:

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv* env = NULL;

    g_jvm = vm;
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

    pthread_mutex_init(&g_clazz.mutex, NULL );

    // FindClass returns LocalReference
    IJK_FIND_JAVA_CLASS(env, g_clazz.clazz, JNI_CLASS_IJKPLAYER);
    (*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );

    ijkmp_global_init();
    ijkmp_global_set_inject_callback(inject_callback);

    FFmpegApi_global_init(env);

    return JNI_VERSION_1_4;
}

這個方法里面,初始化了很多內容,首先RegisterNatives注冊g_methods中的native方法,其中包括:

static JNINativeMethod g_methods[] = {
    {
        "_setDataSource",
        "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V",
        (void *) IjkMediaPlayer_setDataSourceAndHeaders
    },
    { "_setDataSourceFd",       "(I)V",     (void *) IjkMediaPlayer_setDataSourceFd },
    { "_setDataSource",         "(Ltv/danmaku/ijk/media/player/misc/IMediaDataSource;)V", (void *)IjkMediaPlayer_setDataSourceCallback },
//...
    { "_setVideoSurface",       "(Landroid/view/Surface;)V", (void *) IjkMediaPlayer_setVideoSurface },
    { "_prepareAsync",          "()V",      (void *) IjkMediaPlayer_prepareAsync },
    { "_start",                 "()V",      (void *) IjkMediaPlayer_start },
    { "_stop",                  "()V",      (void *) IjkMediaPlayer_stop },
    { "seekTo",                 "(J)V",     (void *) IjkMediaPlayer_seekTo },
    { "_pause",                 "()V",      (void *) IjkMediaPlayer_pause },
    { "isPlaying",              "()Z",      (void *) IjkMediaPlayer_isPlaying },
    { "getAudioSessionId",      "()I",      (void *) IjkMediaPlayer_getAudioSessionId },
    { "native_init",            "()V",      (void *) IjkMediaPlayer_native_init },
    { "native_setup",           "(Ljava/lang/Object;)V", (void *) IjkMediaPlayer_native_setup },
//......
    { "native_profileBegin",    "(Ljava/lang/String;)V",    (void *) IjkMediaPlayer_native_profileBegin },
    { "native_profileEnd",      "()V",                      (void *) IjkMediaPlayer_native_profileEnd },
};

注冊后,相當于java層聲明為native方法的函數與c層有一個映射關系;舉個栗子,當我們java里面調用_start()方法的時候,其實調用的是c層中的IjkMediaPlayer_start()方法;對應地,作者在java層聲明了一個修飾java方法的annotation,這些函數也會在這里注冊,實現后面在C層也能調用java層的函數。是不是很神奇??

接著還調用了幾個init方法:
**ffp_global_init() **-->主要是ffmpeg的初始化工作, 前面說過ijkplayer是基于ffmpeg的。
FFmpegApi_global_init(env)-->初始化FFmpegApi#av_base64_encode函數。

后面調用的IjkMediaPlayer.native_profileBegin("libijkplayer.so");發現并沒有什么作用的樣子,至少我注釋了還是能成功運行??磥磉@里要留坑了。

然后我們再看到videoView.setVideoURI();,這里的url支持本地視頻和在線視頻,接著我們繼續看到它的實現:

 private void openVideo() {
        if (mUri == null || mSurfaceHolder == null) {
            // not ready for playback just yet, will try again later
            return;
        }
        // we shouldn't clear the target state, because somebody might have
        // called start() previously
        release(false);

        AudioManager am = (AudioManager) mAppContext.getSystemService(Context.AUDIO_SERVICE);
        am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);

        try {
            mMediaPlayer = createPlayer(mSettings.getPlayer());
            mMediaPlayer.setOnPreparedListener(mPreparedListener);
            //這里省略一大波setListener......
            mCurrentBufferPercentage = 0;
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                mMediaPlayer.setDataSource(mAppContext, mUri, mHeaders);
            } else {
                mMediaPlayer.setDataSource(mUri.toString());
            }
            bindSurfaceHolder(mMediaPlayer, mSurfaceHolder);
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mMediaPlayer.setScreenOnWhilePlaying(true);
            mMediaPlayer.prepareAsync();
            if (mHudViewHolder != null)
                mHudViewHolder.setMediaPlayer(mMediaPlayer);

            // REMOVED: mPendingSubtitleTracks

            // we don't set the target state here either, but preserve the
            // target state that was there before.
            mCurrentState = STATE_PREPARING;
            attachMediaController();
        } catch (IOException ex) {
           ...
        } catch (IllegalArgumentException ex) {
          ...
        } finally {
            
        }
    }

在這里, 做了下面幾件事:

  • 獲得AudioManager
  • 通過setting獲取player的類型
  • 初始化IMediaPlayer (createPlayer()函數)
  • 然后調用setDataSource(String)
  • 屏幕常亮等
  • 設置MediaController
  • mMediaPlayer.prepareAsync()

初始化IMediaPlayer

這里只講一下IjkMediaPlayer,其實我們除了使用IjkMediaPlayer,還可以使用IjkExoMediaPlayer或者AndroidMediaPlayer。只需要修改一下createPlayer(int playerType)的參數就行了。

IjkMediaPlayer初始化過程中,做了好多好多事,我們來一一看一下:

 public IjkMediaPlayer(IjkLibLoader libLoader) {
        initPlayer(libLoader);
    }

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

由于之前調用了一次loadLibrariesOnce(),所以這里并沒有什么用。

initNativeOnce()經過幾次跳轉調用了native方法的native_init函數,這里跳轉到C代碼里面,對應調用了IjkMediaPlayer_native_init(),不過看起來并沒有什么操作。接著初始化了一個handler,后面在c層通過調用java的方法來post message。

這里最后調用了native方法native_setup(new WeakReference<IjkMediaPlayer>(this));對應C語言的IjkMediaPlayer_native_setup()方法,也在ijkplayer_jni.c文件里面:

static void
IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    MPTRACE("%s\n", __func__);
    IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
    JNI_CHECK_GOTO(mp, env, "java/lang/OutOfMemoryError", "mpjni: native_setup: ijkmp_create() failed", LABEL_RETURN);

    jni_set_media_player(env, thiz, mp);
    ijkmp_set_weak_thiz(mp, (*env)->NewGlobalRef(env, weak_this));
    ijkmp_set_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
    ijkmp_android_set_mediacodec_select_callback(mp, mediacodec_select_callback, (*env)->NewGlobalRef(env, weak_this));

LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
}

先看ijkmp_android_create(message_loop);/ijkplayer_android.c:

IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
    IjkMediaPlayer *mp = ijkmp_create(msg_loop);
    if (!mp)
        goto fail;

    mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface(); //視頻輸出設備創建
    if (!mp->ffplayer->vout)
        goto fail;

    mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer);    
    if (!mp->ffplayer->pipeline)
        goto fail;

    ffpipeline_set_vout(mp->ffplayer->pipeline, mp->ffplayer- >vout);  

    return mp;

fail:
    ijkmp_dec_ref_p(&mp);
    return NULL;
}

首先創建一個IJKMediaPlayer類型的player吧,先貼出IJKPlayerMediaPlayer的結構:

struct IjkMediaPlayer {
    volatile int ref_count;
    pthread_mutex_t mutex;  //這是一個互斥鎖,因為后面有很多多線程操作,同步少不了
    FFPlayer *ffplayer;    //ffplayer

    int (*msg_loop)(void*);    //msg的一個處理函數
    SDL_Thread *msg_thread;
    SDL_Thread _msg_thread;

    int mp_state;
    char *data_source;    //數據源
    void *weak_thiz;

    int restart;
    int restart_from_beginning;
    int seek_req;
    long seek_msec;
};

create_ijkplayer(ijkmp_create(msg_loop)函數)的同時,也給它指定了msg的處理函數和ffplayer。

接下來就是對ijkplayer結構體中的ffplayer進行設置,剛剛通過SDL_VoutAndroid_CreateForAndroidSurface()創建了輸出設備vout,后面在ffpipeline_set_vout()里面:mp->ffplayer->opaque->weak_vout = vout

然后再jni_set_media_player(),這里基本就結束了。

其實細細分析還有的地方沒有分析到,比如在SDL_VoutAndroid_CreateForAndroidSurface()創建視頻輸出設備的時候,跟蹤函數跳轉發現,硬解用的mediacidec,軟解用的ffmpeg:

static SDL_VoutOverlay *func_create_overlay_l(int width, int height, int frame_format, SDL_Vout *vout)
{
    switch (frame_format) {
    case IJK_AV_PIX_FMT__ANDROID_MEDIACODEC:
        return SDL_VoutAMediaCodec_CreateOverlay(width, height, vout);
    default:
        return SDL_VoutFFmpeg_CreateOverlay(width, height, frame_format, vout);
    }
}

還有渲染視頻設備用的ANativeWindow,對接surface等。還有一些地方是設置了函數指針,現在分析的話,后面可能會懵,這些放到后面再來分析。

OK,現在返回java層,剛剛分析到了IJKMediaPlayer的構造函數,作者在構造函數里面做了很多事情的樣子。

setDataSource(String)

接著我們設置了視頻源,在JNI層對應IjkMediaPlayer_setDataSourceAndHeaders()/ijkplayer_jni.c。同樣代碼跟蹤到了ijkmp_set_data_source_l()

   //...
    freep((void**)&mp->data_source);
    mp->data_source = strdup(url);
   //...
    ijkmp_change_state_l(mp, MP_STATE_INITIALIZED);
   //...

ijkmp_change_state_l里面封裝了一個what為MP_STATE_INITIALIZEDAVMessage,最后把其放入ijkplayer->ffplayer->msg_queue中,然后調用:

int SDL_CondSignal(SDL_cond *cond)
{
    assert(cond);
    if (!cond)
        return -1;

    return pthread_cond_signal(&cond->id);
}

cond->id其實是ijkplayer->ffplayer->msg_queue->cond->id,類型為pthread_cond_t

那么pthread_cond_signal這個函數是干嘛的呢?

其實在liunx里面,pthread_cond_signal函數的作用是發送一個信號給另外一個正在處于阻塞等待狀態的線程,使其脫離阻塞狀態,繼續執行,當某個線程繼續執行的時候發現msg_queue中有msg的時候會有相應操作。到底這里把是發給誰呢?請繼續往下面讀。

prepareAsync() 函數

其中調用了IjkMediaPlayer_prepareAsync/ijkplayer_jni.c,我們繼續看它做了哪些操作。

同樣,這個函數最終調用了ijkmp_prepare_async_l()

static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
    assert(mp);

   //...
    assert(mp->data_source);

    ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);

    msg_queue_start(&mp->ffplayer->msg_queue);

    // released in msg_loop
    ijkmp_inc_ref(mp);
    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;
}

和上面一樣,ijkmp_change_state_l()msg_queue發送一個FFP_MSG_PLAYBACK_STATE_CHANGED狀態

咦?這里又發送了一個狀態FFP_MSG_FLUSH,然而函數名是msg_queue_start(),這是什么意思呢?

然后接下來就創建了一個msg_loop的消息循環線程,線程入口為ijkmp_msg_loop。這里其實就是上面放進msg_queue的message的處理線程。后面再具體分析各個線程做了什么。

在最后,這里繼續調用ffp_prepare_async_l().然而在這個函數里面:

int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)
{
//....

    av_log(NULL, AV_LOG_INFO, "===== versions =====\n");
    ffp_show_version_str(ffp, "FFmpeg",         av_version_info());
    ffp_show_version_int(ffp, "libavutil",      avutil_version());
    ffp_show_version_int(ffp, "libavcodec",     avcodec_version());
    ffp_show_version_int(ffp, "libavformat",    avformat_version());
    ffp_show_version_int(ffp, "libswscale",     swscale_version());
    ffp_show_version_int(ffp, "libswresample",  swresample_version());
    av_log(NULL, AV_LOG_INFO, "===== options =====\n");
    ffp_show_dict(ffp, "player-opts", ffp->player_opts);
    ffp_show_dict(ffp, "format-opts", ffp->format_opts);
    ffp_show_dict(ffp, "codec-opts ", ffp->codec_opts);
    ffp_show_dict(ffp, "sws-opts   ", ffp->sws_dict);
    ffp_show_dict(ffp, "swr-opts   ", ffp->swr_opts);
    av_log(NULL, AV_LOG_INFO, "===================\n");
//...
    VideoState *is = stream_open(ffp, file_name, NULL);
//...

    ffp->is = is;
    ffp->input_filename = av_strdup(file_name);
    return 0;
}

看到這一大波log信息沒有,要是大家運行了上一篇的demo,會發現這不就是打印的log么?

然后繼續跟蹤代碼:

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;
//...
  
    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");
    //...
}

上面函數里面只留下了重要的代碼,這里創建了兩個線程,一個是以video_refresh_thread為入口的視頻顯示線程。還有一個是以read_thread為入口的網絡數據或者本地文件的數據讀取線程。

其實在read_thread里面還向msg_queue發送了一個消息,那就是ffp_notify_msg1(ffp, FFP_MSG_PREPARED);,消息循環線程收到這個消息后,會調用java層的函數,向handler發送個MEDIA_PREPARED消息,然后在handler的handleMessage函數里面會處理這個消息:

    case MEDIA_PREPARED:
                player.notifyOnPrepared();
                return;

最終會調用我們上一文設置的:

videoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(IMediaPlayer mp) {
                videoView.start();
            }
        });

的onPrepared()函數,然后初始化好了,接下來就可以播放了。

** 如果大家還想了解ijkplayer的工作流程的話,可以關注下android下的ijkplayer。**

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

推薦閱讀更多精彩內容

  • 本文記錄的是ijkplayer的初始化流程(重點在分析底層c代碼的邏輯),為了更好的理解這部分內容,建議大家下載i...
    ce0b74704937閱讀 3,096評論 0 1
  • 1 背景 公司的底層播放器實際上是ffplayer作為基礎修改的,當然需要好好學習研究。 記錄下來,作為以后備忘。...
    nothingwxq閱讀 4,379評論 4 5
  • 背景 最近調研做視頻秒開,使用B站開源的ijkplayer作為播放器。ijkplayer基于ffmpeg的播放器。...
    None_Ling閱讀 2,689評論 0 2
  • 本文主要針對B站開源播放器IJKPlayer的部分源碼閱讀筆記,包括Java代碼和C代碼,涉及到部分FFmpeg和...
    駱駝騎士閱讀 922評論 0 0
  • 夜鶯2517閱讀 127,749評論 1 9