banner
jzman

jzman

Coding、思考、自觉。
github

IjkPlayer系列之播放器創建流程

今天介紹下 IjkPlayer 的播放器創建流程,本文開始將正式開始 IjkPlayer 的源碼閱讀之旅,主要內容如下:

  1. 初始化 so
  2. Java 層播放器創建
  3. IjkMediaPlayer 結構體
  4. Native 層播放器創建
  5. 調用流程圖

閱讀之前可以先看前面幾篇文章:

初始化 so#

從 IjkPlayer 源碼中VideoActivity查看 IjkPlayer 的初始化,關鍵代碼如下:

// 加載so庫
IjkMediaPlayer.loadLibrariesOnce(null);
// android-ndk-profiler性能分析
IjkMediaPlayer.native_profileBegin("libijkplayer.so");

查看loadLibrariesOnce源碼如下:

private static volatile boolean mIsLibLoaded = false;
public static void loadLibrariesOnce(IjkLibLoader libLoader) {
    synchronized (IjkMediaPlayer.class) {
        // 保證只加載一次
        if (!mIsLibLoaded) {
            if (libLoader == null)
                // sLocalLibLoader為默認的IjkLibLoader
                libLoader = sLocalLibLoader;
            libLoader.loadLibrary("ijkffmpeg");
            libLoader.loadLibrary("ijksdl");
            libLoader.loadLibrary("ijkplayer");
            mIsLibLoaded = true;
        }
    }
}

loadLibrariesOnce 加載相應的 so 庫,native_profileBegin 則是 android-ndk-profiler 工具進行性能分析的函數,對應的還有native_profileEnd,其定義如下:

public static native void native_profileBegin(String libName);
public static native void native_profileEnd();

上述方法分別調用了函數 monstartupmoncleanup,分別在程序運行開始和結束調用,關於 android-ndk-profiler 的這裡不做過多介紹。

IjkPlayer 系列之 JNI 基礎及源碼目錄介紹 這篇文章中介紹了 JNI 的註冊方式,其中 IjkPlayer 使用的就是其動態註冊方式,即當調用 System.loadLibrary 加載庫的時候會去查找 JNI_OnLoad 這個函數,然後在該函數回調中進行註冊,下面看下 ijkplayer 中的 JNI_OnLoad 實現,如下:

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv* env = NULL;
    // 保存全局的JavaVM
    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 );
    // 將IjkMediaPlayer轉換為一個全局引用
    IJK_FIND_JAVA_CLASS(env, g_clazz.clazz, JNI_CLASS_IJKPLAYER);
    // 註冊Native方法與Java方法相對應
    (*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );
    // 初始化:註冊編解碼器、解復用器、協議
    ijkmp_global_init();
    // 設置回調,對應Java層的onNativeInvoke回調函數
    ijkmp_global_set_inject_callback(inject_callback);
    // 註冊av_base64_encode與FFmpegApi_av_base64_encode的對應關係
    FFmpegApi_global_init(env);
    return JNI_VERSION_1_4;
}

