一. 功能介绍
- 一个文字聊天室(如图下半部分)
- 一个语音聊天室(语音房)(上半部分)
- 五个麦位
- 可上麦下麦
二. 业务梳理
业务层面的语音聊天室 在用户创建聊天室时请求业务(自己)服务器,业务服务器创建聊天室,并返回给创者者当前聊天室的 id,同时其他用户可通过获取聊天室列表接口获取到此聊天室的 id。用户调根据此聊天室 id 加入
IM 聊天室
和音频 RTC
房间。当前用户属于哪个聊天室,当前聊天室内有哪些用户是基于此聊天室。
备注: 在创建一个房间时, 后台依次会调用融云server API 创建一个语音通话, 文字聊天都包含的一个房间(ROOM).IM 即时通讯层面的语音聊天室 加入 IM 聊天室后,用户可以发送文本消息聊天。另外维护聊天室各种状态的信令消息也通过 IM 服务来收发。(理论上 文字聊天(IM), 语音房(RTC) 使用一个目标Id即可)
3, RTC 音频层面的语音聊天室 加入 RTC 聊天室后,用户可以获得到当前语音聊天室内所有发布音频流的用户,并选择订阅音频流来收听目标用户的声音,也可以自己发布音频流让其他人听到自己的声音。
三. 前端(App端) 梳理
A 可见 B、C、D 的音频、视频
B 可按需选择只听 A、D 的音频,只看 A、C 的视频
C 可按需选择只听 A、B、D 的音频,但不看其他人视频
D 可按需选择只看 A、B、C 的视频,但不听其他人的音频
四. iOS 端伪代码, 以下全是伪代码
- RTC(语音视频室基于IM)所以第一步 需要连接IM
[[KKIMMgr shareInstance] checkAndConnectRCSuccess:^{
/// 连接成功, 再去创建一个上述功能的房间
} error:^(RCConnectErrorCode status) {
[CC_Notice show:@"聊天室未连接"];
}];
- 创建房间成功后呢, 后台会返回一个目标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:@"进入房间失败!"];
});
}];
- 做一个上麦, 下麦的管理
没有伪代码
- 对于再麦位上的用户, 扩散动画处理, 也是一段伪代码.
- (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;
}
}
}
}
});
}
- 解释
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;
}
六 总结
- 创建房间(具备RTC功能的房间) 重要!
- 加入房间
- 麦位管理
- 动画处理
- 麦克风和扬声器的管理
七. 优化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));
}];
});
}
完
文/夏天然后