通过阅读本文,您可以了解Android端依赖FFmpeg的其他播放器(本文以ijkplayer tag k0.8.8为例)集成Native RTS SDK实现超低延时直播的方法。
前提条件
您已完成ijkplayer源码的编译。具体操作,请参见ijkplayer中README.md介绍。
操作步骤
- 下载并解压ijkplayer源码。下载地址,请参见ijkplayer。
- 下载并解压Native RTS SDK。下载地址,请参见SDK下载。
- 修改ijk编译脚本。
- 修改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"
- 修改ijkplayer-android/init-android.sh,pull_fork的调用只保留armv7a和arm64。
- ijkplayer集成Native RTS SDK作为插件,ijkplayer集成Native RTS SDK有以下两种方式:
集成方式 描述 优点 缺点 拓展FFmpeg 拓展ijk中FFmpeg的demuxer。 使用更加简单,不需要根据ARTC的URL做逻辑区分。 需要重新编译FFmpeg库。 拓展ijk ijkplayer中添AVInputFormat。 不需要编译FFmpeg。 ff_ffplay.c中需要添加部分逻辑代码。 方式一:拓展FFmpeg- 复制Native RTS SDK中的rtsdec.c、ali_net_api.h、rts|api.h和rts_messages.h文件至ijkplayer/android/contrib/ffmpeg-arm64/libavformat和ijkplayer/android/contrib/ffmpeg-armv7a/libavformat目录下。
- 修改Makefile文件并编译rtsdec.c文件。
修改ijkplayer/android/contrib/ffmpeg-arm64/libavformat/Makefile和ijkplayer/android/contrib/ffmpeg-armv7a/libavformat/Makefile,并编译rtsdec.c文件。
- 修改allformats.c文件。
修改ijkplayer/android/contrib/ffmpeg-arm64/libavformat/allformats.c和ijkplayer/android/contrib/ffmpeg-armv7a/libavformat/allformats.c,默认支持ARTC协议。
extern AVInputFormat ff_rtc_demuxer; av_register_input_format(&ff_rtc_demuxer);
- 编译。
添加Android NDK环境变量,并在ijkplayer/android/contrib目录下执行./compile-ffmpeg.sh all运行该脚本。编译完成之后,检查并确保ijkplayer/android/contrib/build目录下有对应的FFmpeg编译输出文件。
- 加入Native RTS SDK的动态库。
复制Native RTS SDK的动态库至对应的ijkplayer/android/contrib/build/ffmpeg-arm64/output和ijkplayer/android/contrib/build/ffmpeg-armv7a/output目录中。
复制Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/android/contrib/build/ffmpeg-arm64/output/include和ijkplayer/android/contrib/build/ffmpeg-armv7a/output/include目录中。
- 引入Native RTS SDK的动态库。修改ijkplayer/android/ijkplayer/ijkplayer-armv7a/src/main/jni/ffmpeg/Android.mk和ijkplayer/android/ijkplayer/ijkplayer-arm64/src/main/jni/ffmpeg/Android.mk文件。
include $(CLEAR_VARS) LOCAL_MODULE := rtssdk LOCAL_SRC_FILES := $(MY_APP_FFMPEG_OUTPUT_PATH)/libRtsSDK.so include $(PREBUILT_SHARED_LIBRARY)
- 修改ijkplayer/ijkmedia/ijkplayer/Android.mk文件,使ijkplayer依赖Native RTS SDK动态库。
- 在ff_ffplay.c中添加RTS逻辑。
修改ijkplayer/ijkmedia/ijkplayer/ff_ffplay.c文件,设置ARTC的AVInputFormat函数指针。
extern AVInputFormat ff_rtc_demuxer; extern int artc_reload(AVFormatContext *ctx); extern void av_set_rts_demuxer_funcs(const struct rts_glue_funcs *funcs); extern void artc_set_rts_param(char* key, char* value); extern long long artc_get_state(AVFormatContext *ctx, int key); int version = 2; const struct rts_glue_funcs* rts_funcs = get_rts_funcs(version); // set to ffmpeg plugin av_set_rts_demuxer_funcs(rts_funcs); artc_set_rts_param((char*)"AutoReconnect", (char*)"false");
- 编译。
在ijkplayer/android目录下执行./compile-ijk.sh all运行该脚本。
方式二:拓展ijk- 加入Native RTS SDK的动态库。
复制Native RTS SDK的动态库至对应的ijkplayer/android/contrib/build/ffmpeg-arm64/output和ijkplayer/android/contrib/build/ffmpeg-armv7a/output目录中。
复制Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/android/contrib/build/ffmpeg-arm64/output/include和ijkplayer/android/contrib/build/ffmpeg-armv7a/output/include目录中。
- 引入Native RTS SDK的动态库。修改ijkplayer/android/ijkplayer/ijkplayer-armv7a/src/main/jni/ffmpeg/Android.mk和ijkplayer/android/ijkplayer/ijkplayer-arm64/src/main/jni/ffmpeg/Android.mk文件。
include $(CLEAR_VARS) LOCAL_MODULE := rtssdk LOCAL_SRC_FILES := $(MY_APP_FFMPEG_OUTPUT_PATH)/libRtsSDK.so include $(PREBUILT_SHARED_LIBRARY)
- 修改ijkplayer/ijkmedia/ijkplayer/Android.mk文件,使ijkplayer依赖Native RTS SDK动态库。
- 在ff_ffplay.c中添加RTS逻辑。
修改ijkplayer/ijkmedia/ijkplayer/ff_ffplay.c文件,设置ARTC的AVInputFormat函数指针。
if(strncmp(is->filename, "artc://", 7) == 0) { extern AVInputFormat ff_rtc_demuxer; extern int artc_reload(AVFormatContext *ctx); extern void av_set_rts_demuxer_funcs(const struct rts_glue_funcs *funcs); extern void artc_set_rts_param(char* key, char* value); extern long long artc_get_state(AVFormatContext *ctx, int key); int version = 2; const struct rts_glue_funcs* rts_funcs = get_rts_funcs(version); // set to ffmpeg plugin av_set_rts_demuxer_funcs(rts_funcs); artc_set_rts_param((char*)"AutoReconnect", (char*)"false"); is->iformat = &ff_rtc_demuxer; } else { if(ffp->iformat_name) is->iformat = av_find_input_format(ffp->iformat_name); }
- 复制Native RTS SDK中的rtsdec.c文件至ijkplayer/ijkmedia/ijkplayer目录下,并修改该目录中android.mk文件添加rtsdec.c文件的编译描述。
LOCAL_SRC_FILES += rtsdec.c
- 用户工程集成ijk。
- 调用ijkplayer接口实现超低延时直播功能。
- 创建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://<播流地址>");
- 状态控制
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(); } }
- 创建ijkplayer
监听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:receive artc message 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:send message to app 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文件,在
static int is_realtime(AVFormatContext *s){}
方法中插入如下RTS代码块。static int is_realtime(AVFormatContext *s) { if( !strcmp(s->iformat->name, "rtp") || !strcmp(s->iformat->name, "rtsp") || !strcmp(s->iformat->name, "sdp") // ***rts代码块 begin*** || !strcmp(s->iformat->name, "artc") // ***rts代码块 end*** ) return 1; if(s->pb && ( !strncmp(s->filename, "rtp:", 4) || !strncmp(s->filename, "udp:", 4) ) ) return 1; return 0;
- 修改ff_ffplay.c文件,在
static int read_thread(void *arg){}
方法内插入如下RTS代码块。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代码块 begin*** ic->opaque = ffp; ic->control_message_cb = onArtcDemuxerMessage; // ***rts代码块 end*** ...... 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代码块 begin*** 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代码块 end*** ...... pkt->flags = 0; // ***rts代码块 begin*** 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 ) { // accelerate 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) { // restore speed 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代码块 end*** ...... }
- 修改ff_ffmsg.h文件,增加RTS消息的接口声明。
#define FFP_MSG_ARTC_DIRECTCOMPONENTMSG 3000
- 将RTS消息发送到上层。
- 修改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: post artc event 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:post event 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; } } }
在IMediaPlayer.java增加RTS消息回调。//aliyun rts:artc message callback interface OnARTCMessageListener{ void onMessage(String name,int externValue,String extraMsg); }
在AbstractMediaPlayer.java中增加监听设置。//aliyun rts : set artc message listener public final void setOnARTCMessageListener(OnARTCMessageListener listener){ this.mOnARTCMessageListener = listener; } //aliyun rts : call back protected final void notifyOnARTCMessage(String name,int externValue,String extraMsg){ if(mOnARTCMessageListener != null){ mOnARTCMessageListener.onMessage(name,externValue,extraMsg); } }
执行./compile-ijk.sh重新编译即可。
- 修改ijkplayer_android_def.h增加RTS消息枚举类型。
- 修改ff_ffplay.c文件,在
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文件,在
static int read_thread(void *arg){}
方法内插入如下RTS代码块。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代码块 begin*** 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代码块 end*** ...... }
- 修改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 : reload native 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 { "_reload", "()V", (void *) IjkMediaPlayer_reload }, }
- 修改IMediaPlayer.java文件,增加reload接口。
//aliyun rts: reload void reload();
- 修改IjkMediaPlayer.java,调用native reload。
//aliyun rts : native method private native void _reload(); @Override public void reload() { _reload(); }
- 修改ijkplayer_jni.c增加reload接口。
- 修改ff_ffplay.h文件,增加rts_reload_flag变量的定义。
调用监听RTS消息回调方法实现直播降级
- 监听RTS消息回调。
mIjkPlayer.setOnARTCMessageListener(new IMediaPlayer.OnARTCMessageListener() { @Override public void onMessage(String name, int externValue, String extraMsg) {} });
- 监听RTS消息回调方法中实现直播降级逻辑。
直播降级是将前缀为artc://的播放器url源字符串直接修改为rtmp://的前缀或者改为http://xxxx.flv的形式,然后更新播放器UrlSource,并开始播放的策略。
if (SOURCE_URL.startsWith("artc://")) { mIjkPlayer.stop();//停止 mIjkPlayer.reset();//重置,ijk 复用时,不重置会 crash mIjkPlayer.setDataSource("http://xxx.flv"); mIjkPlayer.prepareAsync(); }
启播或者直播过程中,播放器事件回调收到播放组件中透传输出的消息时,解析播放器事件说明JSON字符串,得到的code是RtsSDK中的message。收到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的stop信令 */ 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 执行降级逻辑 } } } });
- 监听RTS消息回调。