APP在线抢答解决方案(RTC直播间抢答或者抢背唱歌)

    xiaoxiao2022-07-07  226

    前言

    2018年下半年要说最火的产品莫过于音遇了,上线短短一连个月,估值将近2亿美金,他的产品也被各大公司所模仿借鉴,包括我们公司?!接下来我将说说这款APP的直播间的解决方案,以供大家参考!

    也算是对我上一份工作的一个总结吧!?

    产品分析

    a.音视频处理

    游戏直播间是一种在线推流和拉流的操作,目前主要的推流和拉流的方式有常见的RTMP和RTC两种;

    RTMP: 基于 TCP 的标准协议,与 CDN 架构兼容,对客户来说在现有单向直播架构上,接入成本比较低,但是缺点也多,回音、传输延时大、耗费CPU等资源等等,但是便宜;

    RTC:降低了音视频通信的接入门槛,而且用户体验比较好,最主要的是没有延迟;缺点就是价格昂贵(如果你公司不差钱就当我没说?);

    但是如果是普通的视频直播什么的使用RTMP也没啥问题,关键是这种抢答类的产品对实时性要求比较高,肯定要选择RTC了!

    目前RTC如果使用第三方收费都不便宜,我们公司的产品用的是七牛云存储的RTC直播间,2019年第一版刚出来,价格还很便宜?。

    相关:《七牛实时音视频云》

    b.指令下发

    如果想让前端与后端实时通讯,进行数据传输和指令交互,就需要做socket长连接,这样即需要后台开发人员对服务器进行配置还需要公司出资购买新的服务器,一切为了省钱......

    这样我就想到了使用IM即使聊天来代替socket长连接的实时交互,关键是在游戏直播间还必须用到IM聊天系统;具体的实现思路是在游戏直播间由服务器扮演管理员的角色,对房间的所有人所在的群组发送管理员消息,当然所以得管理员消息我们会进行筛选和过滤,这样就不会暂时在聊天界面上了,还有一些答题信息或者答题结果我们都可以以data或字典的形式放在自定义的字段里面,代码如下:

    WEAKBLOCK; [TXIMTools manager].getNewMessages = ^(TIMMessage *meg) { //[2019-03-18 16:03:00][status:2 sender=255762] [TIMTextElem=text:准备好了吗!快点开始吧!] for (RAUserModel *model in self.userArray) { if([model.userId intValue] == [meg.sender intValue]){ [weakSelf.chatView addOneTXIMChatMeg:meg number:model.number]; break; } } }; - (void)getAdminOrder:(NSInteger)code dataDict:(NSDictionary *)dataDict{ WEAKBLOCK; if(code == 2000){ //游戏开始 gameRoom _isStratGame = YES; [RAMatchStateView showRAReadyStartViewFinished:^{ NSLog(@"游戏开始"); }]; [self upLoadUserArrayWithGameRoom:dataDict code:code]; [self.engine joinRoomWithToken:_currentUserModel.rtcToken userData:nil]; self.topView.hidden = NO; self.likesBtn.hidden = NO; }else if (code == 2001){ //游戏出题 gameRoom //...此处省略2000行代码... }else if(code == 2002){ //用户抢答(谁抢到了) "id", "number", "name", "gender", "type" }else if(code == 2003){ //通知哪个用户抢到 "id", "number", "name", "gender", "type" _answerUserInfors = dataDict;//答题者信息 [self.subjectView hideRASubjectView]; [self.robButton hideRARobButton]; [self.matchStateView refreshAnswerStateView:GetChanceType number:dataDict[@"number"] name:dataDict[@"name"]]; if([dataDict[@"id"] integerValue] == [_currentUserModel.userId integerValue]){ //当前用户抢到了 等待答题 [MBProgressHUD SHOWPrompttextNeedClose:@"等待答题···"]; } }else if(code == 2004){ //全军覆没 返回null [self.subjectView hideRASubjectView]; [self.robButton hideRARobButton]; [self.matchStateView refreshAnswerStateView:NoOneAnswerType number:nil name:nil]; _answerUserInfors = nil; }else if(code == 2006){ //答题成功了 返回null //...此处省略2000行代码... }else if(code == 2007){ //答题打错了 返回null //...此处省略2000行代码... }else if(code == 2008){ //游戏结束 gameRoom [self upLoadUserArrayWithGameRoom:dataDict code:code]; [self.subjectView hideRASubjectView]; [self.matchStateView hideAnswerStateView]; [self gameOver]; _answerUserInfors = nil; }else if(code == 2009){ //加入房间 room [self analysisRoomDataDict:dataDict]; }else if(code == 2010){ //退出房间 room [self analysisRoomDataDict:dataDict]; }else if(code == 2011){ //邀请好友进入房间 "id"(用户ID), "number", "name", "type"(用户类型), "gameType", "gradeDesc", "articleType", "roomId" //在base处理 }else if(code == 2012){ //淘汰用户 返回 "id", "number", "name" 用户Id 用户number 用户名称 NSString *number = [NSString stringWithFormat:@"%@",dataDict[@"number"]]; NSString *name = dataDict[@"name"]; [self.subjectView hideRASubjectView]; [self.matchStateView refreshAnswerStateView:DieOutType number:number name:name]; }else if(code == 2013){ //等待下一题 [self.subjectView hideRASubjectView]; [self.matchStateView refreshAnswerStateView:AnswerNextQuestionType number:nil name:nil]; [MBProgressHUD HIDEPrompttextByClose]; _answerUserInfors = nil; }else if(code == 2014){ //谁接背 返回 "id", "number", "name" 用户Id 用户number 用户名称 //...此处省略2000行代码... }else if(code == 2015){ //会 进入答题流程 返回 "id", "number", "name" 用户Id 用户number 用户名称 //保持状态 }else if(code == 2016){ //不会 返回 "id", "number", "name" 用户Id 用户number 用户名称 //保持状态 }else if(code == 2018){ //抢背开始 //...此处省略2000行代码... }else if(code == 2019){ //用户答题指令 返回 "id", "number", "name" 用户Id 用户number 用户名称 开始答题 [self answerAndStopAnswer:dataDict]; }else if(code == 2020){ //取消匹配 room //在WaitingGameRoomViewController界面处理UI [self analysisRoomDataDict:dataDict]; }else if(code == 2021){ //匹配中 room [self showMatchingView]; }else if(code == 2022){ //匹配成功 gameRoom [self upLoadUserArrayWithGameRoom:dataDict code:code]; self.headView.hidden = YES; self.inviteBtn.hidden = YES; self.quickStartBtn.hidden = YES; }else if(code == 2023){ //用户准备 返回 "id", "number", "name" 用户Id 用户number 用户名称 [self userReadyOrUnReady:dataDict[@"id"] isReady:YES]; }else if(code == 2024){ //用户取消准备 返回 "id", "number", "name" 用户Id 用户number 用户名称 [self userReadyOrUnReady:dataDict[@"id"] isReady:NO]; }else if(code == 2025){ //用户送礼 giveUserNumber userNumber pic [self.giftListView addOneSentGiftMeg:dataDict]; }else if(code == 2026){ //用户被踢出房间(只有被踢出的人才能收到这个消息) roomId - (void)kickoutUser:(NSString *)userId; [self.engine kickoutUser:_currentUserModel.userId]; [ZFJReciteAlertView showAlertViewSureBtnWithTitle:@"您已被房主移出房间!" selectedIndex:^(NSInteger index) { [weakSelf leaveRoom]; }]; }else if(code == 2027){ //开始推流 返回答题用户 返回 "id", "number", "name" 用户Id 用户number 用户名称 //只有答题者才可以推流 if([dataDict[@"id"] integerValue] == [_currentUserModel.userId integerValue]){ [self.engine publishAudio]; } } }

    注明:以上只提供逻辑指令处理,不会给出详细数据处理或者动画代码!

    语言识别

    我们需要将用户说的话识别为文字(汉子或英语),然后与数据库的答案进行对照,然后给出得分。这里的语言识别当然推荐使用科大讯飞了,比较赫赫有名的走在语言识别前端的技术(收费高);但是我们产品用的是百度语言识别,为什么呢?免费、免费、免费,重要的事情我说三遍!!!

    一开始想把百度语言识别的SDK直接集成在工程中,这样识别的效率高、耗时短,但是,有两点不好的地方,对于我这中优化狂魔所不能忍的是:

    a.语言识别的库占用很大一部分内存,是API的包增加了20多M;

    b.我们还要对语音进行保存,这样在一边识别的时候我们还要一边录音,导致的问题就是,耗内存资源;

    所以经过考虑,我们把百度语言识别交给服务端的小伙伴来搞,前端只需要录音就可以了,然后把MP3文件直接传给后端,后端小伙伴在指令里面给出结果!So easy!(妈妈再也不用担心我的包太大了)?

    音频处理

    这个功能我们需要经常使用AVAudioRecorder,比如录音、播放领读、播放动画音效等等;AVAudioRecorder频繁创建肯定会影响APP性能,所以我把AVAudioRecorder封装起来丢进单利里面;封装的方法包含录音和播放的功能,代码如下(仅提供思路):

    @interface RecordOperation : NSObject //-----------------------------录音相关----------------------------- /** 普通MP3录音 @return self */ - (instancetype)init; /** 开始录音 */ - (void)startRecord; /** 结束录音返回caf @param scuBlock caf回调音频文件地址 */ - (void)stopRecord_Caf_ScuBlock:(void(^)(NSString *urlPath, CGFloat audioSeconds))scuBlock; /** 获取录音声波状态 @param averagePower 声波值回调 */ - (void)getAveragePowerForChannel:(AveragePower)value; /** 播放录音的文件 */ - (void)playListenningRecognition; /** 是否正在录音 */ @property (nonatomic,assign) BOOL isRecording; //-----------------------------播放相关----------------------------- /** 播放音频文件 @param urlStr 音频文件地址 @param duration 音频文件总时长 @param playCompleted 播放完成回调 */ - (void)playVoiceBubbleWithUrlStr:(NSString *)urlStr duration:(VoiceInforsBlock)duration playCompleted:(PlayCompleted)playCompleted; /** 获取播放音频文件信息 @param currentTime 播放时间 @param progress 进度 */ - (void)getVoiceBubbleCurrentTime:(VoiceInforsBlock)currentTime progress:(VoiceInforsBlock)progress; /** 停止播放音频文件 */ - (void)stopPlayVoiceBubble; @end

    控件封装

    像这种交互性比较强的产品,各种题目信息或者答题状态还有试图动画也肯定必不可少了,这就需要我们把相同的控件和功能进行打包封装,以减少C的代码,不会使Controller过于臃肿,也更利于代码的刻度与赏心悦目的赶脚!

    下面的代码是题目状态信息的过渡动画,仅供参考:

    @interface RAMatchStateView : UIView /** 正在匹配初始化 @return self */ - (instancetype)initReadyToStartView; /** 显示匹配成功 */ - (void)showMatchCompletion; /** 准备开始 1 2 3 GO @param finished 完成回调 */ + (void)showRAReadyStartViewFinished:(FinishCountDownBlock)finished; /** 答题状态初始化 @param frame 坐标 @return self */ - (instancetype)initAnswerStateView:(CGRect)frame; /** 刷新控件答题状态 @param stateType 状态类型 @param number 用户ID @param name 用户name */ - (void)refreshAnswerStateView:(AnswerStateType)stateType number:(NSString *)number name:(NSString *)name; /** 隐藏控件答题状态 */ - (void)hideAnswerStateView; @end

    产品展示

    结束语

    欢迎各位大神补充!

     

     

     

     

     

     

    最新回复(0)