多人实时通话


为满足不同场景需求,3.5.0版本开始将实时音视频会议划分了不同的类型,不同类型对应了不同场景,使你能够轻松地将实时音视频功能集成到你的应用或者网站中。

在创建会议时可以传入以下几种类型:

   1. Communication:普通通信会议,最多支持参会者6人,会议里的每个参会者都可以自由说话和发布视频,该会议类型在服务器不做语音的再编码,音质最好,适用于远程医疗,在线客服等场景;
   2. Large Communication:大型通信会议,最多参会者30人,会议里的每个参会者都可以自由说话,最多支持6个人发布视频,该会议模式在服务器做混音处理,支持更多的人说话,适用于大型会议等场景;
   3. Live:互动视频会议,会议里支持最多6个主播和600个观众,观众可以通过连麦与主播互动,该会议类型适用于在线教育,互动直播等场景。

产品特性

  • SDK采用模块化设计,极简的 API 设计,方便用户使用单一模块实现特定功能;
  • 适配主流 iOS 设备和 主流 Android 终端设备,保证用户体验一致;
  • 支持手机端和 Web 端互通,极大方便开发者的全平台业务;
  • 视频码率(带宽占用)自适应的,非固定值,会自动根据网络拥塞情况调整清晰度,码率,帧率等。各个分辨率的码率范围如下:
      240p:150k~400kbps,推荐 300kbps
      480p:300k~1Mbps,推荐 500kbps
      720p:900k ~2.5Mbps,推荐 1Mbps
      1080p: 2M ~ 5Mbps,推荐 3Mbps

音视频通信的简要步骤

SDK 能够支持音频和视频通信。创建音视频通信的过程简单来说,可以分为以下几步:

  1. 初始化 SDK,设置监听代理
  2. create: 创建会议
  3. join: 加入会议
  4. pub: 发布音视频数据流
  5. sub: 订阅并播放音视频数据流
  6. leave: 离开会议

基本知识

IEMConferenceManager.h:多人音视频操作接口

EMConferenceManagerDelegate:会议监听类,会回调会议成员变化,会议数据流变化等,需要在代码中设置监听

EMConferenceType:多人会议类型

  1. Communication:普通通信会议,最多支持参会者6人,成员都可以自由说话和发布视频,成员角色Speaker
  2. Large Communication:大型通信会议,最多参会者30人,成员都可以自由说话和发布视频,成员角色Speaker
  3. Live:互动视频会议,会议里支持最多6个主播和600个观众

EMCallConference:会议类,用户可以维护SDK返回的实例,不允许进行copy和new

  注意: EMCallConference中会出现两个ID属性,分别是callId和confId,两个ID都是标识符,callId是本地生成,confId是服务器端生成,邀请或者加入所需要的均为confId 

EMCallMember:会议成员类,用户可以维护SDK返回的实例,不允许进行copy和new

EMCallStream:会议中的数据流类,包含音频数据和视频数据,用户可以维护SDK返回的实例,不允许进行copy和new

EMStreamParam:自己发布的数据流的各种配置,需要在调用发布接口时作为参数传入

EMConferenceRole:多人会议成员角色

  1. 观众Audience:只能观看收听音视频,即只能订阅流
  2. 主播Speaker:能上传自己的音视频,能观看收听其他主播的音视频,即能发布流和订阅流
  3. 管理员Admin:能创建会议,销毁会议,移除会议成员,切换其他成员的角色,订阅流,发布流
  
  注意:
  >> 每个人必须调用join接口成功后,才算是加入会议(即成为会议成员)。会议成员才允许进行其他操作比如订阅流、发布流等
  >> 成员如果想改变自己角色,必须想办法通知管理员,只有管理员才能修改

如何使用SDK实现多人实时音视频会议

Communication和Large Communication除了最大成员数不一样,流程几乎是一样的。以下是从创建会议到离开会议完整的流程讲解:

注册监听

进入会议之前,调用[IEMConferenceManager addDelegate:delegateQueue:]方法指定回调监听对象,在该方法中:

  ∵ 指定一个 delegate 对象,SDK 通过指定的 delegate 通知应用程序 SDK 的运行事件,如:成员加入或离开会议,数据流更新等。
  ∵ 回调方法在哪个队列中调用
//Objective-C
[[EMClient sharedClient].conferenceManager addDelegate:self delegateQueue:nil];

用户A创建会议

SDK没有提供单独的创建接口,只提供了一个createAndJoin接口,A调用该接口后,将拥有一个会议实例Conference,并且A已经是Conference的成员且角色是Admin

