iOS视频直播笔记

最近在做视频和直播业务,整理成文章和demo,记录一下开发心得
因为直播已经火了好几年了,移动端的直播 的核心功能其实已经很成熟了,大多集成一些第三方sdk或开源项目就可以实现,但是直播涉及技术点多,比如播放器,IM即时通讯,弹幕,礼物等,如果有推流还要搭建推流端,难点在于这些技术相互关联,做到不卡顿,流畅度好。
本文demoZBLiveRoom
包含 直播间, 抖音短视频,广告+正片,视频历史弹幕绑定,以后还会陆续更新
直播间

livegif.gif

抖音
douyingif.gif

广告+正片
adgif.gif

视频历史弹幕绑定
danmugif.gif

播放器

首先说下 播放器的选择,因为我们服务器选择的阿里云 做直播推流相关服务,我第一时间也选择的阿里云播放器,尝试集成了一下,就一字 坑,阿里云播放器,真的是难用,看里面的代码,应该是换了好几个人维护,居然有注释 这个属性是干嘛的,自己的人都看不懂,我们怎么玩 。同时安卓同事也反馈,太难用了,我们果断换。接下来研究了ijkplayer,这个也是iOS用的比较广泛的,很多坑前人基本都踩没了,控制层也有很多出名的比如ZFPlayer结合ijkplayer
,正好我们app 直播和视频 都需要支持。但是就一点ijkplayer 太大了,最新版500多M ,git根本传不上去,也找了很多方案,基本上就是把ijkplayer放在本地,不传git。这个在多人开发的团队还是不太方便的。而且打完包确实大了不少,所以又舍弃了。后来我又调研了其他一些播放器,都感觉不太满意,最后在同事的推荐下,找到了 SuperPlayer_iOS,是腾讯对自家的TXLiteAVSDK_Player 的一个封装,还是比较满意的,整个sdk大小100M多点,看打印的log ,TXLiteAVSDK_Player 底层也应该是对ijkplayer的封装,
SuperPlayer_iOS 当然也有缺点,最不能让人忍受的就是横屏的问题,SuperPlayer_iOS的横屏是一个假横屏,用View做的方向改变,而控制器还是竖屏,会造成很多需求有问题,比如我们在横屏时想弹出键盘,你会发现是竖屏键盘,很坑,所有如果你的项目对横屏有类似业务的,还是不要使用SuperPlayer_iOS,推荐ZFPlayer,后期我们播放视频业务已经切换到了ZFPlayer

当然每个app,都是要单独设计UI控制层的,SuperPlayer_iOS 虽然支持自定义控制层,但是有些地方的UI或控制,它还是无法自定义,所以不能用cocoapods 去管理,pod install一下,所有更改恢复原样了,当然我这个demo 还是用的 cocoapods集成的,如果如果SuperPlayer_iOS 的默认控制层,或自定义控制层,能满足你的需求,直接pod 'SuperPlayer'就可以了

如果不能满足手动复制了 SuperPlayer_iOS 所有代码
pod 'TXLiteAVSDK_Player'
pod 'MMLayout'
//下面这俩个项目里已经有,就不用添加了
//pod 'SDWebImage'
//pod 'AFNetworking'
创建播放器

#import <SuperPlayer/SuperPlayer.h>

    _playerView = [[SuperPlayerView alloc] init];
    _playerView.delegate = self;
    _playerView.playerConfig.enableLog=NO;
    self.playerFatherView = [[UIView alloc] init];
    self.playerFatherView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:self.playerFatherView];
    [self.playerFatherView mas_makeConstraints:^(MASConstraintMaker *make) {
        if (@available(iOS 11.0, *)) {
            make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
        } else {
            make.top.mas_equalTo(20+self.navigationController.navigationBar.bounds.size.height);
        }
        make.leading.trailing.mas_equalTo(0);
        make.height.mas_equalTo(self.playerFatherView.mas_width).multipliedBy(9.0f/16.0f);// 这里宽高比16:9,可自定义宽高比
    }];
    _playerView.fatherView =_playerFatherView;// 设置父视图

