iOS - 融云RTC功能梳理

一. 功能介绍

功能需求
  1. 一个文字聊天室(如图下半部分)
  2. 一个语音聊天室(语音房)(上半部分)
  3. 五个麦位
  4. 可上麦下麦

二. 业务梳理

融云流程图
  1. 业务层面的语音聊天室  在用户创建聊天室时请求业务(自己)服务器,业务服务器创建聊天室,并返回给创者者当前聊天室的 id,同时其他用户可通过获取聊天室列表接口获取到此聊天室的 id。用户调根据此聊天室 id 加入 IM 聊天室音频 RTC房间。当前用户属于哪个聊天室,当前聊天室内有哪些用户是基于此聊天室。
    备注: 在创建一个房间时, 后台依次会调用融云server API 创建一个语音通话, 文字聊天都包含的一个房间(ROOM).

  2. IM 即时通讯层面的语音聊天室  加入 IM 聊天室后,用户可以发送文本消息聊天。另外维护聊天室各种状态的信令消息也通过 IM 服务来收发。(理论上 文字聊天(IM), 语音房(RTC) 使用一个目标Id即可)

3, RTC 音频层面的语音聊天室   加入 RTC 聊天室后,用户可以获得到当前语音聊天室内所有发布音频流的用户,并选择订阅音频流来收听目标用户的声音,也可以自己发布音频流让其他人听到自己的声音。

三. 前端(App端) 梳理

屏幕快照 2019-10-26 下午5.00.05.png
  • A 可见 B、C、D 的音频、视频

  • B 可按需选择只听 A、D 的音频,只看 A、C 的视频

  • C 可按需选择只听 A、B、D 的音频,但不看其他人视频

  • D 可按需选择只看 A、B、C 的视频,但不听其他人的音频

四. iOS 端伪代码, 以下全是伪代码

  1. RTC(语音视频室基于IM)所以第一步 需要连接IM
[[KKIMMgr shareInstance] checkAndConnectRCSuccess:^{
        /// 连接成功, 再去创建一个上述功能的房间
    } error:^(RCConnectErrorCode status) {
        [CC_Notice show:@"聊天室未连接"];
    }];
  1. 创建房间成功后呢, 后台会返回一个目标Id, 或者叫做房间Id (roomId), 通过这个Id去连接 RTC Room
///  KKRtcService 是对融云RTC "<RongRTCLib/RongRTCLib.h>"进行了一层封装
[[KKRtcService shareInstance] joinRoom:channelId success:^(RongRTCRoom * _Nonnull room) {
                    
                    dispatch_async(dispatch_get_main_queue(), ^{
                           /// 证明连接成功RTC
                           ///  跳转进入房间
                        });
                        
                    });
                } error:^(RongRTCCode code) {
                    dispatch_main_async_safe(^{
                        [CC_Notice show:@"进入房间失败!"];
                    });
                }];          
  1. 做一个上麦, 下麦的管理

没有伪代码

  1. 对于再麦位上的用户, 扩散动画处理, 也是一段伪代码.
- (void)didReportStatForm:(RongRTCStatisticalForm *)form {
    //1.发送
    for(RongRTCStreamStat *streamStat in form.sendStats){
        //只处理音频
        //处理发送的音频 动画
        if ([streamStat.mediaType isEqualToString:RongRTCMediaTypeAudio]) {
            [self updateMicPositionSpeaking:streamStat.trackId audioLevel:streamStat.audioLevel];
        }
    }
    
    //2.接收
    for(RongRTCStreamStat *streamStat in form.recvStats){
        //只处理音频
        //处理接收的音频 动画
        if ([streamStat.mediaType isEqualToString:RongRTCMediaTypeAudio]) {
             [self updateMicPositionSpeaking:streamStat.trackId audioLevel:streamStat.audioLevel];
        }
    }
}

- (void)updateMicPositionSpeaking:(NSString *)userId audioLevel:(NSInteger)audioLevel {
    dispatch_async(dispatch_get_main_queue(), ^{
        /// 这里是处理了 CollectionViewCell里面的动画逻辑
        /// 关键在与userId的判断和audioLevel音量的双重条件, 来控制动画
        for (KKPlayerCardCollectionCell *cell in self.playerListView.visibleCells) {
            if (cell.dataModel.userId.length > 0 && [userId containsString:cell.dataModel.userId]) {
                CCLOG(@"cell.dataModel.userId === %@", cell.dataModel.userId);
                
                if (self.isMicOpen == YES) {
                    cell.isSpeaking = audioLevel > 0;
                }else {
                    if ([userId containsString:[KKUserInfoMgr shareInstance].userId]) {
                        cell.isSpeaking = NO;
                    }
                }
            }
        }
    });
}
  1. 解释