其中數組 g_methods 定義了 Native 函數與 Java 方法的一一對應關係,如下:

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 },
    { "_setAndroidIOCallback",  "(Ltv/danmaku/ijk/media/player/misc/IAndroidIO;)V", (void *)IjkMediaPlayer_setAndroidIOCallback },

    { "_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 },
    { "getCurrentPosition",     "()J",      (void *) IjkMediaPlayer_getCurrentPosition },
    { "getDuration",            "()J",      (void *) IjkMediaPlayer_getDuration },
    { "_release",               "()V",      (void *) IjkMediaPlayer_release },
    { "_reset",                 "()V",      (void *) IjkMediaPlayer_reset },
    { "setVolume",              "(FF)V",    (void *) IjkMediaPlayer_setVolume },
    { "getAudioSessionId",      "()I",      (void *) IjkMediaPlayer_getAudioSessionId },
    { "native_init",            "()V",      (void *) IjkMediaPlayer_native_init },
    { "native_setup",           "(Ljava/lang/Object;)V", (void *) IjkMediaPlayer_native_setup },
    { "native_finalize",        "()V",      (void *) IjkMediaPlayer_native_finalize },

    { "_setOption",             "(ILjava/lang/String;Ljava/lang/String;)V", (void *) IjkMediaPlayer_setOption },
    { "_setOption",             "(ILjava/lang/String;J)V",                  (void *) IjkMediaPlayer_setOptionLong },

    { "_getColorFormatName",    "(I)Ljava/lang/String;",    (void *) IjkMediaPlayer_getColorFormatName },
    { "_getVideoCodecInfo",     "()Ljava/lang/String;",     (void *) IjkMediaPlayer_getVideoCodecInfo },
    { "_getAudioCodecInfo",     "()Ljava/lang/String;",     (void *) IjkMediaPlayer_getAudioCodecInfo },
    { "_getMediaMeta",          "()Landroid/os/Bundle;",    (void *) IjkMediaPlayer_getMediaMeta },
    { "_setLoopCount",          "(I)V",                     (void *) IjkMediaPlayer_setLoopCount },
    { "_getLoopCount",          "()I",                      (void *) IjkMediaPlayer_getLoopCount },
    { "_getPropertyFloat",      "(IF)F",                    (void *) ijkMediaPlayer_getPropertyFloat },
    { "_setPropertyFloat",      "(IF)V",                    (void *) ijkMediaPlayer_setPropertyFloat },
    { "_getPropertyLong",       "(IJ)J",                    (void *) ijkMediaPlayer_getPropertyLong },
    { "_setPropertyLong",       "(IJ)V",                    (void *) ijkMediaPlayer_setPropertyLong },
    { "_setStreamSelected",     "(IZ)V",                    (void *) ijkMediaPlayer_setStreamSelected },

    { "native_profileBegin",    "(Ljava/lang/String;)V",    (void *) IjkMediaPlayer_native_profileBegin },
    { "native_profileEnd",      "()V",                      (void *) IjkMediaPlayer_native_profileEnd },

    { "native_setLogLevel",     "(I)V",                     (void *) IjkMediaPlayer_native_setLogLevel },
    { "_setFrameAtTime",        "(Ljava/lang/String;JJII)V", (void *) IjkMediaPlayer_setFrameAtTime },
};

上述代碼主要工作如下:

  • 註冊 Native 方法與 Java 方法之間的對應關係。
  • 註冊編解碼器、解復用器、協議等。
  • 設置 onNativeInvoke 回調供 Native 層調用,主要關心其返回值,比如斷網重連等。

Java 層播放器創建#

看下 Java 層 IjkMediaPlayer 的創建

IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer();

查看構造方法如下:

public IjkMediaPlayer() {
    // sLocalLibLoader為默認的IjkLibLoader
    this(sLocalLibLoader);
}

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

private void initPlayer(IjkLibLoader libLoader) {
    // 嘗試加載庫,避免之前從未加載過so
    loadLibrariesOnce(libLoader);
    // 對應c層IjkMediaPlayer_native_init方法,暫時為空實現
    initNativeOnce();
    
    // 用來處理Iik底層傳遞上來的消息
    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;
    }

    // IjkMediaPlayer包裝成弱引用傳遞到native層
    native_setup(new WeakReference<IjkMediaPlayer>(this));
}

可見初始化的時候加載了 ijkffmpeg、ijksdl、ijkplayer 庫,創建了 mEventHandler 處理 Native 拋上來的播放事件,比如播放開始、播放完成、播放出錯等事件,具體由 Native 層調用 Java 層的函數 postEventFromNative 來發送對應的事件,在閱讀 C 層代碼之前,先了解一下 IjkMediaPlayer 結構體。

IjkMediaPlayer 結構體#

IjkPlayer 對應 IjkMediaPlayer 結構體,其初始化主要是對該結構體進行初始化,其定義如下:

