本文链接:https://rainmonth.github.io/posts/A191206.html
摘要
结合源码,分析下ijkplayer的初始化流程,并对这个过程中的一些关键点(如消息循环、线程创建C层和Java层的通信几个方面来)做了一个详细分析。
整体结构
先看看有哪些Player可供使用
1 | IMediaPlayer (tv.danmaku.ijk.media.player) # 播放器通用接口,不同播放器的差异化通过这个接口来体现 |
通用接口定义
1 | // IMediaPlayer.java |
通过上面的简单分析,可以看出IMediaPlayer抽象出了不同版本播放器的通用功能,也保留了不同播放器独特支持的方法。
IjkMediaPlayer实现
IjkMediaPlayer的创建
IjkMediaPlayer的创建是有Java层和C/C++层联合完成的,现在结合源码看看具体的步骤
Java层
IjkMediaPlayer的Java层比较简单,主要就是继承AbstractMediaPlayer,重写IMediaPlayer的相关方法,定义一些与C/C++交互的方法,并在合适的时机调用它。这里主要来说说IjkMediaPlayer的创建,因为它涉及到so库的加载。上代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68/**
* 默认的so库加载器,在类被加载的时候就创建了
* Load them by yourself, if your libraries are not installed at default place.
*/
private static final IjkLibLoader sLocalLibLoader = new IjkLibLoader() {
public void loadLibrary(String libName) throws UnsatisfiedLinkError, SecurityException {
System.loadLibrary(libName);
}
};
// 确保so库只加载一次
private static volatile boolean mIsLibLoaded = false;
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");
mIsLibLoaded = true;
}
}
}
// 确保C层相关代码初始化一次
private static volatile boolean mIsNativeInitialized = false;
private static void initNativeOnce() {
synchronized (IjkMediaPlayer.class) {
if (!mIsNativeInitialized) {
native_init();
mIsNativeInitialized = true;
}
}
}
// 利用上面的sLocalLibLoader来加载so库
public IjkMediaPlayer() {
this(sLocalLibLoader);
}
// 采用自定义的库加载器来加载so
public IjkMediaPlayer(IjkLibLoader libLoader) {
initPlayer(libLoader);
}
// 播放器初始化工作
private void initPlayer(IjkLibLoader libLoader) {
// 加载相关的so文件,主要是ijkffmpeg、ijksdl、ijkplayer三个so文件
loadLibrariesOnce(libLoader);
// c层的初始化
initNativeOnce();
// 根据不同的线程创建不同的Handler
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;
}
// 让c++层以弱引用的方式来引用Java层创建的IjkMediaPlayer(因为Java层创建IjkMedioPlayer对象更容易)
native_setup(new WeakReference<IjkMediaPlayer>(this));
}
通过上面的一系列流程,就完成了IjkMediaPlayer的创建和初始化工作。接下来的操作就主要在c/c++层了,因为每个IMediaPlayer方法的实现,最终都会有一个与之对应的C/C++层方法与之对应,这一点通过源码横容易发现。
C/C++层
接下来分析IjkPlayer c/c++层的实现。根据JNI文档的Invocation API相关章节描述,native库一旦被加加载(即调用System.loadLibrary()),这个native库就对所有的classLoader可见。这就会导致不同类加载器中的连个类会链接到同一个native方法上,会导致以下两个问题:
- 一个类会错误的链接到一个有其他类加载器加载的同名native库上;
- native库很容易混淆调用它的类(因为native库可以同时由不同的ClassLoader加载),这就导致了命名空间隔离无效,会产生类型安全问题;
为了解决上面的问题,每个类加载器都维护了它自己的native库集,JNI规范规定同一个native库只能被一个类加载器加载,当在两个class Loader中加载native库时将会抛出UnsatisfiedLinkError
错误,通过这种方式,会带来以下两点好处:
- 基于ClassLoader命名空间隔离在native库中得以保持,这就避免了不同加载器中同一个类混下的发生;
- 除此之外,native库会在与之对应的classLoader GC时被卸载
为了实现版本管理和资源控制,JNI库要对外提供一下两个方法:JNI_OnLoad和JNI_OnUnload。
所以我们看看ijkplayer中这两个方法的实现(这两个方法在ijkplayer_jni.c这个文件中)
JNI_OnLoad
jint JNI_OnLoad(JavaVM *vm, void *reserved)
该方法在native库被加载(即调用System.loadLibrary())方法的时候被调用,该方法将返回native库所必需的的JNI版本号。如果要使用新的JNI函数,需要返回JNI_VERSION_1_2或以上;如果native库没有提供自己的
JNI_OnLoad方法虚拟机就认为他需要的是
JNI_VERSION_1_1`。如果虚拟机不能识别JNI_OnLoad返回的版本号,虚拟机就会卸载这个native库。
JNI_Onload_L(JavaVM *vm, void *reserved)
如果一个native库 L是通过静态链接的,那么在首次加载这个库的时候JNI_Onload_L
将已相同的参数被调用并且要求返回对应的返回值,返回值必须是JNI_VERSION_1_8
或之后的版本,如果返回的是不识别版本号,虚拟机会和JNI_OnLoad
做同样的操作。
1 | JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) |
看看注册了哪些方法:1
2
3
4
5
6
7
8
9static JNINativeMethod g_methods[] = {
{
"_setDataSource",
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V",
(void *) IjkMediaPlayer_setDataSourceAndHeaders
},
{ "_setDataSourceFd", "(I)V", (void *) IjkMediaPlayer_setDataSourceFd },
...
};
这些都是IjkMediaPlayer定义的native方法,由此可见,这里将Java层的方法和c层的方法映射起来了。比如当Java层调用start方法是,其实调用的是C层的IjkMediaPlayer_start方法。观察IjkMediaPlayer源码时,发现其定义了两个注解: AccessedByNative
和CalledByNative
,顾名思义,这个就是用来表示该变量是供native使用的,该方法是供native调用的,具体可以在IjkMediaPlayer.c
中看到:
1 |
|
定义了一个结构体,里面内容是被注解的Filed和Method,同时可以看到被注解的方法在IjkMediaPlayer.c中定义的方法中别调用。
JNI_OnUnload
void JNI_OnUnload(JavaVM *vm, void *reserved)
和JNI_OnUnload_L(JavaVM *vm, void *reserved)
这两个方法在在包含native库的class Loader被GC时被调用,由于GC的具体时机不明确,所以这个方法通常会运行在不确定的上下文中,所以在调用的时候需要做好防护措施。这个方法里面通常会进行资源的回收释放。
native_setup方法
在initPlayer方法中,会调用native_setup方法,其具体实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20static void
IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
MPTRACE("%s\n", __func__);
// 创建C层的IjkMediaPlayer(其实是一个结构体,该结构体最终在ijkplayer_intrenal.h中定义)
IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
// 检查是否创建成功,如果失败,抛出异常信息,并执行label return(减少mp的引用次数)
JNI_CHECK_GOTO(mp, env, "java/lang/OutOfMemoryError", "mpjni: native_setup: ijkmp_create() failed", LABEL_RETURN);
// 将Java层的IjkPlayer(thiz)和C层的IjkPlayer(mp)绑定
jni_set_media_player(env, thiz, mp);
// 持有Java层IjkPlayer的弱引用
ijkmp_set_weak_thiz(mp, (*env)->NewGlobalRef(env, weak_this));
// 利用mp的弱引用来设置ffp的一些参数
ijkmp_set_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
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);
}
看看ijkmp_android_create
的实现(该方法在ijkplayer_android.c中)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
IjkMediaPlayer *mp = ijkmp_create(msg_loop);
if (!mp)
goto fail;
// 设置vout,其实就是创建Surface,用于展示视频
mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();
if (!mp->ffplayer->vout)
goto fail;
// 设置pipeline
mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer);
if (!mp->ffplayer->pipeline)
goto fail;
// vout和pipeline绑定
ffpipeline_set_vout(mp->ffplayer->pipeline, mp->ffplayer->vout);
return mp;
fail:
ijkmp_dec_ref_p(&mp);
return NULL;
}
分析到此,IjkMediaPlayer的创建大致就完成了。
IjkMediaPlayer数据源的绑定
数据的绑定在Java层由setDataSource来实现,最终调用_setDataSourceXX系列的native方法,可见最终的数据绑定发生在C/C++层。
- _setDataSource,即C层
IjkMediaPlayer_setDataSourceAndHeaders
,对应于Java层的setDataSource
; - _setDataSourceFd,即C层
IjkMediaPlayer_setDataSourceFd
,对应于Java层的setDataSource(FileDescriptor fd)
; - _setDataSource(重载的),即C层
IjkMediaPlayer_setDataSourceCallback
,对应于setAndroidIOCallback
;
这里主要分析IjkMediaPlayer_setDataSourceAndHeaders
的实现,通过代码看它的调用链:1
2
3
4
5
6
7
8
9
10static void
IjkMediaPlayer_setDataSourceAndHeaders(
JNIEnv *env, jobject thiz, jstring path,
jobjectArray keys, jobjectArray values)
{
...
// 真正的设置播放源实现
retval = ijkmp_set_data_source(mp, c_path);
...
}
ijkmp_set_data_source
1
2
3
4
5
6int ijkmp_set_data_source(IjkMediaPlayer *mp, const char *url)
{
...
int retval = ijkmp_set_data_source_l(mp, url);
...
}
ijkmp_set_data_source_l
1
2
3
4
5
6
7
8
9
10
11
12static int ijkmp_set_data_source_l(IjkMediaPlayer *mp, const char *url)
{
...
freep((void**)&mp->data_source);
mp->data_source = strdup(url);
if (!mp->data_source)
return EIJK_OUT_OF_MEMORY;
ijkmp_change_state_l(mp, MP_STATE_INITIALIZED);
return 0;
}
最终走到msg_queue_put_private
,这个过程其实就是生成一个AVMessage对象,然后放入ijkplayer->ffplayer->msg_queue中,最后通知其他线程去处理。1
2
3
4
5
6inline static int msg_queue_put_private(MessageQueue *q, AVMessage *msg)
{
...
SDL_CondSignal(q->cond);
return 0;
}SDL_CondSignal
1
2
3
4
5
6
7
8int SDL_CondSignal(SDL_cond *cond)
{
assert(cond);
if (!cond)
return -1;
return pthread_cond_signal(&cond->id);
}
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行,当某个线程继续执行的时候发现msg_queue中有msg的时候会有相应操作。这个pthread_cond_signal就是负责通知其他线程来处理消息队列中的消息的。具体是那个线程,在我们分析完prepareAsync
函数的调用过程后就明白了。
prepareAsync
prepareAsync
最终调用的是C层的IJK_MediaPlayer_prepareAsync
方法。调用链:IjkMediaPlayer_prepareAsync()->ijkmp_prepare_async()->ijkmp_prepare_async_l(),和setDataSource
的过程有点类似,这里详细分析下ijkmp_prepare_async_l()
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
...
// 发送MP_STATE_ASYNC_PREPARING消息到消息队列
ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
// 发送FFP_MSG_FLUSH消息到消息队列
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");
// 真正的prepare
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;
}
ffp_prepare_async_l
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)
{
...
// log输出(运行ijkplayer的demo是可以看到这些输出
av_log(NULL, AV_LOG_INFO, "===== versions =====\n");
ffp_show_version_str(ffp, "ijkplayer", ijk_version_info());
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");
av_opt_set_dict(ffp, &ffp->player_opts);
if (!ffp->aout) {
ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
if (!ffp->aout)
return -1;
}
...
// 流处理开始
VideoState *is = stream_open(ffp, file_name, NULL);
...
return 0;
}
stream_open
中做了很多工作,但从线程层面,主要创建了两个线程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
...
// 创建线程用以视频的显示
is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
if (!is->video_refresh_tid) {
av_freep(&ffp->is);
return NULL;
}
is->initialized_decoder = 0;
// 创建读取网络数据或本地数据的线程,这个线程即read_thread内部还做了很多的消息处理,比如FFP_MSG_PREPARED(接到这个消息后,会调用Java层函数,向handler发送`MEDIA_PREPARED`、FFP_REQ_START等消息)
is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
if (!is->read_tid) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
goto fail;
}
...
return is;
}
在读取线程read_thread
发送FFP_MSG_PREPARED消息后,C层会调用Java层定义好的函数postEventFromNative
向Java层发送MEDIA_PREPARED
消息,这样就实现了c层到Java层的通信了。注意C层的消息循环处理函数在ijkplayer_jnc.c文件中。
受到消息后,player.notifyOnPrepared()
会执行,这样就通知到了Java层,播放器已经准备就绪了。
总结
本文分析的ijkplayer的整体结构,并详细介绍了ijkplayer的初始化流程,同时了解到了ijkplayerC层比较重要的几个线程(ijkmp_msg_loop
、video_refresh_thread
、read_thread
)的创建时机,ijkplayer消息的处理、ijkplayerC层和Java层之间的通信方式,后续会介绍读取线程read_thread
的具体工作过程。