banner
jzman

jzman

Coding、思考、自觉。
github

IjkPlayerシリーズのプレーヤー作成プロセス

今日は IjkPlayer のプレーヤー作成プロセスについて紹介します。本記事は IjkPlayer のソースコードを読む旅の正式なスタートとなり、主な内容は以下の通りです:

  1. so の初期化
  2. Java 層のプレーヤー作成
  3. IjkMediaPlayer 構造体
  4. ネイティブ層のプレーヤー作成
  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);
    // ネイティブメソッドと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はネイティブ関数と 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 },
};

上記のコードの主な作業は以下の通りです:

  • ネイティブメソッドと Java メソッドの対応関係を登録。
  • コーデック、デマルチプレクサ、プロトコルなどを登録。
  • ネイティブ層から呼び出されるonNativeInvokeコールバックを設定し、主にその戻り値に関心を持つ(例えば、ネットワーク切断の再接続など)。

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_setup(new WeakReference<IjkMediaPlayer>(this));
}

初期化時にijkffmpegijksdlijkplayerライブラリがロードされ、mEventHandlerがネイティブからの再生イベント(再生開始、再生完了、再生エラーなど)を処理するために作成されます。具体的には、ネイティブ層が Java 層の関数postEventFromNativeを呼び出して対応するイベントを送信します。C 層のコードを読む前に、IjkMediaPlayer構造体について理解しておきましょう。

IjkMediaPlayer 構造体#

IjkPlayer に対応するIjkMediaPlayer構造体は、その初期化が主にこの構造体の初期化に関わります。その定義は以下の通りです:

struct IjkMediaPlayer {
    /* IjkMediaPlayerが一度作成されるとref_countが1増加 */
    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;
    /* ユーザーがシークバーを操作したかどうかを示すフラグ */
    int seek_req;
    /* シークのミリ秒値 */
    long seek_msec;
};

再生プロセス全体で関わるパラメータは基本的にIjkMediaPlayer構造体のメンバー変数です。以降、ネイティブ層の IjkPlayer の作成はこの構造体のメンバー変数の初期化を行います。

ネイティブ層のプレーヤー作成#

ネイティブ層のプレーヤー作成は主に構造体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_createFFPlayerを作成し、msg_loopを割り当てます。msg_loopのイベントループ関連の呼び出しについては後の文章で紹介します。次に以下の関数を見てみましょう:

  • SDL_VoutAndroid_CreateForAndroidSurface:IjkPlayer の表示コンテキストSDL_Voutを初期化します。
  • ffpipeline_create_from_androidIJKFF_Pipelineを初期化し、デコーダ、音声出力を設定します。
  • ffpipeline_set_voutSDL_VoutIJKFF_Pipeline_Opaqueにバインドします。

呼び出しフローチャート#

IjkPlayer の作成に関する主な関数呼び出しフローは以下の通りです:

Mermaid Loading...

他の詳細は後の文章で紹介します。次の記事では IjkPlayer のメッセージループメカニズムについて紹介します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。