播放时建议使用真机,模拟器会有很多问题。
在播放器 播放时 一般都会有移动网络 提示 ,如果是移动网络 提示一下,如果是wifi 直接播放

 if ([self isNetworkWiFi]==NO) {
       //移动网络提示ui
 }else{ 
    SuperPlayerModel *playerModel = [[SuperPlayerModel alloc] init];
    /**
     坑一
     模拟器播放 flv格式 画面红色,有声音,真机没有问题。
     因为SuperPlayerViewConfig 内的hwAcceleration 方法 ,模拟器默认硬解码为默认 NO造成的 ,
    坑二
    模拟器 播放mp4格式视频 帧数很低 20-30fps,使用真机帧数恢复正常猜测还是解码的问题,直播 基本57-60fps 使用真机正常
     */
    playerModel.videoURL=@"http://URL";
    [_playerView.coverImageView sd_setImageWithURL:[NSURL URLWithString:@"http://1252463788.vod2.myqcloud.com/e12fcc4dvodgzp1252463788/28742df34564972819219071568/4564972819209692959.jpeg"]];
    [_playerView playWithModel:playerModel];
}

移动端播放直播流格式 一般推荐用 flv的


4FCEED16-1B05-425A-BC7D-25D78A30A634.png

聊天室

聊天室 分两部分吧
一、IM的通信服务,我们并没有使用第三方服务,长链接由自家服务器开发,因为我们pc端也要做直播业务,而pc端支持只支持WebSocket协议,iOS实现WebSocke通信,我们选择了facebook的SocketRocket,SocketRocket非常完美,没什么可说的,唯一的缺点可能就是好几年不更新了。

另外长链接的保活机制需要注意几点,大约逻辑就是
连接失败,可以实现掉线自动重连
1.判断当前网络环境,如果断网了就不要连了,等待网络到来,在发起重连;
2.判断调用层是否需要连接,例如用户都没在聊天界面,连接上去浪费流量

二、聊天室,参考了下面这两位大神的文章
翻炒吧蛋滚饭:直播中聊天室踩过的坑以及我的填坑历程
大怪猿:iOS直播间聊天室—图文混排加载网络图片
有面向对象开发,面向过程开发,面向bug开发,那么我就是面向大神开发
聊天室ui,是tableView搭建的
主要就是大量数据的时候,如何保证直播间流程,不卡顿,对整个项目的性能没有影响。
归纳总结了几个优化重点:

  1. 添加数据时 ,不要使用[self.tableView reloadData];,使用单行刷新,
    当数量大的时候,这个真的很明显。
  2. 对于ui不同聊天数据,要使用不同cell
  3. cell的高度缓存
  4. 在聊天数据大于一定量时,需要删除前面的部分数据,已保证不卡顿,
    这个我做过实验,iphone 6在1400多条数据的时候在不断的添加数据,不管用哪种方式刷新tableView,就会明显的卡顿,应该内存方面的问题了
  5. 使用缓存数据源,
    当接收到数据,不要直接刷新,而是给缓存数据源,当达到条件后,在把缓存数据给正式数据源
  6. 在适当的时候刷新tableView,
    比如我的播放器横屏了,比如我滑动聊天列表,并没有在聊天室的最下边,其实都是不用去刷新tableView的
  7. 无限刷新 改为固定时间刷新
    在聊天数据量非常大的时候,比如1秒钟好几十条,那tableView就会刷新好几十次,这太可怕了,所以可以使用固定时间刷新tableView,因为有缓存数据源,我们可以加逻辑,比如每1秒钟把缓存数据源添加到正式数据,并刷新tableView
    做了以上的优化后,聊天室基本不会卡顿了

弹幕

弹幕使用了OCBarrage,这个真的是很优秀的开源项目了。
以下是相关代码

//懒加载
- (OCBarrageManager *)barrageManager{
    if (!_barrageManager) {
         _barrageManager = [[OCBarrageManager alloc] init];
        _barrageManager.renderView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    }
    return _barrageManager;
}
//添加到播放器上
 [self.playerView addSubview:self.barrageManager.renderView];
 [self.playerView sendSubviewToBack:self.barrageManager.renderView];//防止挡住控制层
 [self.barrageManager.renderView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.equalTo(self.playerView);
        make.top.equalTo(@(20));
        make.bottom.equalTo(self.playerView.mas_bottom).offset(-20);
 }];
//配置每一条弹幕 并发送
- (void)sendbarrage:(NSString *)str{
     OCBarrageTextDescriptor *textDescriptor = [[OCBarrageTextDescriptor alloc] init];
     textDescriptor.text = str;
     textDescriptor.textColor = kRandomColor;
//    CGFloat bannerHeight = 185.0/2.0;
     textDescriptor.renderRange = NSMakeRange(0,100)
     textDescriptor.positionPriority = OCBarragePositionLow;
     textDescriptor.textFont = [UIFont systemFontOfSize:14];
     textDescriptor.strokeColor = [[UIColor blackColor] colorWithAlphaComponent:0.3];
     textDescriptor.strokeWidth = -1;
     textDescriptor.animationDuration = arc4random()%5 + 10;
     textDescriptor.barrageCellClass = [OCBarrageTextCell class];
    self.barrageManager.renderView.renderPositionStyle=OCBarrageRenderPositionIncrease;
     [self.barrageManager renderBarrageDescriptor:textDescriptor];
}

