通过阅读本文,您可以了解iOS端依赖FFmpeg的其他播放器(本文以ijkplayer tag k0.8.8为例)集成Native RTS SDK实现超低延时直播的方法。
前提条件
您已完成ijkplayer源码的编译。具体操作,请参见ijkplayer中README.md介绍。
操作步骤
- 下载并解压ijkplayer源码。下载地址,请参见ijkplayer。
- 下载并解压Native RTS SDK。下载地址,请参见SDK下载。
- ijkplayer集成Native RTS SDK作为插件,ijkplayer集成Native RTS SDK有以下两种方式:
集成方式 描述 优点 缺点 拓展FFmpeg 拓展ijk中FFmpeg的demuxer。 使用更加简单,不需要根据ARTC的URL做逻辑区分。 需要重新编译FFmpeg库。 拓展ijk ijkplayer中添AVInputFormat。 不需要编译FFmpeg。 ff_ffplay.c中需要添加部分逻辑代码。 拓展FFmpeg- 在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
- 进入ijkplayer的iOS目录下,如果已经有编译ffmpeg命令,出现有build目录下的ffmpeg-$arch,先执行./compile-ffmpeg.sh clean,清理一遍。
- 复制Native RTS SDK中的rtsdec.c文件至ijkplayer/ios/ffmpeg-$arch/libavformat目录。
- 修改Makefile文件并编译rtsdec.c文件。
- 修改allformats.c文件。
修改ijkplayer/ios/ffmpeg-$arch/libavformat/allformats.c,默认支持ARTC协议。
- 修改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"
- 编译。
在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。
- 复制Native RTS SDK中的RtsSDK.framework文件至ijkplayer/ios/IJKMediaDemo/IJKMediaDemo目录中。
- 使用Xcode打开ios/IJKMediaDemo/IJKMediaDemo.xcodeproj。
- 添加RtsSDK.framework的依赖。
- 加入Native RTS SDK的动态库。
导入Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/ios/build/universal/lib目录中。
- 在ff_ffplay.c中添加RTS逻辑。
导入头文件rts_api.h。
#include "rts_api.h"
修改 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");
拓展ijk- 在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
- 进入ijkplayer的iOS目录下,如果已经有编译ffmpeg命令,出现有build目录下的ffmpeg-$arch,先执行./compile-ffmpeg.sh clean,清理一遍。
- 运行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"
- 编译。
在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。
- 复制Native RTS SDK中的RtsSDK.framework文件至ijkplayer/ios/IJKMediaDemo/IJKMediaDemo目录中。
- 工程中导入Native RTS SDK中的rtsdec.c文件。
工程中导入Native RTS SDK的头文件rts_api.h和rts_messages.h分别至ijkplayer/ios/build/universal/lib目录中。
- 在ff_ffplay.c中添加RTS逻辑。
导入头文件rts_api.h。
#include "rts_api.h"
修改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); }
- 在ijkplayer根目录下,执行./init-ios.sh进行初始化。
- 编译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文件。
- 导入Native RTS SDK中RtsSDK.framework和ijkplayer中IJKMediaFramework.framework文件至自定义工程中。
- 调用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;
- 开始播放。
- 创建ijkplayer
监听RTS事件
ijkplayer监听Native RTS SDK的消息回调
打开ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj工程,给相关文件添加相关适配方法。
- 修改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消息发送到上层。
- 修改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。
- 修改IJKMediaPlayback.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接口。
- 修改IJKFFMoviePlayerController.h文件,增加外部调用rtsReload方法声明。
- (void)rtsReload;
- 修改IJKFFMoviePlayerController.m文件,增加外部调用rtsReload方法实现。
- (void)rtsReload { ijkmp_rts_reload(_mediaPlayer); }
- Archive编译执行IJKMediaPlayer.xcodeproj工程,得到最新的IJKMediaFramework.framework。
- 修改IJKFFMoviePlayerController.h文件,增加外部调用rtsReload方法声明。
- 修改ff_ffplay.h文件,增加rts_reload_flag变量的定义。
调用监听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; } }
- 监听RTS消息回调。