今日は IjkPlayer のプレーヤー作成プロセスについて紹介します。本記事は IjkPlayer のソースコードを読む旅の正式なスタートとなり、主な内容は以下の通りです:
- so の初期化
- Java 層のプレーヤー作成
- IjkMediaPlayer 構造体
- ネイティブ層のプレーヤー作成
- 呼び出しフローチャート
読む前に、前の数記事を先に見ておくと良いでしょう:
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();
上記のメソッドはそれぞれmonstartup
とmoncleanup
関数を呼び出し、プログラムの実行開始と終了時に呼ばれます。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);
// ネイティブメソッドと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
はネイティブ関数と 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 },
};
上記のコードの主な作業は以下の通りです:
- ネイティブメソッドと Java メソッドの対応関係を登録。
- コーデック、デマルチプレクサ、プロトコルなどを登録。
- ネイティブ層から呼び出される
onNativeInvoke
コールバックを設定し、主にその戻り値に関心を持つ(例えば、ネットワーク切断の再接続など)。
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_setup(new WeakReference<IjkMediaPlayer>(this));
}
初期化時にijkffmpeg
、ijksdl
、ijkplayer
ライブラリがロードされ、mEventHandler
がネイティブからの再生イベント(再生開始、再生完了、再生エラーなど)を処理するために作成されます。具体的には、ネイティブ層が Java 層の関数postEventFromNative
を呼び出して対応するイベントを送信します。C 層のコードを読む前に、IjkMediaPlayer
構造体について理解しておきましょう。
IjkMediaPlayer 構造体#
IjkPlayer に対応するIjkMediaPlayer
構造体は、その初期化が主にこの構造体の初期化に関わります。その定義は以下の通りです:
struct IjkMediaPlayer {
/* IjkMediaPlayerが一度作成されるとref_countが1増加 */
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;
/* ユーザーがシークバーを操作したかどうかを示すフラグ */
int seek_req;
/* シークのミリ秒値 */
long seek_msec;
};
再生プロセス全体で関わるパラメータは基本的にIjkMediaPlayer
構造体のメンバー変数です。以降、ネイティブ層の IjkPlayer の作成はこの構造体のメンバー変数の初期化を行います。
ネイティブ層のプレーヤー作成#
ネイティブ層のプレーヤー作成は主に構造体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_Vout
をIJKFF_Pipeline_Opaque
にバインドします。
呼び出しフローチャート#
IjkPlayer の作成に関する主な関数呼び出しフローは以下の通りです:
他の詳細は後の文章で紹介します。次の記事では IjkPlayer のメッセージループメカニズムについて紹介します。