Android端

通过阅读本文,您可以了解Android端依赖FFmpeg的其他播放器(本文以ijkplayer tag k0.8.8为例)集成Native RTS SDK实现超低延时直播的方法。

前提条件

您已完成ijkplayer源码的编译。具体操作,请参见ijkplayer中README.md介绍。

操作步骤

  1. 下载并解压ijkplayer源码。下载地址,请参见ijkplayer

  2. 下载并解压Native RTS SDK。下载地址,请参见SDK下载

  3. 修改ijk编译脚本。

    • 修改ijkplayer-android/init-android.sh,pull_fork的调用只保留armv7a和arm64。init-android.sh

    • 修改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"
  4. ijkplayer集成Native RTS SDK作为插件,ijkplayer集成Native RTS SDK有以下两种方式:

    集成方式

    描述

    优点

    缺点

    拓展FFmpeg

    拓展ijk中FFmpeg的demuxer。

    使用更加简单,不需要根据ARTC的URL做逻辑区分。

    需要重新编译FFmpeg库。

    拓展ijk

    ijkplayer中添AVInputFormat。

    不需要编译FFmpeg。

    ff_ffplay.c中需要添加部分逻辑代码。

    方式一:拓展FFmpeg

    1. 复制Native RTS SDK中的rtsdec.c、ali_net_api.h、rts_api.h和rts_messages.h文件至ijkplayer/android/contrib/ffmpeg-arm64/libavformatijkplayer/android/contrib/ffmpeg-armv7a/libavformat目录下。

    2. 修改Makefile文件并编译rtsdec.c文件。

      修改ijkplayer/android/contrib/ffmpeg-arm64/libavformat/Makefileijkplayer/android/contrib/ffmpeg-armv7a/libavformat/Makefile,并编译rtsdec.c文件。

      rtsdec.c

    3. 修改allformats.c文件。

      修改ijkplayer/android/contrib/ffmpeg-arm64/libavformat/allformats.cijkplayer/android/contrib/ffmpeg-armv7a/libavformat/allformats.c,默认支持ARTC协议。

      allformats.c

          extern AVInputFormat ff_rtc_demuxer;
          av_register_input_format(&ff_rtc_demuxer);
    4. 编译。

      添加Android NDK环境变量,并在ijkplayer/android/contrib目录下执行./compile-ffmpeg.sh all运行该脚本。编译完成之后,检查并确保ijkplayer/android/contrib/build目录下有对应的FFmpeg编译输出文件。

    5. 加入Native RTS SDK的动态库。

      复制Native RTS SDK的动态库至对应的ijkplayer/android/contrib/build/ffmpeg-arm64/outputijkplayer/android/contrib/build/ffmpeg-armv7a/output目录中。

      复制Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/android/contrib/build/ffmpeg-arm64/output/includeijkplayer/android/contrib/build/ffmpeg-armv7a/output/include目录中。

    6. 引入Native RTS SDK的动态库。

      修改ijkplayer/android/ijkplayer/ijkplayer-armv7a/src/main/jni/ffmpeg/Android.mkijkplayer/android/ijkplayer/ijkplayer-arm64/src/main/jni/ffmpeg/Android.mk文件。018

      include $(CLEAR_VARS)
      LOCAL_MODULE := rtssdk
      LOCAL_SRC_FILES := $(MY_APP_FFMPEG_OUTPUT_PATH)/libRtsSDK.so
      include $(PREBUILT_SHARED_LIBRARY)
    7. 修改ijkplayer/ijkmedia/ijkplayer/Android.mk文件,使ijkplayer依赖Native RTS SDK动态库。019

    8. 在ff_ffplay.c中添加RTS逻辑。

      修改ijkplayer/ijkmedia/ijkplayer/ff_ffplay.c文件,设置ARTC的AVInputFormat函数指针。

      021

          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");
    9. 编译。

      ijkplayer/android目录下执行./compile-ijk.sh all运行该脚本。

    方式二:拓展ijk

    1. 加入Native RTS SDK的动态库。

      复制Native RTS SDK的动态库至对应的ijkplayer/android/contrib/build/ffmpeg-arm64/outputijkplayer/android/contrib/build/ffmpeg-armv7a/output目录中。

      复制Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/android/contrib/build/ffmpeg-arm64/output/includeijkplayer/android/contrib/build/ffmpeg-armv7a/output/include目录中。

    2. 引入Native RTS SDK的动态库。

      修改ijkplayer/android/ijkplayer/ijkplayer-armv7a/src/main/jni/ffmpeg/Android.mkijkplayer/android/ijkplayer/ijkplayer-arm64/src/main/jni/ffmpeg/Android.mk文件。018

      include $(CLEAR_VARS)
      LOCAL_MODULE := rtssdk
      LOCAL_SRC_FILES := $(MY_APP_FFMPEG_OUTPUT_PATH)/libRtsSDK.so
      include $(PREBUILT_SHARED_LIBRARY)
    3. 修改ijkplayer/ijkmedia/ijkplayer/Android.mk文件,使ijkplayer依赖Native RTS SDK动态库。019

    4. 在ff_ffplay.c中添加RTS逻辑。

      修改ijkplayer/ijkmedia/ijkplayer/ff_ffplay.c文件,设置ARTC的AVInputFormat函数指针。

      053

      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);
          }
    5. 复制Native RTS SDK中的rtsdec.c文件至ijkplayer/ijkmedia/ijkplayer目录下,并修改该目录中android.mk文件添加rtsdec.c文件的编译描述。

      LOCAL_SRC_FILES += rtsdec.c
  5. 用户工程集成ijk。

    1. 复制Native RTS SDK中的RtsNetSDK.jar文件至工程中。

    2. 在使用RTS播放的Activity内引入Native RTS SDK动态库。

      static {
          System.loadLibrary("RtsSDK");
      }
    3. 导入ijkplayer-arm64、ijkplayer-armv7a、ijkplayer-java模块。

      060

  6. 调用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();
          }
      }

监听RTS事件

  • ijkplayer监听Native RTS SDK的消息回调

    打开ijkplayer/android/ijkplayer项目。

    1. 修改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;
      }
    2. 修改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;
       }                        
    3. 修改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***
        ......
      }
    4. 修改ff_ffmsg.h文件,增加RTS消息的接口声明。

      #define FFP_MSG_ARTC_DIRECTCOMPONENTMSG     3000
    5. 将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增加执行Native RTS SDK的重试方法

    1. 修改ff_ffplay.h文件,增加rts_reload_flag变量的定义。

      int       rts_reload_flag;
    2. 修改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;
         }
      }
    3. 修改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***
         ......
      }
    4. 修改ijkplayer.h文件,增加ijkmp_rts_reload方法定义。

      void            ijkmp_rts_reload(IjkMediaPlayer *mp);
    5. 修改ijkplayer.c文件,实现ijkmp_rts_reload方法。

      void ijkmp_rts_reload(IjkMediaPlayer *mp)
      {
          ffp_rts_reload(mp->ffplayer);
      }
    6. 上层增加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();
            }
  • 调用监听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 执行降级逻辑
                  }
              }
          }
      });