全部產品
Search
文件中心

:iOS端

更新時間:Jun 30, 2024

通過閱讀本文,您可以瞭解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;
        }
      }