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 结构体,其初始化主要是对该结构体进行初始化,其定义如下:

整个播放流程中涉及到的参数基本都是 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 中消息循环机制。

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.