Android 开源库分析——IjkPlayer简介

本文链接:https://rainmonth.github.io/posts/A191206.html

摘要

结合源码,分析下ijkplayer的初始化流程,并对这个过程中的一些关键点(如消息循环、线程创建C层和Java层的通信几个方面来)做了一个详细分析。

整体结构

先看看有哪些Player可供使用

1
2
3
4
5
6
7
8
IMediaPlayer (tv.danmaku.ijk.media.player)                  # 播放器通用接口,不同播放器的差异化通过这个接口来体现
TextureMediaPlayer (tv.danmaku.ijk.media.player) # 供IjkVideoView使用的播放器
MediaPlayerProxy (tv.danmaku.ijk.media.player) # extureMediaPlayer播放器代理类,内部维护了一个TextureMediaPlayer实例
TextureMediaPlayer (tv.danmaku.ijk.media.player)
AbstractMediaPlayer (tv.danmaku.ijk.media.player) # 主要定义了设置监听器的方法和事件通知的方法
AndroidMediaPlayer (tv.danmaku.ijk.media.player) # Android系统MediaPlayer 的Ijk实现
IjkMediaPlayer (tv.danmaku.ijk.media.player) # IjkMediaPlayer实现,里面的一些底层方法是基于ffplay来实现的,ffplay是基于FFmpeg实现的跨平台播放器
IjkExoMediaPlayer (tv.danmaku.ijk.media.exo) # ExoPlayer的Ijk实现,内部持有一个DemoPlayer实例,该实例持有一个ExoPlayerImpl实例,播放最终都是通过它来完成的

通用接口定义

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// IMediaPlayer.java
public interface IMediaPlayer {
/*
* 常量定义,最好不要修改
*/
int MEDIA_INFO_UNKNOWN = 1;
...

// 设置视频媒体展示的容器
void setDisplay(SurfaceHolder sh);

// 不同版本的播放源设置
void setDataSource(Context context, Uri uri)
throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
void setDataSource(Context context, Uri uri, Map<String, String> headers)
throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;

void setDataSource(FileDescriptor fd)
throws IOException, IllegalArgumentException, IllegalStateException;

void setDataSource(String path)
throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;

// 获取播放源
String getDataSource();

// 异步资源准备,对于流媒体,应该进行异步prepare,避免缓冲所有内容引起的阻塞(当然如果有这种需求就另当别论了)
void prepareAsync() throws IllegalStateException;
// 开始播放
void start() throws IllegalStateException;
// 停止播放
void stop() throws IllegalStateException;
// 暂停播放
void pause() throws IllegalStateException;
// 播放时是否保持屏幕常亮(最好调用这个方法,因为它无需请求权限)
void setScreenOnWhilePlaying(boolean screenOn);
// 获取视频的宽
int getVideoWidth();
// 获取视频的高
int getVideoHeight();
// 是否正在播放
boolean isPlaying();
// 拖动支持
void seekTo(long msec) throws IllegalStateException;
// 当前的位置
long getCurrentPosition();
// 获取时长
long getDuration();
// 播放器资源释放
void release();
// 播放器重置
void reset();
// 音量设置
void setVolume(float leftVolume, float rightVolume);
// 获取AudioSessionId,只有系统的MediaPlayer才支持
int getAudioSessionId();
// ExoPlayer不支持
MediaInfo getMediaInfo();

@SuppressWarnings("EmptyMethod")
@Deprecated
void setLogEnabled(boolean enable);

@Deprecated
boolean isPlayable();

// 监听器设置 这些方法主要有AbstractMediaPlayer来实现
void setOnPreparedListener(OnPreparedListener listener);
...

/*--------------------
* 回调定义
*/
interface OnPreparedListener {
void onPrepared(IMediaPlayer mp);
}
...

/*--------------------
* 可选项设置,差异化的体现(有的播放器并不支持)
*/
// 设置音频资源的类型
void setAudioStreamType(int streamtype);

@Deprecated
void setKeepInBackground(boolean keepInBackground);

// IjkMediaPlayer才支持
int getVideoSarNum();

int getVideoSarDen();

@Deprecated
void setWakeMode(Context context, int mode);

void setLooping(boolean looping);

boolean isLooping();

/*--------------------
* AndroidMediaPlayer: JELLY_BEAN才支持
*/
ITrackInfo[] getTrackInfo();

/*--------------------
* AndroidMediaPlayer: ICE_CREAM_SANDWICH才支持
*/
void setSurface(Surface surface);

/*--------------------
* AndroidMediaPlayer: M:才支持
*/
void setDataSource(IMediaDataSource mediaDataSource);
}

通过上面的简单分析,可以看出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() {
@Override
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
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
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 );

// 获取到IjkPlayer的引用,即g_clazz.clazz的值
IJK_FIND_JAVA_CLASS(env, g_clazz.clazz, JNI_CLASS_IJKPLAYER);
// 方法注册,形成Java层定义的native方法和c层定义的方法之间的映射
(*env)->RegisterNatives(env, g_clazz.clazz, g_methods, NELEM(g_methods) );

// 最终就是FFmpeg的全局初始化工作,具体可查看ff_ffplay.c的ffp_global_init方法
ijkmp_global_init();
// 这只inject_callback
ijkmp_global_set_inject_callback(inject_callback);

// FFmpegApi初始化
FFmpegApi_global_init(env);

return JNI_VERSION_1_4;
}

看看注册了哪些方法:

1
2
3
4
5
6
7
8
9
static 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源码时,发现其定义了两个注解: AccessedByNativeCalledByNative ,顾名思义,这个就是用来表示该变量是供native使用的,该方法是供native调用的,具体可以在IjkMediaPlayer.c中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "IjkMediaPlayer.h"

typedef struct J4AC_tv_danmaku_ijk_media_player_IjkMediaPlayer {
jclass id;

jfieldID field_mNativeMediaPlayer;
jfieldID field_mNativeMediaDataSource;
jfieldID field_mNativeAndroidIO;
jmethodID method_postEventFromNative;
jmethodID method_onSelectCodec;
jmethodID method_onNativeInvoke;
} J4AC_tv_danmaku_ijk_media_player_IjkMediaPlayer;
static J4AC_tv_danmaku_ijk_media_player_IjkMediaPlayer class_J4AC_tv_danmaku_ijk_media_player_IjkMediaPlayer;

定义了一个结构体,里面内容是被注解的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
20
static 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
24
IjkMediaPlayer *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
10
static 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
6
int 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
12
static 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
6
inline 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
8
int 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
23
static 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
32
int 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
22
static 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_loopvideo_refresh_threadread_thread)的创建时机,ijkplayer消息的处理、ijkplayerC层和Java层之间的通信方式,后续会介绍读取线程read_thread的具体工作过程。

参考文章