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阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容

  • 本文记录的是ijkplayer的初始化流程(重点在分析底层c代码的逻辑),为了更好的理解这部分内容,建议大家下载i...
    ce0b74704937阅读 2,981评论 0 1
  • 1 背景 公司的底层播放器实际上是ffplayer作为基础修改的,当然需要好好学习研究。 记录下来,作为以后备忘。...
    nothingwxq阅读 4,272评论 4 5
  • 背景 最近调研做视频秒开,使用B站开源的ijkplayer作为播放器。ijkplayer基于ffmpeg的播放器。...
    None_Ling阅读 2,589评论 0 2
  • 本文主要针对B站开源播放器IJKPlayer的部分源码阅读笔记,包括Java代码和C代码,涉及到部分FFmpeg和...
    骆驼骑士阅读 848评论 0 0
  • 夜莺2517阅读 127,659评论 1 9