このトピックでは、Android 上で Native RTS SDK を FFmpeg ベースのプレーヤーと統合して RTS を実装する方法について説明します。このトピックでは、ijkplayer の k0.8.8 タグを例として使用します。
前提条件
ijkplayer のソースコードがコンパイル済みであること。詳細については、「ijkplayer」リポジトリの README.md ファイルをご参照ください。
手順
ijkplayer のソースコードをダウンロードして解凍します。ダウンロードリンクについては、「ijkplayer」をご参照ください。
Native RTS SDK をダウンロードして解凍します。ダウンロードリンクについては、「リリースノート」をご参照ください。
ijkplayer のコンパイルスクリプトを変更します。
ijkplayer-android/init-android.sh を変更します。`pull_fork` 呼び出しで、armv7a と arm64 アーキテクチャのみを保持します。

ijkplayer-android/config/module-lite.sh を変更して、パルス符号変調 (PCM) デコーディングをサポートします。Native RTS SDK は PCM データを出力します。
# aliyun rts export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=pcm_s16be_planar" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=pcm_s16le" export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-decoder=pcm_s16le_planar"ijkplayer-android/android/contrib/compile-ffmpeg.sh を変更します。`FF_ACT_ARCHS_64` については、armv7a と arm64 アーキテクチャのみを保持します。
FF_ACT_ARCHS_64="armv7a arm64"ijkplayer-android/android/compile-ijk.sh を変更します。`ACT_ABI_64` については、armv7a と arm64 アーキテクチャのみを保持します。
ACT_ABI_64="armv7a arm64"
Native RTS SDK をプラグインとして ijkplayer に統合します。次の 2 つの方法のいずれかを使用できます。
統合方法
説明
利点
欠点
FFmpeg の拡張
ijkplayer の FFmpeg のデマルチプレクサを拡張します。
この方法は簡単です。Alibaba Real-Time Communication (ARTC) URL 用に個別のロジックを追加する必要はありません。
FFmpeg ライブラリを再コンパイルする必要があります。
ijkplayer の拡張
AVInputFormat を ijkplayer に追加します。
FFmpeg をコンパイルする必要はありません。
ff_ffplay.c ファイルにロジックコードを追加する必要があります。
ijkplayer をプロジェクトに統合します。
Native RTS SDK から RtsNetSDK.jar ファイルをプロジェクトにコピーします。
Native RTS SDK の動的ライブラリを、RTS 再生に使用される Activity にインポートします。
static { System.loadLibrary("RtsSDK"); }ijkplayer-arm64、ijkplayer-armv7a、および ijkplayer-java モジュールをインポートします。