5.1 简单理解上麦需要发布音频流
5.2 下麦取消发布音频流
5.3 想听到谁的声音/视频 你就去订阅谁
5.4 不想听谁的声音关闭扬声器就好了

五. RTCServiceMgr 以下代码通用

@interface RTCServiceMgr : NSObject

#pragma mark - life circle
+ (instancetype)shareInstance;
- (void)remove;

#pragma mark - rtc
/** 当前加入的 rtc房间 (当加入房间成功之后,才会是有效值 */
@property (nonatomic, strong, readonly) RongRTCRoom *rtcRoom;

/** 关闭所有声音 */
@property (nonatomic, assign) BOOL muteAllVoice;

/** 设置 直播间代理 */
- (void)setRTCRoomDelegate:(id<RongRTCRoomDelegate>)delegate;

/** 设置 直播间引擎代理 */
- (void)setRTCEngineDelegate:(id<RongRTCActivityMonitorDelegate>)delegate;


#pragma mark 加入/退出
/** 加入直播间 */
- (void)joinRoom:(NSString *)roomId success:(void (^)( RongRTCRoom *room))success error:(void (^)(RongRTCCode code))error;

/** 退出直播间 */
- (void)leaveRoom:(NSString*)roomId success:(void (^)(void))success error:(void (^)(RongRTCCode code))error;


#pragma mark  音频流
/** 发布 音视频流 */
- (void)pulishCurrentUserAudioStream;

/** 取消发布 音视频流 */
- (void)unpublishCurrentUserAudioStream;

/** 订阅 远端音频stream */
- (void)subscribeRemoteUserAudioStream:(NSString *)userId;

/** 取消订阅 远端音频stream */
- (void)unsubscribeRemoteUserAudioStream:(NSString *)userId;


#pragma mark 麦克风/扬声器
/** 设置麦克风 是否可用 */
- (void)setMicrophoneDisable:(BOOL)disable;

/** 设置扬声器 是否可用 */
- (void)useSpeaker:(BOOL)useSpeaker;

@end
@interface RTCServiceMgr () 
@property (nonatomic, strong) RongRTCRoom *rtcRoom;
@property (nonatomic, strong) RongRTCVideoCaptureParam *captureParam;
@property (nonatomic, strong) RongRTCAVCapturer *capturer;
@end

@implementation RTCServiceMgr

static RTCServiceMgr *rongRtcMgr = nil;
static dispatch_once_t onceToken;

#pragma mark - getter/setter
#pragma mark getter
- (RongRTCAVCapturer *)capturer {
    if(!_capturer) {
        _capturer = [RongRTCAVCapturer sharedInstance];
    }
    return _capturer;
}
- (RongRTCVideoCaptureParam *)captureParam {
    if(!_captureParam) {
        _captureParam = [[RongRTCVideoCaptureParam alloc] init];
        _captureParam.turnOnCamera = NO;
    }
    return _captureParam;
    
}

#pragma mark setter
- (void)setMuteAllVoice:(BOOL)mute {
    _muteAllVoice = mute;
    
    for(RongRTCRemoteUser *remoteUser in self.rtcRoom.remoteUsers) {
        for(RongRTCAVInputStream *stream in remoteUser.remoteAVStreams) {
            if(stream.streamType == RTCMediaTypeAudio) {
                stream.disable = mute;
            }
        }
    }
}


#pragma mark - life circle
+ (instancetype)shareInstance {
    dispatch_once(&onceToken, ^{
        rongRtcMgr = [[KKRtcService alloc] init];
    });
    return rongRtcMgr;
}

- (instancetype)init {
    self = [super init];
    if (self) {
    }
    return self;
}

/** 清空 */
- (void)remove{
    //1.删除userDefault中的用户信息
    
    
    //2.释放self
    rongRtcMgr = nil;
    onceToken = 0;
}


#pragma mark - rtc
- (void)setRTCRoomDelegate:(id<RongRTCRoomDelegate>)delegate {
    if(!self.rtcRoom) {
        //[CC_Notice show:@"尚未加入招募厅,无法设置代理"];
        return;
    }
    self.rtcRoom.delegate = delegate;
}

