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

前提条件

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

操作步骤

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

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

  3. ijkplayer集成Native RTS SDK作为插件,ijkplayer集成Native RTS SDK有以下两种方式:

    集成方式

    描述

    优点

    缺点

    拓展FFmpeg

    拓展ijk中FFmpeg的demuxer。

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

    需要重新编译FFmpeg库。

    拓展ijk

    ijkplayer中添AVInputFormat。

    不需要编译FFmpeg。

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

    拓展FFmpeg

    1. 在ijkplayer根目录下,执行./init-ios.sh进行初始化。

      需注意Xcode14版本已不支持armv7、i386、x86_64等架构,需要在init-ios.sh文件中提前去除相关架构。

      以Xcode14编译为例:

      # FF_ALL_ARCHS_IOS8_SDK="armv7 arm64 i386 x86_64"
      FF_ALL_ARCHS_IOS11_SDK="arm64"
      FF_ALL_ARCHS=$FF_ALL_ARCHS_IOS11_SDK
    2. 进入ijkplayer的iOS目录下,如果已经有编译ffmpeg命令,出现有build目录下的ffmpeg-$arch,先执行./compile-ffmpeg.sh clean,清理一遍。

    3. 复制Native RTS SDK中的rtsdec.c文件至ijkplayer/ios/ffmpeg-$arch/libavformat目录。

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

    5. 修改allformats.c文件。

      修改ijkplayer/ios/ffmpeg-$arch/libavformat/allformats.c,默认支持ARTC协议。

      016

    6. 修改FFmpeg编译脚本ijkplayer/config/module-lite.sh,使其支持PCM解码(Native RTS SDK输出为PCM数据)。

      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"

      可选:如果想支持HTTPS协议的流,需要同时在文件中添加openssl支持。

      export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-openssl"
    7. 编译。

      ijkplayer/ios目录下,执行./compile-ffmpeg.sh all运行该脚本,同时可以参考a步骤在脚本中提前去除相关架构。编译完成之后,检查并确保ijkplayer/ios/build/universal目录下有对应的FFmpeg编译输出文件。

      可选:如果想支持HTTPS协议的流,需要先执行./compile-openssl.sh all运行该脚本,同时可以参考a步骤在脚本中提前去除相关架构,编译完成后,会在ijkplayer/ios/build/universal/lib生成libssl.a和libcrypto.a两个文件。然后再编译./compile-ffmpeg.sh all

    8. 复制Native RTS SDK中的RtsSDK.framework文件至ijkplayer/ios/IJKMediaDemo/IJKMediaDemo目录中。

    9. 使用Xcode打开ios/IJKMediaDemo/IJKMediaDemo.xcodeproj

    10. 添加RtsSDK.framework的依赖。264

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

      导入Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/ios/build/universal/lib目录中。

      rts_api.h

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

      导入头文件rts_api.h。

      #include "rts_api.h"

      修改ijkplayer/ijkmedia/ijkplayer/ff_ffplay.c文件,设置ARTC的AVInputFormat函数指针。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");

    拓展ijk

    1. 在ijkplayer根目录下,执行./init-ios.sh进行初始化。

      需注意Xcode14版本已不支持armv7、i386、x86_64等架构,需要在init-ios.sh文件中提前去除相关架构。

      以Xcode14编译为例:

      # FF_ALL_ARCHS_IOS8_SDK="armv7 arm64 i386 x86_64"
      FF_ALL_ARCHS_IOS11_SDK="arm64"
      FF_ALL_ARCHS=$FF_ALL_ARCHS_IOS11_SDK
    2. 进入ijkplayer的iOS目录下,如果已经有编译ffmpeg命令,出现有build目录下的ffmpeg-$arch,先执行./compile-ffmpeg.sh clean,清理一遍。

    3. 运行FFmpeg编译脚本ijkplayer/config/module-lite.sh,使其支持PCM解码(Native RTS SDK输出为PCM数据)。

      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"

      可选:如果想支持HTTPS协议的流,需要同时在文件中添加openssl支持。

      export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-openssl"
    4. 编译。

      在ijkplayer/ios目录下,执行./compile-ffmpeg.sh all运行该脚本,同时可以参考a步骤在脚本中提前去除相关架构。编译完成之后,检查并确保ijkplayer/ios/build/universal目录下有对应的FFmpeg编译输出文件。

      可选:如果想支持HTTPS协议的流,需要先执行./compile-openssl.sh all运行该脚本,同时可以参考a步骤在脚本中提前去除相关架构,编译完成后,会在ijkplayer/ios/build/universal/lib生成libssl.a和libcrypto.a两个文件。然后再编译./compile-ffmpeg.sh all

    5. 复制Native RTS SDK中的RtsSDK.framework文件至ijkplayer/ios/IJKMediaDemo/IJKMediaDemo目录中。

    6. 工程中导入Native RTS SDK中的rtsdec.c文件。270

      工程中导入Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/ios/build/universal/lib目录中。

      rts_api.h

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

      导入头文件rts_api.h。

      #include "rts_api.h"

      修改ijkplayer/ijkmedia/ijkplayer/ff_ffplay.c文件,设置ARTC的AVInputFormat函数指针。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);
      }
  4. 编译IJKMediaPlayer工程。

    完成“拓展FFmpeg”或"拓展ijk"其中一个操作后,可以直接使用Archive编译执行ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj工程,最终在Products下生成IJKMediaFramework.framework 文件。

    可选:如果想支持HTTPS协议的流,需要在编译执行前,在工程的Build Phases的Link Binary With Libraries中导入前面生成的ijkplayer/ios/build/universal/lib下的libssl.a和libcrypto.a文件。

    IJKMediaPlayer

  5. 导入Native RTS SDK中RtsSDK.framework和ijkplayer中IJKMediaFramework.framework文件至自定义工程中。

    273

  6. 调用ijkplayer接口实现超低延时直播功能。

    • 创建ijkplayer

      //生成artc协议url
      _url = @"artc://xxxx";
      
      //创建自定义playerView
      [self.view addSubview:self.playerView];
      ...
      
      IJKFFOptions *options = [IJKFFOptions optionsByDefault];
      //硬解码
      [options setPlayerOptionIntValue:1 forKey:@"videotoolbox"];
      //软解码
      //[options setPlayerOptionIntValue:0 forKey:@"videotoolbox"];
      
      _ijkPlayer = [[IJKFFMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:_url] withOptions:options];
      _ijkPlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
      _ijkPlayer.view.frame = self.playerView.bounds;
      _ijkPlayer.scalingMode = IJKMPMovieScalingModeAspectFit; //缩放模式
      _ijkPlayer.shouldAutoplay = YES; //开启自动播放
      [self.playerView addSubview:_ijkPlayer.view];
    • 播放控制

      • 开始播放。

        开始播放需要在主线程中调用。每次重新开始播放前,建议主动调用下停止播放和释放播放器的流程。

        dispatch_async(dispatch_get_main_queue(), ^{
            [_ijkPlayer prepareToPlay];
            [_ijkPlayer play];
        });
      • 停止播放和释放播放器。

        // 停止播放
        [_ijkPlayer stop];
        
        // 释放播放器
        [_ijkPlayer shutdown];
        _ijkPlayer = nil;

