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 中消息循环机制。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。