-(void)setRTCEngineDelegate:(id<RongRTCActivityMonitorDelegate>)delegate {
    [RongRTCEngine sharedEngine].monitorDelegate = delegate;
}


#pragma mark 加入/退出 rtc 房间
- (void)joinRoom:(NSString *)roomId success:(void (^)( RongRTCRoom  * _Nullable room))success error:(void (^)(RongRTCCode code))error {
    
    [[RongRTCEngine sharedEngine] joinRoom:roomId completion:^(RongRTCRoom * _Nullable room, RongRTCCode code) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if(RongRTCCodeSuccess == code) {
                self.rtcRoom = room;
                if(success) {
                    success(room);
                }
                
            }else if(RongRTCCodeJoinRepeatedRoom == code || RongRTCCodeJoinToSameRoom == code) {
                //当 RTC 出现此类错误时,RTC 不会再下发 room 对象,只能用上次的 room
                if(success) {
                    success(self.rtcRoom);
                }
                
            }else {
                if(error) {
                    error(code);
                }
            }
            
        });
    }];
}

- (void)leaveRoom:(NSString*)roomId success:(void (^)(void))success error:(void (^)(RongRTCCode code))error {
    
    [[RongRTCEngine sharedEngine] leaveRoom:roomId completion:^(BOOL isSuccess, RongRTCCode code) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            BBLOG(@"离开 RTCRoom ,code = %ld",(long)code);
            
            //成功 或 不再房间中 或 没有匹配的RTC room
            if(isSuccess || RongRTCCodeSuccess == code ||
               RongRTCCodeNotInRoom == code ||
               RongRTCCodeNoMatchedRoom == code) {
                
                self.rtcRoom = nil;
                if(success) {
                    success();
                }
                
            }else {
                if(error) {
                    error(code);
                }
            }
            
        });
    }];
}


#pragma mark 音频流
/** 发布 音视频流 */
- (void)pulishCurrentUserAudioStream {
    if(!self.rtcRoom) {
        //房间没生成
        return;
    }
    
    //1.采集
    self.captureParam.turnOnCamera = NO;
    [self.capturer setCaptureParam:self.captureParam];
    [self.capturer startCapture];
    
    //2.发布
    [self.rtcRoom publishDefaultAVStream:^(BOOL isSuccess, RongRTCCode desc) {
        BBLOG(@"当前用户发布音频流 %@",@(desc));
    }];
}

/** 取消发布 音视频流 */
- (void)unpublishCurrentUserAudioStream {
    if(!self.rtcRoom) {
        //房间没生成
        return;
    }
    //1.关闭采集
    [self.capturer stopCapture];
    
    //2.取消发布
    [self.rtcRoom unpublishDefaultAVStream:^(BOOL isSuccess, RongRTCCode desc) {
        BBLOG(@"当前用户取消发送音视频流 %@",@(desc));
    }];
    
}

/** 订阅 远端音频stream */
- (void)subscribeRemoteUserAudioStream:(NSString *)userId {
    if(!self.rtcRoom) {
        //房间没生成
        return;
    }
    
    if(userId.length < 1){
        return;
    }
    
    RongRTCRemoteUser *remoteUser = [self getRTCRemoteUser:userId];
    if(remoteUser.remoteAVStreams.count <= 0) {
        [CC_Notice show:@"远端资源不存在,不能订阅音频"];
        //BBLOG(@"subscribe --- user:%@ streams:%@",remoteUser.userId,remoteUser.remoteAVStreams);
        return;
    }
    
    [self.rtcRoom subscribeAVStream:remoteUser.remoteAVStreams tinyStreams:nil completion:^(BOOL isSuccess, RongRTCCode desc) {
        
        BOOL mute = [KKRtcService shareInstance].muteAllVoice;
        for(RongRTCAVInputStream *stream in remoteUser.remoteAVStreams) {
            if(stream.streamType == RTCMediaTypeAudio) {
                stream.disable = mute;
            }
        }
        BBLOG(@"subscribe --- 订阅流 %@ success:%@ code:%@",remoteUser.userId,@(isSuccess),@(desc));
    }];
}