监听RTS事件

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

    打开ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj工程,给相关文件添加相关适配方法。

    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消息发送到上层。

      • 修改IJKMediaPlayback.h文件,增加外部调用RTS消息监听名字声明。

        IJK_EXTERN NSString *const IJKMPMoviePlayerRtsMsgNotification;
      • 修改IJKMediaPlayback.m文件,增加外部调用RTS消息监听名字定义。

        NSString *const IJKMPMoviePlayerRtsMsgNotification = @"IJKMPMoviePlayerRtsMsgNotification";
      • 修改IJKFFMoviePlayerController.m文件,postEvent:方法内增加RTS消息监听的处理。

        - (void)postEvent: (IJKFFMoviePlayerMessage *)msg
        {
            ......
          case FFP_MSG_ARTC_DIRECTCOMPONENTMSG:{
             NSString *rtsMsg = [[NSString alloc] initWithUTF8String:avmsg->obj];
             int type = avmsg->arg1;
             if (!rtsMsg) {
                rtsMsg = @"";
             }
             NSDictionary *dic = @{@"type":@(type),@"msg":rtsMsg};
             [[NSNotificationCenter defaultCenter] postNotificationName:IJKMPMoviePlayerRtsMsgNotification
                            object:dic];
             break;
            }
           default:
             // NSLog(@"unknown FFP_MSG_xxx(%d)\n", avmsg->what);
             break;
        }
      • Archive编译执行IJKMediaPlayer.xcodeproj工程,得到最新的IJKMediaFramework.framework。

  • 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接口。

      • 修改IJKFFMoviePlayerController.h文件,增加外部调用rtsReload方法声明。

        - (void)rtsReload;
      • 修改IJKFFMoviePlayerController.m文件,增加外部调用rtsReload方法实现。

        - (void)rtsReload {
            ijkmp_rts_reload(_mediaPlayer);
        }
      • Archive编译执行IJKMediaPlayer.xcodeproj工程,得到最新的IJKMediaFramework.framework。

  • 调用监听RTS消息回调方法实现直播降级

    • 监听RTS消息回调。

      [[NSNotificationCenter defaultCenter] addObserver:self
                                               selector:@selector(reviceMsg:)
                                                   name:IJKMPMoviePlayerRtsMsgNotification
                                                 object:nil];
    • 监听RTS消息回调方法中实现直播降级逻辑。

      直播降级是将前缀为artc://的播放器url源字符串直接修改为rtmp://的前缀或者改为http://xxxx.flv的形式,然后更新播放器UrlSource,并开始播放的策略。

      // 直播降级,传统直播URL,如http://xxx.flv、rtmp://xxx等
      - (void)convertArtcToRtmpPlay {
          // 获取当前的播放url,截取url的前缀
          NSArray *urlSeparated = [self.url componentsSeparatedByString:@"://"];
          NSString *urlPrefix = urlSeparated.firstObject;
          // 判断url前缀是否是artc,如果是的话就降级为传统直播
          if ([urlPrefix isEqualToString:@"artc"]) {
              // http://xxx.flv 建议使用
              _url = [[@"http://" stringByAppendingString:urlSeparated.lastObject] stringByAppendingString:@".flv"];
              // rtmp://xxx
              // _url = [@"rtmp://" stringByAppendingString:urlSeparated.lastObject];
      
                  // 停止播放并销毁播放器
            [_ijkPlayer stop];
            [_ijkPlayer shutdown];
            _ijkPlayer = nil;
      
            // 重新设置播放源
            _ijkPlayer = [[IJKFFMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:_url] withOptions:options];
            ......
                  // 进行播放
            dispatch_async(dispatch_get_main_queue(), ^{
                      [_ijkPlayer prepareToPlay];
                      [_ijkPlayer play];
                  });
          }
      }

      需要提前导入RtsSDK的API。

      #import <RtsSDK/rts_messages.h>

      然后处理播放器事件回调。

      -(void)reviceMsg:(NSNotification*)notification{
          NSDictionary *dic = [notification object];
        NSNumber *type = dic[@"type"];
        switch (type.intValue) {
          case E_DNS_FAIL:
          case E_AUTH_FAIL:
          case E_CONN_TIMEOUT:
          case E_SUB_TIMEOUT:
          case E_SUB_NO_STREAM:
          {
            // 直播降级
            [self convertArtcToRtmpPlay];
          }
              break;
          case E_STREAM_BROKEN:
          {
             static BOOL retryStartPlay = YES;
             // 第一次收到RTS媒体超时先重试播放一次,然后如果再次收到就直接降级播放
             if (retryStartPlay) {
                 dispatch_async(dispatch_get_main_queue(), ^{
                   [_ijkPlayer rtsReload];
                   });
                 retryStartPlay = NO;
              } else {
                 // 直播降级
                 [self convertArtcToRtmpPlay];
              }
           }
            break;
          case E_RECV_STOP_SIGNAL:
                  {
                // 停止播放并销毁播放器
                [_ijkPlayer stop];
              [_ijkPlayer shutdown];
              _ijkPlayer = nil;
            }
            break;
          default:
            break;
        }
      }