struct IjkMediaPlayer {
    /* IjkMediaPlayer創建一次則ref_count計數加一次 */
    volatile int ref_count;
    /* 保護接口調用的鎖*/
    pthread_mutex_t mutex;
    /* FFPlayer是原ffplayer裡的結構體,有被ijk擴展 */
    FFPlayer *ffplayer;
    /* 用於ijkPlayer回調給應用層的一個消息循環函數 */
    int (*msg_loop)(void*);
    /* 消息線程 */
    SDL_Thread *msg_thread;
    SDL_Thread _msg_thread;
    /* 播放器狀態 */
    int mp_state;
    /* 播放地址 */
    char *data_source;
    /* Java層IjkMediaPlayer對應弱引用對象 */
    void *weak_thiz;
    /* 是否重新播放 */
    int restart;
    /* restart是否從頭開始 */
    int restart_from_beginning;
    /* 標識用户是不是seek了進度條 */
    int seek_req;
    /* seek的毫秒值 */
    long seek_msec;
};

整個播放流程中涉及到的參數基本都是 IjkMediaPlayer 結構體中的成員變量,後面 Native 層 IjkPlayer 的創建都是在初始化上述結構體中的成員變量。

Native 層播放器創建#

Native 層播放器的創建主要是結構體 IjkMediaPlayer 的創建及初始化,這裡接著上文繼續查看 native_setup 方法的具體實現如下:

static void
IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    MPTRACE("%s\n", __func__);
    // 創建C層對應的IjkMediaPlayer
    IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
    JNI_CHECK_GOTO(mp, env, "java/lang/OutOfMemoryError", "mpjni: native_setup: ijkmp_create() failed", LABEL_RETURN);
    // Java層mNativeMediaPlayer初始化
    jni_set_media_player(env, thiz, mp);
    // 初始化mp->weak_thiz
    ijkmp_set_weak_thiz(mp, (*env)->NewGlobalRef(env, weak_this));
    // 初始化ffp->inject_opaque等
    ijkmp_set_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
    // 初始化ffp->ijkio_inject_opaque等
    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);
}

上述代碼主要是開始創建 C 層的 IjkMediaPlayer 、其對應結構體部分屬性的初始化及解碼器回調,mediacodec_select_callback 會通過調用 Java 層的函數 onSelectCodec 獲取合適的解碼器信息,可在 IjkMediaCodecInfo 中進行解碼器的適配。

繼續查看 ijkmp_android_create 函數如下:

IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
    // 填充IjkMediaPlayer結構體
    IjkMediaPlayer *mp = ijkmp_create(msg_loop);
    if (!mp)
        goto fail;
    // 初始化SDL_Vout,表示IJK中的顯示上下文
    mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();
    if (!mp->ffplayer->vout)
        goto fail;
    // 初始化IJKFF_Pipeline,解碼器、音頻輸出
    mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer);
    if (!mp->ffplayer->pipeline)
        goto fail;
    // 綁定SDL_Vout到IJKFF_Pipeline_Opaque
    ffpipeline_set_vout(mp->ffplayer->pipeline, mp->ffplayer->vout);
    return mp;
fail:
    ijkmp_dec_ref_p(&mp);
    return NULL;
}

其中 ijkmp_create 主要是創建了 FFPlayer,並將 msg_loop 進行賦值,msg_loop 事件循環相關調用將在後續文章中介紹,繼續看下後面幾個函數:

  • SDL_VoutAndroid_CreateForAndroidSurface:初始化 IjkPlayer 的 顯示上下文 SDL_Vout
  • ffpipeline_create_from_android: 初始化IJKFF_Pipeline,解碼器、音頻輸出。
  • ffpipeline_set_vout:綁定SDL_VoutIJKFF_Pipeline_Opaque

調用流程圖#

IjkPlayer 創建主要函數調用流程如下:

Mermaid Loading...

其他細節會在後續的文章中介紹,下一篇將介紹 IjkPlayer 中消息循環機制。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。