ijkplayer初始化流程

本文记录的是ijkplayer的初始化流程(重点在分析底层c代码的逻辑),为了更好的理解这部分内容,建议大家下载ijk的源码,最好结合ijkplayer android端调试配置好环境,有利于查看底层c代码

直接切入主题,因为要看初始化流程,直接看到VideoActivity.java/onCreate(),代码如下

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // init player
        IjkMediaPlayer.loadLibrariesOnce(null);
        ...
        // prefer mVideoPath
        if (mVideoPath != null)
            mVideoView.setVideoPath(mVideoPath);
        else if (mVideoUri != null)
            mVideoView.setVideoURI(mVideoUri);
        else {
            ...
        }
        ...
    }

从上面代码看到,VideoActivity.java/onCreate()函数主要做了两件事:
1. IjkMediaPlayer.loadLibrariesOnce(null),加载库
2. setVideoPath或者setVideoURI设置播放路径

我们一个一个来仔细看这两步分别做了什么事
1. IjkMediaPlayer.loadLibrariesOnce()
代码如下

    public static void loadLibrariesOnce(IjkLibLoader libLoader) {
        synchronized (IjkMediaPlayer.class) {
            if (!mIsLibLoaded) {
              if (libLoader == null)
                    libLoader = sLocalLibLoader;               

                // libLoader.loadLibrary("ijkffmpeg");
                libLoader.loadLibrary("ijksdl");
                libLoader.loadLibrary("ijkplayer");
                ...
            }
        }
    }

可以看到该函数里面调用了loadLibrary函数加载底层库(我这里为什么注销libLoader.loadLibrary("ijkffmpeg")这一行与调试有关,具体查看ijkplayer android端调试),加载好库后,java是怎么能够调用c代码的呢?

原来还有一个jni层的概念,在调用System.loadLibrary()函数时,会调用JNI_OnLoad,而IjkMediaPlayer.loadLibrariesOnce传入的是null,逻辑走libLoader=sLoadLibLoader,所以libLoader.loadLibrary最后就是调用的System.loadLibrary,此时就会调用ijkplayer_jni.c/JNI_OnLoad()函数

我们继续看看ijkplayer_jni.c/JNI_OnLoad()干了些什么事,代码如下

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    ...
    (*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );

    ijkmp_global_init();
    ...
}

ijkplayer_jni.c/JNI_OnLoad()主要做了两件事:
1) (*env)->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 },
    { "_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 },
    { "_injectCacheNode",       "(IJJJJ)V",                 (void *) IjkMediaPlayer_injectCacheNode },
};

这个类似数组的结构相当于是定义java层与c层之间的映射关系,比如在java层调用_prepareAsync函数,其实最终调用的是c层的IjkMediaPlayer_prepareAsync函数
2) ijkplayer_jni.c/ijkmp_global_init()->ijkplayer.c/ffp_global_init()->ff_ffplayer.c/ffp_global_init(),可见最终调用的是ff_ffplayer.c/ffp_global_init(),函数实现如下

void ffp_global_init()
{
    ...
    /* register all codecs, demux and protocols */
    avcodec_register_all();  //注册所有编译好的编解码器
    ...
    av_register_all();  //注册所有编译好的封装器
    ...
    avformat_network_init();  //对一些网络功能进行初始化
    ...
}

其中ff_ffplayer.c/ffp_global_init()又主要做了三件事

  • avcodec_register_all(),注册所有编译好的编解码器
  • av_register_all(),注册所有编译好的封装器
  • avformat_network_init(),对一些网络功能进行初始化

2. setVideoPath或者setVideoURI
其实二者都是调用的IjkVideoView.java/setVideoURI()最后调用IjkVideoView.java/openVideo(),IjkVideoView.java/openVideo()代码如下

        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());
            ...
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                    mSettings.getUsingMediaDataSource() &&
                    (TextUtils.isEmpty(scheme) || scheme.equalsIgnoreCase("file"))) {
                IMediaDataSource dataSource = new FileMediaDataSource(new File(mUri.toString()));
                mMediaPlayer.setDataSource(dataSource);
            }  else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                mMediaPlayer.setDataSource(mAppContext, mUri, mHeaders);
            } else {
                mMediaPlayer.setDataSource(mUri.toString());
            }
            ...
            mMediaPlayer.prepareAsync();
            ...
        } catch (IOException ex) {
            ...
        } catch (IllegalArgumentException ex) {
            ...
        } finally {
            // REMOVED: mPendingSubtitleTracks.clear();
        }
    }

IjkVideoView.java/openVideo()又主要做了三件事:
1) createPlayer,这里默认是创建IjkMediaPlayer(当然这里你也可以选择创建其他的播放器),最终调用IjkMediaPlayer.java/initPlayer()函数初始化播放器,初始化代码如下

   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));
    }