但是有一个业务需求,我们不止有直播,还有很多视频,视频播放历史弹幕,这个OCBarrage并没有现成的方法,
iOS还有个开源弹幕BarrageRenderer,这个是有视频时间绑定弹幕的功能,但是我试了试,有很多bug,最终还是放弃了。
作为面向大神开发的我,今天要雄起了,自己实现一个弹幕队列的功能

弹幕队列

服务器下发历史弹幕格式 基本大约是这样的.弹幕的时间 和弹幕的文本内容

[
    {
    "seconds": "1",//弹幕时间(秒)
    "barrage": "大家好",//弹幕内容
    },
    {
    "seconds": "40",//弹幕时间(秒)
    "barrage": "风力雨里,我在评论区等你",//弹幕内容
    },
   {
    "seconds": "40",//弹幕时间(秒)
    "barrage": "这个内容我喜欢",//弹幕内容
    },
    {
    "seconds": "111",//弹幕时间(秒)
    "barrage": "我是andi",//弹幕内容
    },
]

基本思路就是把 弹幕以弹幕时间为key 文本为Value 存在字典里,这个存还是有些讲究的,可以看到上面在40秒的时候同时存在两个弹幕,这种情况其实很常见,火的视频每秒甚至10多条,所以存的时候弹幕时间为key,而Value就需要是一个同一时间文本的数组, 之后在取也是取的数组。
取弹幕 有两种方式

  1. 在视频当前时间回调取(推荐)
    这个是 视频走到哪,根据视频的当前时间取对应的弹幕,播放和暂停也没有操作。
  2. 使用计时器
    在视频的开始播放时,就开启计时器,每一秒获取一次 当前视频播放的时间,根据获取的时间取对应的弹幕,暂停时需要关闭计时器,播放需要开启计时器,

下面使用ZFPlayer 播放视频 并绑定历史弹幕,

self.barrageQueue=[[ZBBarrageQueue alloc]init];
 self.barrageQueue.delegate=self;
//加载弹幕数据
  [barbrageArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * _Nonnull stop) {
        ZBBarrageModel *model=[[ZBBarrageModel alloc]init];
        model.text=obj[@"barrage"];
        model.seconds=[obj[@"seconds"]integerValue];
        [listArray addObject:model];
  }];
  [self.barrageQueue loadBarrageList:listArray];

 __block NSInteger tempTime;
 self.player.playerPlayTimeChanged = ^(id<ZFPlayerMediaPlayback>  _Nonnull asset, NSTimeInterval currentTime, NSTimeInterval duration) {
        @strongify(self)
        //因为此回调是0.1秒一次,所以做了此判断,
        tempTime=currentTime;
        if (self.currentTime!=tempTime) {
            //弹幕列队 和 视频时间绑定
            [self.barrageQueue startQueueWithCurrentTime:tempTime];
        }
        self.currentTime=tempTime;
  };
#pragma mark - 弹幕队列代理
- (void)barrageQueueGetTextArray:(NSArray *)textArray{
    [textArray enumerateObjectsUsingBlock:^(NSString *text, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"第%ld元素 发送的弹幕 %@",idx,text);
        [self sendbarrage:text isMe:NO];
    }];
}

当然取弹幕队列其实还有一些要完善的问题,
1.比如滑动进度条回退视频会重复加载弹幕,而有时可能就回退几秒钟,当前弹幕还在屏幕显示,怎么去重。

礼物

因为我们项目,目前并没有礼物这个业务,所以并没有深入研究。demo礼物直接使用了大怪猿:iOS端直播间礼物模块,感觉非常不错,有时间会仔细阅读一下源码

其他

有些功能还要说下

  1. 聊天输入框,我的实现思路就是 监听键盘弹起和回收事件, 使用的是textView,因为有的业务需求输入的字数很多,textView可以换行
  2. 横屏弹键盘这个上面也提到 SuperPlayer_iOS是假横屏,横屏弹的依然是竖屏键盘,可以使用ZFPlayer 这个是控制器也会横屏,所以横屏键盘没有问题,唯一要注意的是要在AppDelegate实现下面的方法,防止整个工程都会跟着手机方向横屏
/// 在这里写支持的旋转方向,为了防止横屏方向,应用启动时候界面变为横屏模式
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
    if (self.allowOrentitaionRotation) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    }
    return UIInterfaceOrientationMaskPortrait;
}

最后奉上本文demoZBLiveRoom