最近在做視頻和直播業(yè)務(wù)洞翩,整理成文章和demo,記錄一下開發(fā)心得
因為直播已經(jīng)火了好幾年了岗喉,移動端的直播 的核心功能其實已經(jīng)很成熟了哼勇,大多集成一些第三方sdk或開源項目就可以實現(xiàn),但是直播涉及技術(shù)點多乖菱,比如播放器,IM即時通訊,彈幕,禮物等坡锡,如果有推流還要搭建推流端,難點在于這些技術(shù)相互關(guān)聯(lián)窒所,做到不卡頓鹉勒,流暢度好。
本文demoZBLiveRoom
包含 直播間吵取, 抖音短視頻禽额,廣告+正片,視頻歷史彈幕綁定,以后還會陸續(xù)更新
直播間
抖音
廣告+正片
視頻歷史彈幕綁定
播放器
首先說下 播放器的選擇脯倒,因為我們服務(wù)器選擇的阿里云 做直播推流相關(guān)服務(wù)实辑,我第一時間也選擇的阿里云播放器,嘗試集成了一下藻丢,就一字 坑剪撬,阿里云播放器,真的是難用悠反,看里面的代碼残黑,應(yīng)該是換了好幾個人維護,居然有注釋 這個屬性是干嘛的斋否,自己的人都看不懂梨水,我們怎么玩 。同時安卓同事也反饋茵臭,太難用了疫诽,我們果斷換。接下來研究了ijkplayer旦委,這個也是iOS用的比較廣泛的奇徒,很多坑前人基本都踩沒了,控制層也有很多出名的比如ZFPlayer結(jié)合ijkplayer
缨硝,正好我們app 直播和視頻 都需要支持逼龟。但是就一點ijkplayer 太大了,最新版500多M ,git根本傳不上去追葡,也找了很多方案,基本上就是把ijkplayer放在本地奕短,不傳git宜肉。這個在多人開發(fā)的團隊還是不太方便的。而且打完包確實大了不少翎碑,所以又舍棄了谬返。后來我又調(diào)研了其他一些播放器,都感覺不太滿意日杈,最后在同事的推薦下遣铝,找到了 SuperPlayer_iOS,是騰訊對自家的TXLiteAVSDK_Player 的一個封裝莉擒,還是比較滿意的,整個sdk大小100M多點酿炸,看打印的log ,TXLiteAVSDK_Player 底層也應(yīng)該是對ijkplayer的封裝涨冀,
SuperPlayer_iOS 當然也有缺點填硕,最不能讓人忍受的就是橫屏的問題,SuperPlayer_iOS的橫屏是一個假橫屏,用View做的方向改變扁眯,而控制器還是豎屏壮莹,會造成很多需求有問題,比如我們在橫屏?xí)r想彈出鍵盤姻檀,你會發(fā)現(xiàn)是豎屏鍵盤命满,很坑,所有如果你的項目對橫屏有類似業(yè)務(wù)的绣版,還是不要使用SuperPlayer_iOS胶台,推薦ZFPlayer,后期我們播放視頻業(yè)務(wù)已經(jīng)切換到了ZFPlayer
當然每個app僵娃,都是要單獨設(shè)計UI控制層的概作,SuperPlayer_iOS 雖然支持自定義控制層,但是有些地方的UI或控制,它還是無法自定義默怨,所以不能用cocoapods 去管理讯榕,pod install一下,所有更改恢復(fù)原樣了匙睹,當然我這個demo 還是用的 cocoapods集成的愚屁,如果如果SuperPlayer_iOS 的默認控制層,或自定義控制層痕檬,能滿足你的需求霎槐,直接pod 'SuperPlayer'就可以了
如果不能滿足手動復(fù)制了 SuperPlayer_iOS 所有代碼
pod 'TXLiteAVSDK_Player'
pod 'MMLayout'
//下面這倆個項目里已經(jīng)有,就不用添加了
//pod 'SDWebImage'
//pod 'AFNetworking'
創(chuàng)建播放器
#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;// 設(shè)置父視圖
播放時建議使用真機梦谜,模擬器會有很多問題丘跌。
在播放器 播放時 一般都會有移動網(wǎng)絡(luò) 提示 ,如果是移動網(wǎng)絡(luò) 提示一下,如果是wifi 直接播放
if ([self isNetworkWiFi]==NO) {
//移動網(wǎng)絡(luò)提示ui
}else{
SuperPlayerModel *playerModel = [[SuperPlayerModel alloc] init];
/**
坑一
模擬器播放 flv格式 畫面紅色唁桩,有聲音闭树,真機沒有問題。
因為SuperPlayerViewConfig 內(nèi)的hwAcceleration 方法 荒澡,模擬器默認硬解碼為默認 NO造成的 报辱,
坑二
模擬器 播放mp4格式視頻 幀數(shù)很低 20-30fps,使用真機幀數(shù)恢復(fù)正常猜測還是解碼的問題单山,直播 基本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的
聊天室
聊天室 分兩部分吧
一碍现、IM的通信服務(wù),我們并沒有使用第三方服務(wù)米奸,長鏈接由自家服務(wù)器開發(fā)昼接,因為我們pc端也要做直播業(yè)務(wù),而pc端支持只支持WebSocket協(xié)議悴晰,iOS實現(xiàn)WebSocke通信辩棒,我們選擇了facebook的SocketRocket,SocketRocket非常完美,沒什么可說的一睁,唯一的缺點可能就是好幾年不更新了钻弄。
另外長鏈接的保活機制需要注意幾點者吁,大約邏輯就是
連接失敗窘俺,可以實現(xiàn)掉線自動重連
1.判斷當前網(wǎng)絡(luò)環(huán)境,如果斷網(wǎng)了就不要連了复凳,等待網(wǎng)絡(luò)到來瘤泪,在發(fā)起重連;
2.判斷調(diào)用層是否需要連接,例如用戶都沒在聊天界面育八,連接上去浪費流量
二对途、聊天室,參考了下面這兩位大神的文章
翻炒吧蛋滾飯:直播中聊天室踩過的坑以及我的填坑歷程
大怪猿:iOS直播間聊天室—圖文混排加載網(wǎng)絡(luò)圖片髓棋、
有面向?qū)ο箝_發(fā)实檀,面向過程開發(fā),面向bug開發(fā)按声,那么我就是面向大神開發(fā)
聊天室ui膳犹,是tableView搭建的
主要就是大量數(shù)據(jù)的時候,如何保證直播間流程签则,不卡頓须床,對整個項目的性能沒有影響。
歸納總結(jié)了幾個優(yōu)化重點:
- 添加數(shù)據(jù)時 渐裂,不要使用[self.tableView reloadData];豺旬,使用單行刷新,
當數(shù)量大的時候柒凉,這個真的很明顯哈垢。 - 對于ui不同聊天數(shù)據(jù),要使用不同cell
- cell的高度緩存
- 在聊天數(shù)據(jù)大于一定量時扛拨,需要刪除前面的部分數(shù)據(jù),已保證不卡頓举塔,
這個我做過實驗绑警,iphone 6在1400多條數(shù)據(jù)的時候在不斷的添加數(shù)據(jù),不管用哪種方式刷新tableView央渣,就會明顯的卡頓计盒,應(yīng)該內(nèi)存方面的問題了 - 使用緩存數(shù)據(jù)源,
當接收到數(shù)據(jù)芽丹,不要直接刷新北启,而是給緩存數(shù)據(jù)源,當達到條件后,在把緩存數(shù)據(jù)給正式數(shù)據(jù)源 - 在適當?shù)臅r候刷新tableView咕村,
比如我的播放器橫屏了场钉,比如我滑動聊天列表,并沒有在聊天室的最下邊懈涛,其實都是不用去刷新tableView的 - 無限刷新 改為固定時間刷新
在聊天數(shù)據(jù)量非常大的時候逛万,比如1秒鐘好幾十條,那tableView就會刷新好幾十次批钠,這太可怕了宇植,所以可以使用固定時間刷新tableView,因為有緩存數(shù)據(jù)源埋心,我們可以加邏輯指郁,比如每1秒鐘把緩存數(shù)據(jù)源添加到正式數(shù)據(jù),并刷新tableView
做了以上的優(yōu)化后拷呆,聊天室基本不會卡頓了
彈幕
彈幕使用了OCBarrage闲坎,這個真的是很優(yōu)秀的開源項目了。
以下是相關(guān)代碼
//懶加載
- (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);
}];
//配置每一條彈幕 并發(fā)送
- (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];
}
但是有一個業(yè)務(wù)需求洋腮,我們不止有直播箫柳,還有很多視頻,視頻播放歷史彈幕啥供,這個OCBarrage并沒有現(xiàn)成的方法悯恍,
iOS還有個開源彈幕BarrageRenderer,這個是有視頻時間綁定彈幕的功能伙狐,但是我試了試涮毫,有很多bug,最終還是放棄了贷屎。
作為面向大神開發(fā)的我罢防,今天要雄起了,自己實現(xiàn)一個彈幕隊列的功能
彈幕隊列
服務(wù)器下發(fā)歷史彈幕格式 基本大約是這樣的.彈幕的時間 和彈幕的文本內(nèi)容
[
{
"seconds": "1",//彈幕時間(秒)
"barrage": "大家好",//彈幕內(nèi)容
},
{
"seconds": "40",//彈幕時間(秒)
"barrage": "風(fēng)力雨里唉侄,我在評論區(qū)等你",//彈幕內(nèi)容
},
{
"seconds": "40",//彈幕時間(秒)
"barrage": "這個內(nèi)容我喜歡",//彈幕內(nèi)容
},
{
"seconds": "111",//彈幕時間(秒)
"barrage": "我是andi",//彈幕內(nèi)容
},
]
基本思路就是把 彈幕以彈幕時間為key 文本為Value 存在字典里咒吐,這個存還是有些講究的,可以看到上面在40秒的時候同時存在兩個彈幕属划,這種情況其實很常見恬叹,火的視頻每秒甚至10多條,所以存的時候彈幕時間為key同眯,而Value就需要是一個同一時間文本的數(shù)組绽昼, 之后在取也是取的數(shù)組。
取彈幕 有兩種方式
- 在視頻當前時間回調(diào)刃胛稀(推薦)
這個是 視頻走到哪硅确,根據(jù)視頻的當前時間取對應(yīng)的彈幕目溉,播放和暫停也沒有操作。 - 使用計時器
在視頻的開始播放時菱农,就開啟計時器缭付,每一秒獲取一次 當前視頻播放的時間,根據(jù)獲取的時間取對應(yīng)的彈幕大莫,暫停時需要關(guān)閉計時器蛉腌,播放需要開啟計時器,
下面使用ZFPlayer 播放視頻 并綁定歷史彈幕只厘,
self.barrageQueue=[[ZBBarrageQueue alloc]init];
self.barrageQueue.delegate=self;
//加載彈幕數(shù)據(jù)
[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)
//因為此回調(diào)是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元素 發(fā)送的彈幕 %@",idx,text);
[self sendbarrage:text isMe:NO];
}];
}
當然取彈幕隊列其實還有一些要完善的問題羔味,
1.比如滑動進度條回退視頻會重復(fù)加載彈幕河咽,而有時可能就回退幾秒鐘,當前彈幕還在屏幕顯示赋元,怎么去重忘蟹。
禮物
因為我們項目,目前并沒有禮物這個業(yè)務(wù)搁凸,所以并沒有深入研究媚值。demo禮物直接使用了大怪猿:iOS端直播間禮物模塊,感覺非常不錯护糖,有時間會仔細閱讀一下源碼
其他
有些功能還要說下
- 聊天輸入框褥芒,我的實現(xiàn)思路就是 監(jiān)聽鍵盤彈起和回收事件, 使用的是textView嫡良,因為有的業(yè)務(wù)需求輸入的字數(shù)很多锰扶,textView可以換行
- 橫屏彈鍵盤這個上面也提到 SuperPlayer_iOS是假橫屏,橫屏彈的依然是豎屏鍵盤寝受,可以使用ZFPlayer 這個是控制器也會橫屏坷牛,所以橫屏鍵盤沒有問題,唯一要注意的是要在AppDelegate實現(xiàn)下面的方法很澄,防止整個工程都會跟著手機方向橫屏
/// 在這里寫支持的旋轉(zhuǎn)方向京闰,為了防止橫屏方向,應(yīng)用啟動時候界面變?yōu)闄M屏模式
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
if (self.allowOrentitaionRotation) {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
return UIInterfaceOrientationMaskPortrait;
}
最后奉上本文demoZBLiveRoom