//Objective-C
- (void)createAndJoinConference
{
    __weak typeof(self) weakself = self;
    void (^block)(EMCallConference *aCall, NSString *aPassword, EMError *aError) = ^(EMCallConference *aCall, NSString *aPassword, EMError *aError) {
        if (aError) {
            //错误提示
            return ;
        }
            
        //更新页面显示
    };
        
    EMConferenceType type = EMConferenceTypeCommunication;
    //EMConferenceType type = EMConferenceTypeLargeCommunication;
    [[EMClient sharedClient].conferenceManager createAndJoinConferenceWithType:type password:@"password" completion:block];
}
  注意: 如果A只单纯的create,没进行join操作,则A不是Conference的成员,没有相应的角色,不能进行其他操作

管理员A邀请其他人加入会议

SDK没有提供邀请接口,你可以自己实现,比如使用环信IM通过发消息邀请,比如通过发邮件邀请等等。

至于需要发送哪些邀请信息,可以参照SDK中的join接口,目前是需要Conference的confrId和password

比如用环信IM发消息邀请

//Objective-C
- (void)inviteUser:(NSString *)aUserName
{
    NSString *confrId = self.conference.confId;
    NSString *password = self.password;
    EMConferenceType type = self.type;
    NSString *currentUser = [EMClient sharedClient].currentUsername;
    EMTextMessageBody *textBody = [[EMTextMessageBody alloc] initWithText:[[NSString alloc] initWithFormat:@"%@ 邀请你加入直播室: %@", currentUser, confrId]];
    EMMessage *message = [[EMMessage alloc] initWithConversationID:aUserName from:currentUser to:aUserName body:textBody ext:@{@"em_conference_op":@"invite", @"em_conference_id":confrId, @"em_conference_password":password, @"em_conference_type":@(type)}];
    message.chatType = EMChatTypeChat;
    [[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:nil];
}
  注意:使用环信IM邀请多个人时,建议使用群组消息。如果使用单聊发消息请注意每条消息中间的时间间隔,以防触发环信的垃圾消息防御机制

用户B接收到邀请加入会议

用户B解析出邀请消息中带的confrId和password,调用SDK的join接口加入会议,成为会议成员且角色是Speaker.

//Objective-C
- (void)joinConferenceWithConfrId:(NSString *)aConfrId password:(NSString *)aPassword
{
    __weak typeof(self) weakself = self;
    void (^block)(EMCallConference *aCall, EMError *aError) = ^(EMCallConference *aCall, EMError *aError) {
        if (aError) {
            //错误提示
            return ;
        }
            
        //更新页面显示
    };
        
    [[EMClient sharedClient].conferenceManager joinConferenceWithConfId:aConfrId password:aPassword completion:block];
}

用户B成功加入会议后,会议中其他成员会收到回调[EMConferenceManagerDelegate memberDidJoin:member:]

//Objective-C
- (void)memberDidJoin:(EMCallConference *)aConference member:(EMCallMember *)aMember
{
    if ([aConference.callId isEqualToString: self.conference.callId]) {
        NSString *message = [NSString stringWithFormat:@"用户 %@ 加入了会议", aMember.memberName];
        [self showHint:message];
    }
}

成员A发布音视频流

成员A和成员B都有发布流的权限,可以调用SDK的publish接口发布流,该接口用到了EMStreamParam参数,你可以自由配置,比如是否上传视频,是否上传音频,使用前置或后置摄像头,视频码率,显示视频页面等等

//Objective-C
- (void)pubLocalStream
{
    EMStreamParam *pubConfig = [[EMStreamParam alloc] init];
    pubConfig.streamName = @"自己的音视频数据流";
    pubConfig.enableVideo = YES; //是否上传视频数据
    pubConfig.localView = self.localVideoView; //视频显示页面
    pubConfig.isFixedVideoResolution = YES; //是否固定视频分辨率
    pubConfig.videoResolution = EMCallVideoResolution640_480; //视频分辨率
    pubConfig.maxVideoKbps = 0; //最大视频码率
    pubConfig.maxAudioKbps = 0; //最大音频码率
    pubConfig.isBackCamera = NO; //是否使用后置摄像头
        
    __weak typeof(self) weakself = self;
    [[EMClient sharedClient].conferenceManager publishConference:self.conference streamParam:pubConfig completion:^(NSString *aPubStreamId, EMError *aError) {
        if (aError) {
            //显示错误信息
        } else {
            //更新页面显示
        }
    }];
}
  注意:如果是纯音频会议,只需要在发布数据流时将EMStreamParam中的enableVideo置为NO即可

其他成员收到通知并订阅流

成员A成功发布数据流后,会议中其他成员会收到监听类回调[EMConferenceManagerDelegate streamDidUpdate:addStream:],如果成员B想看成员A的音视频,可以调用subscribe接口进行订阅

//Objective-C
- (void)streamDidUpdate:(EMCallConference *)aConference addStream:(EMCallStream *)aStream
{
    if ([aConference.callId isEqualToString:self.conference.callId]) {
        [self subStream:aStream];
    }
}
    
- (void)subStream:(EMCallStream *)aStream
{
    //构建数据流视频显示的页面,如果不支持视频可以传nil
    EMCallRemoteView *remoteView = [[EMCallRemoteView alloc] initWithFrame:frame];
    remoteView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    remoteView.scaleMode = EMCallViewScaleModeAspectFill;
    [self.scrollView addSubview:remoteView];
        
    __weak typeof(self) weakSelf = self;
    [[EMClient sharedClient].conferenceManager subscribeConference:self.conference streamId:aStream.streamId remoteVideoView:remoteView completion:^(EMError *aError) {
        if (aError) {
            //提示错误信息
        }
    }];
}

成员A设置自己发布的流

成员A成功的发布了自己的音视频流,在会议过程中,成员A可以进行以下操作:

  >> 切换前后摄像头: updateConferenceWithSwitchCamera:
  >> 开关静音,即订阅成员是否能听到A的声音:updateConference:isMute:
  >> 开关视频,即订阅成员是否能看到A的视频:updateConference:enableVideo:
  >> 重置视频显示页面:updateConference:streamId:remoteVideoView:completion:

当成员A对自己的数据流做以上操作成功后,会议中的其他成员会收到回调[EMConferenceManagerDelegate streamDidUpdate:stream:]

//Objective-C
- (void)streamDidUpdate:(EMCallConference *)aConference stream:(EMCallStream *)aStream
{
    if ([aConference.callId isEqualToString:self.conference.callId] && aStream != nil) {
        //判断本地缓存的EMCallStream实例与aStream有哪些属性不同,并做相应更新
    }
}

成员B取消订阅流

成员B如果不想再看成员A的音视频,可以调用SDK接口unsubscribe

//Objective-C
- (void)unsubStream
{
    __weak typeof(self) weakself = self;
    [[EMClient sharedClient].conferenceManager unsubscribeConference:self.conference streamId:self.pubStreamId completion:^(EMError *aError) {
        //code
    }];
}

成员A取消发布流

成员A可以调用unpublish接口取消自己已经发布的数据流,操作成功后,会议中的其他成员会收到回调[EMConferenceManagerDelegate streamDidUpdate:removeStream:] ,将对应的数据流信息移除

//Objective-C
- (void)unpubStream
{
    __weak typeof(self) weakself = self;
    [[EMClient sharedClient].conferenceManager unpublishConference:self.conference streamId:self.pubStreamId completion:^(EMError *aError) {
        //code
    }];
}

成员B离开会议

成员B调用SDK接口leave离开会议,会议中的其他成员会收到回调[EMConferenceManagerDelegate memberDidLeave:member:]

//Objective-C
- (void)memberDidLeave:(EMCallConference *)aConference member:(EMCallMember *)aMember
{
    if ([aConference.callId isEqualToString:self.conference.callId]) {
        NSString *message = [NSString stringWithFormat:@"成员 %@ 已经离开会议", aMember.memberName];
        [self showHint:message];
    }
}

互动视频会议的基本操作(创建、邀请人、发布流、取消发布流、订阅流、取消订阅流、更新发布流程、离开)对应的接口和回调同通信会议是一样的。也可以说 互动视频会议是在通信会议的基础上,增加了角色管理功能,以下着重讲解互动视频会议中的角色管理相关知识点

1. 创建互动视频会议时,接口[IEMConferenceManager createAndJoinConferenceWithType:password:completion:]传入的type参数是EMConferenceTypeLive

2. 创建者createAndJoin后的角色是Admin,其他成员第一次调用接口[IEMConferenceManager joinConferenceWithConfId:password:completion:]加入直播后的权限是观众Audience,Audience只能订阅数据流

3. 观众Audience如果想发布数据流 即上麦,需要给管理员发申请。SDK没有提供申请接口,你可以自定义。

管理员如果同意Audience上麦,需要调用接口[IEMConferenceManager changeMemberRoleWithConfId:memberNames:role:]将角色Audience更改为Speaker

//Objective-C
- (void)changeRole:(NSString *)aMemberName
{
    [[EMClient sharedClient].conferenceManager changeMemberRoleWithConfId:self.conference.confId memberNames:@[aMemberName] role:EMConferenceRoleSpeaker completion:^(EMError *aError) {
        //code
    }];
}

成员角色改变后,被改变的成员会收到回调

//Objective-C
- (void)roleDidChanged:(EMCallConference *)aConference
{
    //根据不同权限调整操作
}

4. 角色从Audience变为Speaker,成员就可以发布数据流了

  注意:
      >> MemberName和UserName是两个不同的概念,UserName是环信ID,MemberName是环信AppKey和环信ID拼接成的字符串,可通过接口[IEMConferenceManager getMemberNameWithAppkey:username:]获取
      >> 接口中的MemberName参数都是一样的类型

上一页:实时通话

下一页:多设备