ijkplayer API を呼び出して RTS 機能を実装します。
ijkplayer インスタンスの作成
mIjkPlayer = new IjkMediaPlayer();コールバックの設定
mIjkPlayer.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() { @Override public void onPrepared(IMediaPlayer iMediaPlayer) { } }); mIjkPlayer.setOnInfoListener(new IMediaPlayer.OnInfoListener() { @Override public boolean onInfo(IMediaPlayer iMediaPlayer, int arg1, int arg2) { }); mIjkPlayer.setOnVideoSizeChangedListener(new IMediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(IMediaPlayer iMediaPlayer, int width, int height, int sarNum, int sarDen) { } }); mIjkPlayer.setOnErrorListener(new IMediaPlayer.OnErrorListener() { @Override public boolean onError(IMediaPlayer iMediaPlayer, int framework_err, int impl_err) { return false; } });表示ウィンドウの設定
mIjkPlayer.setSurface(surface);再生ソースの設定
mIjkPlayer.setDataSource("artc://<ストリーミング URL>");ステータスの制御
public void prepare() { if(mIjkPlayer != null){ mIjkPlayer.prepareAsync(); } } @Override public void start() { if(mIjkPlayer != null){ mIjkPlayer.start(); } } public void stop() { if(mIjkPlayer != null){ mIjkPlayer.stop(); } } public void release() { if(mIjkPlayer != null){ mIjkPlayer.release(); } }
RTS イベントのリッスン
ijkplayer での Native RTS SDK からのメッセージコールバックのリッスン
ijkplayer/android/ijkplayer プロジェクトを開きます。
ff_ffplay.c ファイルを変更します。次のコードブロックを
static int audio_open(FFPlayer *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params){}メソッドの後に挿入します。extern int artcDemuxerMessage(struct AVFormatContext *s, int type, void *data, size_t data_size); //aliyun rts: artc メッセージを受信 int onArtcDemuxerMessage(struct AVFormatContext *s, int type, void *data, size_t data_size) { return artcDemuxerMessage(s, type, data, data_size); } int artcDemuxerMessage(struct AVFormatContext *s, int type, void *data, size_t data_size) { //aliyun rts: アプリにメッセージを送信 FFPlayer *ffp = (FFPlayer *)s->opaque; const char *data_msg = (const char *)data; ffp_notify_msg4(ffp,FFP_MSG_ARTC_DIRECTCOMPONENTMSG,type,0,data_msg,data_size); return 0; }ff_ffplay.c ファイルを変更します。次の RTS コードブロックを
static int is_realtime(AVFormatContext *s){}メソッドに挿入します。static int is_realtime(AVFormatContext *s) { if( !strcmp(s->iformat->name, "rtp") || !strcmp(s->iformat->name, "rtsp") || !strcmp(s->iformat->name, "sdp") // ***rts コードブロック開始*** || !strcmp(s->iformat->name, "artc") // ***rts コードブロック終了*** ) return 1; if(s->pb && ( !strncmp(s->filename, "rtp:", 4) || !strncmp(s->filename, "udp:", 4) ) ) return 1; return 0; }ff_ffplay.c ファイルを変更します。次の RTS コードブロックを
static int read_thread(void *arg){}メソッドに挿入します。static int read_thread(void *arg) { ...... ic = avformat_alloc_context(); if (!ic) { av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n"); ret = AVERROR(ENOMEM); goto fail; } // ***rts コードブロック開始*** ic->opaque = ffp; ic->control_message_cb = onArtcDemuxerMessage; // ***rts コードブロック終了*** ...... if (ffp->skip_calc_frame_rate) { av_dict_set_int(&ic->metadata, "skip-calc-frame-rate", ffp->skip_calc_frame_rate, 0); av_dict_set_int(&ffp->format_opts, "skip-calc-frame-rate", ffp->skip_calc_frame_rate, 0); } // ***rts コードブロック開始*** if(strncmp(is->filename, "artc://", 7) == 0) { extern AVInputFormat ff_rtc_demuxer; is->iformat = &ff_rtc_demuxer; } else { if(ffp->iformat_name) is->iformat = av_find_input_format(ffp->iformat_name); } // ***rts コードブロック終了*** ...... pkt->flags = 0; // ***rts コードブロック開始*** if(strncmp(is->filename, "artc://", 7) == 0) { bool videoExist = is->video_stream >= 0; bool audioExist = is->audio_stream >= 0; // av_log(NULL, AV_LOG_INFO, "videoDuration %lld audioDuration %lld rate %f videoframeQue %d audioFrameque %d\n", // is->videoq.duration, is->audioq.duration, ffp->pf_playback_rate, // frame_queue_nb_remaining(&is->pictq), frame_queue_nb_remaining(&is->sampq)); if(!videoExist) { if(is->audioq.duration > 300 ) { // 加速 if(ffp->pf_playback_rate <= 1.0) { ffp->pf_playback_rate = 1.3; ffp->pf_playback_rate_changed = 1; av_log(NULL, AV_LOG_INFO, "aliyun rts set rate to %f\n", ffp->pf_playback_rate); } } else if(is->audioq.duration < 200) { // 速度を復元 if(ffp->pf_playback_rate > 1.0) { ffp->pf_playback_rate = 1.0; ffp->pf_playback_rate_changed = 1; av_log(NULL, AV_LOG_INFO, "aliyun rts restore rate 1.0\n"); } } } else if((!videoExist || (videoExist && is->videoq.duration > 300)) && (!audioExist || (audioExist && is->audioq.duration > 300))) { if(ffp->pf_playback_rate <= 1) { ffp->pf_playback_rate = 1.3; ffp->pf_playback_rate_changed = 1; av_log(NULL, AV_LOG_INFO, "aliyun rts set rate 1.1\n"); } } else if((videoExist && is->videoq.duration <= 100) || (audioExist && is->audioq.duration <= 100)){ if(ffp->pf_playback_rate > 1) { ffp->pf_playback_rate = 1; ffp->pf_playback_rate_changed = 1; av_log(NULL, AV_LOG_INFO, "aliyun rts set rate 1\n"); } } } // ***rts コードブロック終了*** ...... }ff_ffmsg.h ファイルを変更して、RTS メッセージのインターフェイス宣言を追加します。
#define FFP_MSG_ARTC_DIRECTCOMPONENTMSG 3000RTS メッセージを上位レイヤーに送信します。
ijkplayer_android_def.h ファイルを変更して、RTS メッセージの列挙型を追加します。
enum media_event_type { // 他のコードは省略 MEDIA_ARTC_MESSAGE = 3000, // aliyun rts : msg info key };ijkplayer_jni.c ファイルを変更して、RTS メッセージを上位レイヤーに送信します。
static void message_loop_n(JNIEnv *env, IjkMediaPlayer *mp) { //... 他のコードは省略 while (1) { switch (msg.what) { //aliyun rts: artc イベントを投稿 case FFP_MSG_ARTC_DIRECTCOMPONENTMSG: if (msg.obj) { const char * result = (const char *)msg.obj; ALOGE("aliyun rts : FFP_MSG_ARTC_DIRECTCOMPONENTMSG = %d , %s\n", msg.arg1,result); jstring data_msg = (*env)->NewStringUTF(env, result); post_event2(env, weak_thiz, MEDIA_ARTC_MESSAGE, msg.arg1, 0, data_msg); J4A_DeleteLocalRef__p(env, &data_msg); } else { post_event2(env, weak_thiz, MEDIA_ARTC_MESSAGE, 0, 0, NULL); } break; } } }IjkMediaPlayer.java ファイルを変更して、RTS メッセージを受信します。
private static class EventHandler extends Handler { @Override public void handleMessage(Message msg) { IjkMediaPlayer player = mWeakPlayer.get(); if (player == null || player.mNativeMediaPlayer == 0) { DebugLog.w(TAG, "IjkMediaPlayer went away with unhandled events"); return; } switch (msg.what) { //aliyun artc: イベントを投稿 case MEDIA_ARTC_MESSAGE: if(msg.arg1 == FFP_ARTC_CONNECT_LOST){ player.notifyOnARTCMessage("NetWorkDisconnect",0,""); }else if(msg.arg1 == FFP_ARTC_RECOVERED){ player.notifyOnARTCMessage("NetWorkRetrySuccess",0,""); }else{ if(msg.obj == null){ player.notifyOnARTCMessage("DirectComponentMSG",0, ""); }else{ String result = (String) msg.obj; result = result.replaceAll("\"",""); player.mAliyunRTSMsgBuilder = new StringBuilder("{\"content\":"); player.mAliyunRTSMsgBuilder.append("\"").append(result).append("\"}"); player.notifyOnARTCMessage("DirectComponentMSG",0, (String) result); } } break; } } }RTS メッセージコールバックを IMediaPlayer.java ファイルに追加します。
//aliyun rts: artc メッセージコールバック interface OnARTCMessageListener{ void onMessage(String name,int externValue,String extraMsg); }リスナー設定を AbstractMediaPlayer.java ファイルに追加します。
//aliyun rts : artc メッセージリスナーを設定 public final void setOnARTCMessageListener(OnARTCMessageListener listener){ this.mOnARTCMessageListener = listener; } //aliyun rts : コールバック protected final void notifyOnARTCMessage(String name,int externValue,String extraMsg){ if(mOnARTCMessageListener != null){ mOnARTCMessageListener.onMessage(name,externValue,extraMsg); } }./compile-ijk.sh スクリプトを実行して再コンパイルします。
ijkplayer に Native RTS SDK のリトライメソッドを追加する
ff_ffplay.h ファイルを変更して、`rts_reload_flag` 変数の定義を追加します。
int rts_reload_flag;ff_ffplay.c ファイルを変更します。次のメソッドブロックを
static int audio_open(FFPlayer *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params){}メソッドの後に挿入します。extern int artc_reload(AVFormatContext *ctx); void ffp_rts_reload(FFPlayer *ffp){ if(rts_reload_flag == 0) { rts_reload_flag = 1; } }ff_ffplay.c ファイルを変更します。次の RTS コードブロックを
static int read_thread(void *arg){}メソッドに挿入します。static int read_thread(void *arg) { ...... #ifdef FFP_MERGE if (is->paused != is->last_paused) { is->last_paused = is->paused; if (is->paused) is->read_pause_return = av_read_pause(ic); else av_read_play(ic); } #endif // ***rts コードブロック開始*** if(rts_reload_flag){ rts_reload_flag = 0; av_log(ffp, AV_LOG_ERROR, "param == ffp_rts_reload\n"); VideoState *is = ffp->is; AVFormatContext *ic = is->ic; artc_reload(ic); } // ***rts コードブロック終了*** ...... }ijkplayer.h ファイルを変更して、`ijkmp_rts_reload` メソッドの定義を追加します。
void ijkmp_rts_reload(IjkMediaPlayer *mp);ijkplayer.c ファイルを変更して、`ijkmp_rts_reload` メソッドを実装します。
void ijkmp_rts_reload(IjkMediaPlayer *mp) { ffp_rts_reload(mp->ffplayer); }上位レイヤーに `rtsReload` インターフェイスを追加します。
ijkplayer_jni.c ファイルを変更して、`reload` インターフェイスを追加します。
//aliyun rts : ネイティブリロード static void IjkMediaPlayer_reload(JNIEnv *env,jobject thiz) { IjkMediaPlayer *mp = jni_get_media_player(env,thiz); ijkmp_rts_reload(mp); LABEL_RETURN: ijkmp_dec_ref_p(&mp); } static JNINativeMethod g_methods[] = { //aliyun rts: リロード { "_reload", "()V", (void *) IjkMediaPlayer_reload }, }IMediaPlayer.java ファイルを変更して、`reload` インターフェイスを追加します。
//aliyun rts: リロード void reload();IjkMediaPlayer.java ファイルを変更して、ネイティブの `reload` メソッドを呼び出します。
//aliyun rts : ネイティブメソッド private native void _reload(); @Override public void reload() { _reload(); }
RTS メッセージコールバックを使用してストリームのデグレードを実装する
RTS メッセージコールバックをリッスンします。
mIjkPlayer.setOnARTCMessageListener(new IMediaPlayer.OnARTCMessageListener() { @Override public void onMessage(String name, int externValue, String extraMsg) {} });RTS メッセージコールバックメソッドでストリームのデグレードロジックを実装します。
ストリームのデグレードは、プレーヤーのソース URL 文字列のプレフィックスを `artc://` から `rtmp://` または `http://xxxx.flv` に変更し、プレーヤーの URL ソースを更新してから再生を再開する戦略です。
if (SOURCE_URL.startsWith("artc://")) { mIjkPlayer.stop();// 再生を停止します。 mIjkPlayer.reset();// プレーヤーをリセットします。リセットせずに ijkplayer を再利用すると、アプリケーションがクラッシュします。 mIjkPlayer.setDataSource("http://xxx.flv"); mIjkPlayer.prepareAsync(); }再生の開始時またはライブストリーミング中に、プレーヤーのイベントコールバックが再生コンポーネントからパススルーメッセージを受信することがあります。イベント記述の JSON 文字列を解析します。`code` フィールドには、RTS SDK からのメッセージが含まれています。メッセージが `E_DNS_FAIL`、`E_AUTH_FAIL`、`E_CONN_TIMEOUT`、`E_SUB_TIMEOUT`、または `E_SUB_NO_STREAM` の場合は、ストリームをデグレードします。メッセージが `E_STREAM_BROKEN` の場合は、再生を一度リトライします。メッセージが再度受信された場合は、ストリームをデグレードします。メッセージが `E_RECV_STOP_SIGNAL` の場合は、再生を直接停止します。ストリームのデグレードは必要ありません。
// カスタム Rts 情報列挙クラス public enum RtsError { /** * DNS 解決に失敗しました。 */ E_DNS_FAIL(20001), /** * 認証に失敗しました。 */ E_AUTH_FAIL(20002), /** * 接続シグナリングがタイムアウトしました。 */ E_CONN_TIMEOUT(20011), /** * サブスクリプションシグナリングがエラーを返すか、タイムアウトしました。 */ E_SUB_TIMEOUT(20012), /** * サブスクライブしたストリームが存在しません。 */ E_SUB_NO_STREAM(20013), /** * メディアタイムアウト。音声または動画パケットが受信されませんでした。 */ E_STREAM_BROKEN(20052), /** * CDN から停止信号を受信しました。 */ E_RECV_STOP_SIGNAL(20061); private int code; RtsError(int code) { this.code = code; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } } mIjkPlayer.setOnARTCMessageListener(new IMediaPlayer.OnARTCMessageListener() { @Override public void onMessage(String name, int externValue, String msg) { if("DirectComponentMSG".equals(name)){ if (msg.contains("code=" + RtsError.E_DNS_FAIL.getCode()) || msg.contains("code=" + RtsError.E_AUTH_FAIL.getCode()) || msg.contains("code=" + RtsError.E_CONN_TIMEOUT.getCode()) || msg.contains("code=" + RtsError.E_SUB_TIMEOUT.getCode()) || msg.contains("code=" + RtsError.E_SUB_NO_STREAM.getCode()) || msg.contains("code=" + RtsError.E_STREAM_BROKEN.getCode()) || msg.contains("code=" + RtsError.E_RECV_STOP_SIGNAL.getCode())) { //TODO: デグレードロジックを実装します。 } } } });