/** 取消订阅 远端音频stream */
- (void)unsubscribeRemoteUserAudioStream:(NSString *)userId {
    
    if(!self.rtcRoom) {
        //房间没生成
        return;
    }
    
    RongRTCRemoteUser *remoteUser = [self getRTCRemoteUser:userId];
    if(remoteUser.remoteAVStreams.count <= 0) {
        //[CC_Notice show:@"远端资源不存在,不能取消订阅音频"];
        BBLOG(@"unsubscribe --- user:%@ streams:%@",remoteUser.userId,remoteUser.remoteAVStreams);
        return;
    }
    
    [self.rtcRoom unsubscribeAVStream:remoteUser.remoteAVStreams completion:^(BOOL isSuccess, RongRTCCode desc) {
        BBLOG(@"取消订阅流 %@ success:%@ code:%@",remoteUser.userId,@(isSuccess),@(desc));
    }];
}


#pragma mark - tool
#pragma mark capturer
- (void)setMicrophoneDisable:(BOOL)disable {
    [self.capturer setMicrophoneDisable:disable];
}

- (void)useSpeaker:(BOOL)useSpeaker {
    [self.capturer useSpeaker:useSpeaker];
}


#pragma mark RemoteUser
/** 获取 rtcRemoteUser */
- (RongRTCRemoteUser *)getRTCRemoteUser:(NSString *)userId{
    for (RongRTCRemoteUser *user in self.rtcRoom.remoteUsers) {
        if ([userId isEqualToString:user.userId]) {
            return user;
        }
    }
    return nil;
}

六 总结

  1. 创建房间(具备RTC功能的房间) 重要!
  2. 加入房间
  3. 麦位管理
  4. 动画处理
  5. 麦克风和扬声器的管理

七. 优化rtcService订阅, 取消订阅,加入线程控制, 解决多人上麦订阅失败的情况

/** 订阅 远端音频stream */
- (void)subscribeRemoteUserAudioStream:(NSString *)userId {
    if(!self.rtcRoom) {
        return;
    }
    
    if(userId.length < 1){
        return;
    }
    
    dispatch_queue_t queue = dispatch_queue_create("kk.rtcService.audioStream", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        RongRTCRemoteUser *remoteUser = [self getRTCRemoteUser:userId];
        if(remoteUser.remoteAVStreams.count <= 0) {
            //BBLOG(@"subscribe --- user:%@ streams:%@",remoteUser.userId,remoteUser.remoteAVStreams);
            return;
        }
        
        [self.rtcRoom subscribeAVStream:remoteUser.remoteAVStreams tinyStreams:nil completion:^(BOOL isSuccess, RongRTCCode desc) {
            BOOL mute = [KKRtcService shareInstance].muteAllVoice;
            for(RongRTCAVInputStream *stream in remoteUser.remoteAVStreams) {
                if(stream.streamType == RTCMediaTypeAudio) {
                    stream.disable = mute;
                }
            }
            BBLOG(@"subscribe --- 订阅流 %@ success:%@ code:%@",remoteUser.userId,@(isSuccess),@(desc));
        }];
    });
    
    
}

/** 取消订阅 远端音频stream */
- (void)unsubscribeRemoteUserAudioStream:(NSString *)userId {
    
    if(!self.rtcRoom) {

        return;
    }
    
    dispatch_queue_t queue = dispatch_queue_create("kk.rtcService.audioStream", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        RongRTCRemoteUser *remoteUser = [self getRTCRemoteUser:userId];
        if(remoteUser.remoteAVStreams.count <= 0) {
            BBLOG(@"unsubscribe --- user:%@ streams:%@",remoteUser.userId,remoteUser.remoteAVStreams);
            return;
        }
        
        [self.rtcRoom unsubscribeAVStream:remoteUser.remoteAVStreams completion:^(BOOL isSuccess, RongRTCCode desc) {
            BBLOG(@"取消订阅流 %@ success:%@ code:%@",remoteUser.userId,@(isSuccess),@(desc));
        }];
    });
}


文/夏天然后

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,659评论 0 15
  • 2017.02.22 可以练习,每当这个时候,脑袋就犯困,我这脑袋真是神奇呀,一说让你做事情,你就犯困,你可不要太...
    Carden阅读 1,325评论 0 1
  • 山楂,又名红果、山里红等,味酸、甘,性微温,归脾、胃、肝经。 现代药理研究表明,山楂具有健脾消积、促进消化、扩张血...
    木木1号阅读 472评论 0 0
  • 文/血木生 昨夜星辰昨夜风,有只恶鬼坐楼东。 我家有一座老宅,在这座宅子中,已经生活过我们家族的很多代人了。 经历...
    血木生阅读 448评论 0 0
  • 平生不会相思,才会相思,便害相思...
    菥筠阅读 216评论 0 1