a. loadLibrariesOnce之前解释过,也调用过
b. initNativeOnce,然后调用IjkMediaPlayer.java/native_init(),其实就是调用IjkMediaPlayer_native_init(),这里没做什么操作好像
c. 创建handler,用于后面c层代码调用java代码来发送message信息
d. native_setup,其实就是调用ijkplayer_jni.c/IjkMediaPlayer_native_setup()函数,该函数里面又调用了ijkplayer_jni.c/ijkmp_android_create()

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;
}
  • ijkplayer_android.c/ijkmp_create()->ijkplayer.c/ffp_create()(这里可以看出ijk其实底层就是包装了一个ffplayer)创建播放器的时候还传入了一个msg_loop(ijkplayer_jni.c/message_loop_n),这个函数名是用于之后创建消息处理线程用的
  • ijkplayer_android.c/SDL_VoutAndroid_CreateForAndroidSurface(),创建图像渲染对象
  • ijkplayer_android.c/ffpipeline_create_from_android(),创建平台相关的IJKFF_Pipeline对象,包括视频解码以及音频输出部分

2) mMediaPlayer.setDataSource用于设置视频源,其实是调用了jni层的ijkplayer_jni.c/IjkMediaPlayer_setDataSourceAndHeaders()函数,最终调用了ijkplayer.c/ijkmp_set_data_source_l()

static int ijkmp_set_data_source_l(IjkMediaPlayer *mp, const char *url)
{
    ...
    mp->data_source = strdup(url);
   ...
    ijkmp_change_state_l(mp, MP_STATE_INITIALIZED);
    return 0;
}
  • mp->data_source = strdup(url)设置视频源
  • ijkmp_change_state_l(),将MP_STATE_INITIALIZED消息推入ffplayer->msg_queue中,就是ffplayer的消息队列

3) mMediaPlayer.prepareAsync(),调用了ijkplayer_jni.c/ijkmp_prepare_async(),最终调用到ijkplayer.c/ijkmp_prepare_async_l()

static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
    ...
    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;
}

a ijkmp_change_state_l、msg_queue_start还是将消息推入ffplayer->msg_queue中
b mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_msg_loop, mp, "ff_msg_loop")用来创建处理处理消息的线程,这里的ijkmp_msg_loop就是在之前jkplayer_android.c/ijkmp_create()赋值的
c 在ffp_prepare_async_l()中

  • 先调用ff_ffplayer.c/ ffpipeline_open_audio_output(),ffpipeline_open_audio_output内部调用func_open_audio_output()该函数指针在前面的ijkplayer_android.c/ffpipeline_create_from_android()中赋值其实就是调用ff_ffpipeline.c/ func_open_audio_output()最终调用ff_ffpipeline_android.c/ func_open_audio_output()用于选择相应音频输出方式(opensles or audiotrack)
  • 调用ff_ffplayer.c/stream_open,其中stream_open()会创建读线程和视频渲染线程,这部分稍后的文章会详细讲解,只是这里稍微说一下在读线程read_thread()准备好后ffp->prepared = true将准备好的标志位置为true,ffp_notify_msg1(ffp, FFP_MSG_PREPARED)将准备好的消息推给消息队列,消息线程接到这个消息后调用java层的函数,向handler发送MEDIA_PREPARED,handler的handleMessage函数里面会处理这个消息
public void handleMessage(Message msg) {
            ...
            switch (msg.what) {
            case MEDIA_PREPARED:
                player.notifyOnPrepared();
                return;
            ...
            default:
                DebugLog.e(TAG, "Unknown message type " + msg.what);
            }
        }
    }

最后调用下面函数,该函数又调用start(),即IjkMediaPlayer_start()函数开始视频的播放,到这里ijkplayer的初始化流程就结束了,内容很多,一定要下载代码多看几次

public void onPrepared(IMediaPlayer mp) {
            mPrepareEndTime = System.currentTimeMillis();
            mHudViewHolder.updateLoadCost(mPrepareEndTime - mPrepareStartTime);
            mCurrentState = STATE_PREPARED;

            // Get the capabilities of the player for this stream
            // REMOVED: Metadata

            if (mOnPreparedListener != null) {
                mOnPreparedListener.onPrepared(mMediaPlayer);
            }
            if (mMediaController != null) {
                mMediaController.setEnabled(true);
            }
            mVideoWidth = mp.getVideoWidth();
            mVideoHeight = mp.getVideoHeight();

            int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
            if (seekToPosition != 0) {
                seekTo(seekToPosition);
            }
            if (mVideoWidth != 0 && mVideoHeight != 0) {
                //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
                // REMOVED: getHolder().setFixedSize(mVideoWidth, mVideoHeight);
                if (mRenderView != null) {
                    mRenderView.setVideoSize(mVideoWidth, mVideoHeight);
                    mRenderView.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen);
                    if (!mRenderView.shouldWaitForResize() || mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
                        // We didn't actually change the size (it was already at the size
                        // we need), so we won't get a "surface changed" callback, so
                        // start the video here instead of in the callback.
                        if (mTargetState == STATE_PLAYING) {
                            start();
                            if (mMediaController != null) {
                                mMediaController.show();
                            }
                        } else if (!isPlaying() &&
                                (seekToPosition != 0 || getCurrentPosition() > 0)) {
                            if (mMediaController != null) {
                                // Show the media controls when we're paused into a video and make 'em stick.
                                mMediaController.show(0);
                            }
                        }
                    }
                }
            } else {
                // We don't know the video size yet, but should start anyway.
                // The video size might be reported to us later.
                if (mTargetState == STATE_PLAYING) {
                    start();
                }
            }
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容