这是本文档旧的修订版!


iOS集成多人通话


多人音视频会议主要涉及到的环信SDK头文件如下:

// 多人会议部分,包含会议id,会议类型等
EMCallConference.h

// 数据流部分,包含数据流id,上传数据流的成员名称等
EMCallStream.h

// 多人实时通话会议方法调用部分,比如添加代理,移除代理,创建并加入会议,上传数据流,订阅其他人的数据流等
IEMConferenceManager.h

// 多人实时通话会议的协议回调方法部分,比如监听有人加入会议,有新的数据流上传回调方法等
EMConferenceManagerDelegate.h

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

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

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

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

EMConferenceType:多人会议类型,目前已经进行优化,请使用Communication类型

  1. Communication:普通通信会议,最多支持参会者9人,成员都可以自由说话和发布视频,成员角色Speaker
  2. Large Communication:大型通信会议,最多参会者30人,成员都可以自由说话和发布视频,成员角色Speaker<
  3. Live:互动视频会议,会议里支持最多9个主播和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没有提供单独的创建接口,提供了createAndJoinConference和joinRoom接口,A调用该接口后,将拥有一个会议实例Conference,同时A将成为该Conference的成员且角色是Admin.

用户创建会议时可以设置参数指定是否支持小程序音视频,是否需要在服务器端录制,录制时是否合并流

  注意: 
  创建加入会议有两组api: createAndJoinConference 和 joinRoom. 
  joinConference 是通过会议ID,密码方式加入会议,而joinRoom是通过房间名称加入。
  创建会议成功以后,默认超时时间为三分钟,超过三分钟没有人加入,会议会自动销毁;另外当会议中所有人离开2分钟后,会议也会被销毁。
  joinRoom api在会议不存在会自动去创建。
//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;
    
    /*!
    *  \~chinese
    *  创建并加入会议
    *
    *  @param aType             会议类型
    *  @param aPassword         会议密码
    *  @param isRecord             是否开启服务端录制
    *  @param isMerge              录制时是否合并数据流
    *  @param aCompletionBlock  完成的回调
    *
    */
    - (void)createAndJoinConferenceWithType:(EMConferenceType)aType
                               password:(NSString *)aPassword
                                 record:(BOOL)isRecord
                            mergeStream:(BOOL)isMerge
                                completion:(void (^)(EMCallConference *aCall, NSString *aPassword, EMError *aError))aCompletionBlock;

    // record 与 mergeStream 根据自己场景需求设置
    [[EMClient sharedClient].conferenceManager createAndJoinConferenceWithType:type password:@"password" record:NO mergeStream:NO completion:block];
}

如果想在创建会议时指定会议中的最大视频数、最大主播数,或开启cdn推流,可以使用带RoomCofnig参数的创建会议接口,此时会议类型使用aType而不是roomconfig中指定的会议类型。接口如下:

/*!
 *  \~chinese
 *  创建并加入会议
 *
 *  @param aType             会议类型
 *  @param aPassword         会议密码
 *  @param aConfrConfig   会议属性配置
 *  @param aCompletionBlock  完成的回调
 *
 *  \~english
 *  Create and join a conference
 *
 *  @param aType             The type of the conference
 *  @param aPassword         The password of the conference
 *  @param aConfrConfig   The config of conference
 *  @param aCompletionBlock  The callback block of completion
 */
- (void)createAndJoinConferenceWithType:(EMConferenceType)aType
                               password:(NSString *)aPassword
                            confrConfig:(RoomConfig*)aConfrConfig
                             completion:(void (^)(EMCallConference *aCall, NSString *aPassword, EMError *aError))aCompletionBlock;

加入房间

用户可以根据房间名和密码,使用加入房间的接口,快速加入一个会议,若该会议不存在,服务器将会自动创建。使用加入房间的接口,加入会议后,其他会议操作与使用createAndJoin接口加入会议后的操作完全相同。

加入房间使用的接口在IEMConferenceManager.h中,用户加入时可以选择使用的角色为观众还是主播,主播可以发布自己的音视频,观众只能订阅音视频。

加入房间有基础版和高级版两个API。基础API只能加入默认的会议选项,不支持服务器录制,使用API如下:

//Objective-C
-(void)joinRoom:(NSString*)roomName
          password:(NSString*)aPassword
                    role:(EMConferenceRole)role
        completion:(void (^)(EMCallConference *aCall, EMError *aError))aCompletionBlock;

高级版API接口加入房间时,可以选择房间的会议类型,是否开启服务端录制,是否混音以及是否支持小程序。API如下

//Objective-C
-(void)joinRoom:(NSString*)roomName
         password:(NSString*)aPassword
                  role:(EMConferenceRole)role
     roomConfig:(RoomConfig*)roomConfig
      completion:(void (^)(EMCallConference *aCall, EMError *aError))aCompletionBlock;

其中RoomConfig定义如下:

//Objective-C
@interface RoomConfig:NSObject
/*!
*  \~chinese
*  会议类型
*
*  \~english
*  The  type of conference
*/
@property (nonatomic) EMConferenceType confrType;
/*!
*  \~chinese
*  录制时是否合并数据流
*
*  \~english
* Whether to merge data streams while recording
*/
@property (nonatomic) BOOL isMerge;
/*!
*  \~chinese
*  是否开启服务端录制
*
*  \~english
*  Whether to record data streams
*/
@property (nonatomic) BOOL isRecord;
/*!
*  \~chinese
*  是否支持微信小程序
*
*  \~english
*  Weather to support wechat mini program
*/
@property (nonatomic) BOOL isSupportWechatMiniProgram;
/*!
*  \~chinese
*  会议中使用的昵称
*
*  \~english
*  The nickName userd in conference
*/
@property (nonatomic) NSString* nickName;
/*!
*  \~chinese
*  成员扩展信息
*
*  \~english
*  The extension info of member
*/
@property (nonatomic) NSString* ext;
/*!
*  \~chinese
*  会议最大主播数
*
*  \~english
*  The  limit count of talkers
*/
@property (nonatomic) NSInteger maxTalkerCount ;
/*!
*  \~chinese
*  会议最大视频上传数
*
*  \~english
*  The  limit count of video streams
*/
@property (nonatomic) NSInteger maxVideoCount;
/*!
*  \~chinese
*  会议最大观众数
*
*  \~english
*  The  limit count of audience
*/
@property (nonatomic) NSInteger maxAudienceCount;
/*!
*  \~chinese
*  cdn 直播推流配置
*
*  \~english
*  The  cdn live config
*/
@property (nonatomic) LiveConfig* liveConfig;
@end

注:RoomConfig中的ext表示会议成员的扩展信息,一般用于存储头像等信息,与pub流的接口中的ext信息是不同的

管理员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即可

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

成员成功发布数据流后,会议中其他成员会收到监听类回调[EMConferenceManagerDelegate streamDidUpdate:addStream:],如果需要关注某一条流,可以调用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) {
            //提示错误信息
        }
    }];
}

操作自己发布的流

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

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

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

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

操作自己订阅的流

对于订阅成功的流,可以有以下操作:

/**
 * \~chinese
 * mute远端音频
 *
 * @param aStreamId        要操作的Steam id
 * @param isMute            是否静音
 *
 * \~english
 * Mute remote audio
 *
 * @param aStreamId        Steam id
 * @param isMute            is mute
 */
- (void)muteRemoteAudio:(NSString *)aStreamId mute:(BOOL)isMute;

/**
 * \~chinese
 * mute远端视频
 *
 * @param aStreamId        要操作的Steam id
 * @param isMute            是否显示
 *
 * \~english
 * Mute remote video
 *
 * @param aStreamId        Steam id
 * @param isMute            is mute
 */
- (void)muteRemoteVideo:(NSString *)aStreamId mute:(BOOL)isMute;

取消订阅流

成员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
{
    [[EMClient sharedClient].conferenceManager unpublishConference:self.conference 
                                                          streamId:self.pubStreamId completion:^(EMError *aError) 
                                                          {
                                                              //code
                                                          }];
}

成员B离开会议

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

[[EMClient sharedClient].conferenceManager leaveConference:aCall completion:nil];
//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];
    }
}

管理员销毁会议

管理员离开会议时,可以选择离开会议或销毁会议,销毁会议后,其他成员都将收到会议结束的回调[EMConferenceManagerDelegate conferenceDidEnd:reason:error];离开会议后,若当前会议中无管理员,服务器会将管理员权限随机分配给某个主播。只有管理员权限可以进行销毁会议操作。

销毁会议使用接口

[[EMClient sharedClient].conferenceManager destroyConferenceWithId:aConfId completion:nil];

离开会议使用接口

[[EMClient sharedClient].conferenceManager leaveConference:aConference completion:nil];

当会议中的成员都离开会议后,服务器会自动将会议结束销毁

会议结束

当会议结束时,成员将收到会议结束的回调[EMConferenceManagerDelegate conferenceDidEnd:reason:error],该回调的详细接口如下:

/*!
 *  \~chinese
 *  会议已经结束
 *
 *  @param aConference       会议实例
 *  @param aReason           结束原因
 *  @param aError            错误信息
 *
 *  \~english
 *  The conference is over
 *
 *  @param aConference       EMConference instance
 *  @param aReason           The end reason
 *  @param aError            The error
 */
- (void)conferenceDidEnd:(EMCallConference *)aConference
                  reason:(EMCallEndReason)aReason
                   error:(EMError *)aError;

EMCallEndReason表示会议结束的原因,管理员正常结束会议为EMCallEndReasonDestroy,被踢出会议为EMCallEndReasonBeenkicked

踢人

管理员可以强制会议成员离开会议,使用接口

/*!
 *  \~chinese
 *  踢人,需要管理员权限
 *
 *  @param aConfId           会议ID(EMCallConference.confId)
 *  @param aMemberNameList   成员名列表
 *  @param aCompletionBlock  完成的回调
 *
 *  \~english
 *  Kick members, requires administrator privileges
 *
 *  @param aConfId           Conference ID (EMCallConference.confId)
 *  @param aMemberNameList   Member Name list
 *  @param aCompletionBlock  The callback block of completion
 */
- (void)kickMemberWithConfId:(NSString *)aConfId
                 memberNames:(NSArray<NSString *> *)aMemberNameList
                  completion:(void (^)(EMError *aError))aCompletionBlock;

管理员变更

当会议中的普通成员成为管理员,或管理员降级为普通成员时,会议中的其他成员将收到管理员变更的回调。管理员变更回调分为管理员新增和管理员移除,回调接口如下:

/*!
 *  \~chinese
 *  管理员新增
 *
 *  @param aConference       会议实例
 *  @param adminmemid         新的管理员memid
 *
 *  \~english
 *  The admin has added
 *
 *  @param aConference       EMConference instance
 *  @param adminmemid         The new admin memid
 */
- (void)adminDidChanged:(EMCallConference *)aConference
               newAdmin:(NSString*)adminmemid;

/*!
 *  \~chinese
 *  管理员放弃
 *
 *  @param aConference       会议实例
 *  @param adminmemid         放弃管理员的memid
 *
 *  \~english
 *  The admin has removed
 *
 *  @param aConference       EMConference instance
 *  @param adminmemid         The removed admin memid
 */
- (void)adminDidChanged:(EMCallConference *)aConference
            removeAdmin:(NSString*)adminmemid;

获取会议信息

在会议进行中,可以通过getConferenceInfo 方法来查询会议信息,从而可以拿到主播列表,观众人数等信息。

/*!
 *  \~chinese
 *  判断会议是否存在
 *
 *  @param aConfId           会议ID(EMCallConference.confId)
 *  @param aPassword         会议密码
 *  @param aCompletionBlock  完成的回调
 *
 *  \~english
 *  Determine if the conference exists
 *
 *  @param aConfId           Conference ID (EMCallConference.confId)
 *  @param aPassword         The password of the conference
 *  @param aCompletionBlock  The callback block of completion
 */
- (void)getConference:(NSString *)aConfId
             password:(NSString *)aPassword
           completion:(void (^)(EMCallConference *aCall, EMError *aError))aCompletionBlock;

管理会议角色

管理员可以对其他观众、主播的角色进行升级、降级处理,接口如下:

/*!
 *  \~chinese
 *  改变成员角色,需要管理员权限
 * 用户角色: Admin > Talker > Audience
 * 当角色升级时,用户需要给管理员发送申请,管理通过该接口改变用户接口.
 * 当角色降级时,用户直接调用该接口即可.
 *
 *  @param aConfId           会议ID(EMCallConference.confId)
 *  @param aMember        成员
 *  @param aRole             成员角色
 *  @param aCompletionBlock  完成的回调
 *
 *  \~english
 *  Changing member roles, requires administrator privileges
 * Role: Admin > Talker > Audience
 * When role upgrade, you need to send a request to Admin, only Admin can upgrade a role.
 * When role degrade, you can degrade with this method yourself.
 *
 *  @param aConfId           Conference ID (EMCallConference.confId)
 *  @param aMember        Member
 *  @param aRole             The Role
 *  @param aCompletionBlock  The callback block of completion
 */
- (void)changeMemberRoleWithConfId:(NSString *)aConfId
                            member:(EMCallMember *)aMember
                              role:(EMConferenceRole)toRole
                        completion:(void (^)(EMError *aError))aCompletionBlock;

全体静音/解除全体静音

管理员可以对会议进行全体静音/解除全体静音设置,设置后,会议中的主播都将处于静音状态,新加入的主播也将自动处于静音状态。只有管理员可以调用此接口。 接口API如下:

/**
* \~chinese
* 开启/停止全体静音,只有管理员可调用此接口
*
* @param enable 是否启用全体静音
* @param completion 回调
*
* \~english
* start/stop mute all members
* @params enable whether to start
* @params completion the callback functio
*/
- (void)muteAll:(BOOL)mute
     completion:(void(^)(EMError *aError))aCompletionBlock;

管理员调用此接口后,会议中的主播将收到全体静音状态的回调,回调函数如下

/*!
 * \~chinese
 * 收到全体静音/解除全体静音的回调
 *
 * @param aConference     会议
 * @param aMuteAll   是否全体静音
 *
 *\~english
 * callback when admin set muteAll/unmuteAll
 *
 * @param aConference     EMCallConference instance
 * @param aMuteAll  Weather muteAll or not
*/
- (void)conferenceDidUpdated:(EMCallConference *)aConference
                  muteAll:(BOOL)aMuteAll;

指定成员静音/解除静音

管理员可以对会议中的指定成员进行静音/解除静音设置,被指定成员可以是主播也可以是管理员。设置后,被指定成员将静音/解除静音。只有管理员可以调用此接口。 接口API如下:

/*!
*  \~chinese
*  将指定成员静音/解除静音,管理员调用
*
*  @param aCall             会议实例(自己创建的无效)
*  @param aMemId 指定成员的memId
*  @param aMute 操作,YES为静音,NO为解除静音
*  @param aCompletionBlock 回调函数
*
*  \~english
*  mute/unmute one member,only admin call this function
*
*  @param aCall             EMConference instance (invalid by yourself)
*  @param aMemId The memid of member
*  @param aMute Opereation,YES means mute,and NO means unmute
*  @param aCompletionBlock The callback function
 */
- (void)setMuteMember:(EMCallConference *)aCall
                memId:(NSString *)aMemId
                 mute:(BOOL)aMute
           completion:(void (^)(EMError *aError))aCompletionBlock;

管理员调用此接口后,被指定的成员将收到静音状态的回调,回调函数如下

/*!
 * \~chinese
 * 收到静音/解除静音的回调
 *
 * @param aConference     会议
 * @param aMute   是否静音
 *
 *\~english
 * callback when recv mute command
 *
 * @param aConference     EMCallConference instance
 * @param aMute  Weather mute or not
*/
- (void)conferenceDidUpdated:(EMCallConference*)aConference
                        mute:(BOOL)aMute;

观众申请主播

会议中的观众角色可以向管理员发申请成为主播,管理员可以选择同意或者拒绝。观众申请主持人的接口需要管理员的memId,先通过获取会议属性接口获取到管理员的memName,然后根据memName以及成员加入的回调中获取到的EMCallMember,获取到memId。接口如下

/*!
*  \~chinese
*  观众申请连麦成为主播,观众角色调用
*
*  @param aCall             会议实例(自己创建的无效)
*  @param aAdminId 管理员的memId
*  @param aCompletionBlock 回调函数
*
*  \~english
*  Request tobe Speaker,only audience call this function
*
*  @param aCall             EMConference instance (invalid by yourself)
*  @param aAdminId The memid of admin
*  @param aCompletionBlock The callback function
*/
- (void)requestTobeSpeaker:(EMCallConference *)aCall adminId:(NSString *)aAdminId completion:(void (^)(EMError *aError))aCompletionBlock;

观众发出申请后,管理员将会收到以下回调:

/*!
 * \~chinese
 * 收到观众申请主播的请求,只有管理员会触发
 *
 * @param aConference     会议
 * @param aMemId   申请人memId
 * @param aNickName 申请人昵称
 * @param aMemName 申请人memName
 *
 *\~english
 * callback when admin recv the request of become speaker
 *
 * @param aConference     EMCallConference instance
 * @param aMemId   The memId of requster
 * @param aNickName  The nickname of requster
 * @param aMemName  The memname of requster
*/
- (void)conferenceReqSpeaker:(EMCallConference*)aConference memId:(NSString*)aMemId nickName:(NSString*)aNickName memName:(NSString*)aMemName;

在回调中,管理员可以选择同意或者拒绝,如果同意,需要调用改变用户权限的接口,然后调用回复接口,如果拒绝,则直接调用回复接口。回复接口如下:

/*!
*  \~chinese
*  管理员同意/拒绝观众的上麦申请,管理员调用
*
*  @param aCall             会议实例(自己创建的无效)
*  @param aMemId 上麦申请的观众的memId
*  @param aResult 操作结果,0为同意,1为拒绝
*  @param aCompletionBlock 回调函数
*
*  \~english
*  Admin agree/disagree  the audience request tobe speaker.only admin call this function
*
*  @param aCall             EMConference instance (invalid by yourself)
*  @param aMemId The memid of member who requeset tobe speaker
*  @param aMute Opereation result,0 means agree,and 1 means disagree
*  @param aCompletionBlock The callback function
 */
- (void)responseReqSpeaker:(EMCallConference *)aCall
                     memId:(NSString *)aMemId
                    result:(NSInteger)aResult
                completion:(void (^)(EMError *aError))aCompletionBlock;

主播申请管理员

会议中的主播角色可以向管理员发申请成为管理员,管理员可以选择同意或者拒绝,成为管理员后,各管理员之间的权限是相同的。主播申请主持人的接口需要管理员的memId,先通过获取会议属性接口获取到管理员的memName,然后根据memName以及成员加入的回调中获取到的EMCallMember,获取到管理员memId。接口如下:

/*!
*  \~chinese
*  主播申请成为管理员,主播角色调用
*
*  @param aCall             会议实例(自己创建的无效)
*  @param aAdminId 管理员的memId
*  @param aCompletionBlock 回调函数
*
*  \~english
*  Request tobe Admin,only speraker call this function
*
*  @param aCall             EMConference instance (invalid by yourself)
*  @param aAdminId The memid of admin
*  @param aCompletionBlock The callback function
 */
- (void)requestTobeAdmin:(EMCallConference *)aCall adminId:(NSString *)aAdminId completion:(void (^)(EMError *aError))aCompletionBlock;

主播发出申请后,管理员将会收到以下回调:

/*!
 * \~chinese
 * 收到主播申请管理员的请求,只有管理员会触发
 *
 * @param aConference     会议
 * @param aMemId   申请人memId
 * @param aNickName 申请人昵称
 * @param aMemName 申请人memName
 *
 *\~english
 * callback when admin recv the request of become admin
 *
 * @param aConference     EMCallConference instance
 * @param aMemId   The memId of requster
 * @param aNickName  The nickname of requster
 * @param aMemName  The memname of requster
*/
- (void)conferenceReqAdmin:(EMCallConference*)aConference memId:(NSString*)aMemId nickName:(NSString*)aNickName memName:(NSString*)aMemName;

在回调中,管理员可以选择同意或者拒绝,如果同意,需要调用改变用户权限的接口,然后调用回复接口,如果拒绝,则直接调用回复接口。回复接口如下:

/*!
*  \~chinese
*  管理员同意/拒绝主播的申请管理员请求,管理员调用
*
*  @param aCall             会议实例(自己创建的无效)
*  @param aMemId 申请管理员的主播的memId
*  @param aResult 操作结果,0为同意,1为拒绝
*  @param aCompletionBlock 回调函数
*
*  \~english
*  Admin agree/disagree  the speaker request tobe admin.only admin call this function
*
*  @param aCall             EMConference instance (invalid by yourself)
*  @param aMemId The memid of speaker who request tobe admin
*  @param aMute Opereation result,0 means agree,and 1 means disagree
*  @param aCompletionBlock The callback function
 */
- (void)responseReqAdmin:(EMCallConference *)aCall memId:(NSString *)aMemId result:(NSInteger)aResult completion:(void (^)(EMError *aError))aCompletionBlock;

视频质量统计数据

使用[conferenceManager enableStatistics:YES]开启视频质量统计后,用户会周期性收到视频质量数据的回调,回调函数定义如下:

/*!
 * \~chinese
 * 当前会议的媒体流质量报告回调
 *
 * @param aConference     会议
 * @param streamId 流ID
 * @param aReport   会议的质量参数
 *
 *\~english
 * get the streamId of stream.callback when publish success
 *
 * @param aConference     EMCallConference instance
 * @param streamId streamId
 * @param aReport  The stat report of stream
*/
- (void)conferenceDidUpdate:(EMCallConference*)aConference streamId:(NSString*)streamId statReport:(EMRTCStatsReport *)aReport;

EMRTCStatsReport的详细信息参见apiDoc

如果用户需要自己采集特定的数据或者对于数据需要先进行一些处理,如变声、美颜等,可以使用SDK的外部输入数据的方法进行。

配置属性

使用外部输入视频数据接口前,需要先进行配置,配置参数为EMStreamParam中的enableCustomizeVideoData,设为YES可开启外部输入视频功能,开启后需要在publishConference的成功回调中开始视频数据采集。

__block NSString *publishId = @"";
EMStreamParam *streamParam = [[EMStreamParam alloc] init];
streamParam.enableCustomizeVideoData = YES; // 开启自定义视频流

[[EMClient sharedClient].conferenceManager publishConference:self.conference
                                                 streamParam:streamParam
                                                  completion:^(NSString *aPubStreamId, EMError*aError) {
       if(!aError) {
            publishId = aPubStreamId;
            // **TODO:这里开启视频数据采集过程**
            [self startCapture]
       } 
}];

在视频数据采集的回调中调用外部输入视频数据接口inputVideoSampleBuffer。

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    if ([self.delegate respondsToSelector:@selector(videoCaptureDataCallback:)])
    {
        // 输入视频流
        [[EMClient sharedClient].conferenceManager inputVideoSampleBuffer:sampleBuffer
                                                         rotation:orientation
                                                       conference:self.conference
                                                publishedStreamId:publishId
                                                       completion:^(EMError *aError) {
            }];
    }
}

在关闭视频传输或会议结束的回调中,停止视频数据的采集

//多人会议挂断触发事件
- (void)hangupAction
{
      [self stopCapture];
}

外部音频数据的参数除开关外,还需要配置采样率及通道数,目前通道数只支持1

EMStreamParam *streamParam = [[EMStreamParam alloc] init];
streamParam.enableCustomizeAudioData = YES; // 开启自定义音频流
streamParam.enableCustomizeAudioData.customAudioSamples = 48000;
streamParam.enableCustomizeAudioData.customAudioChannels = 1;
[[EMClient sharedClient].conferenceManager publishConference:self.conference
                                                 streamParam:streamParam
                                                  completion:^(NSString *aPubStreamId, EMError *aError) 
{

}];

外部输入音频数据的接口为

[[[EMClient sharedClient] conferenceManager] inputCustomAudioData:data];

开启和关闭过程与外部视频的接口使用一致。

使用sdk共享桌面,只能共享指定的view

EMStreamParam *streamParam = [[EMStreamParam alloc] init];
streamParam.type = EMStreamTypeDesktop;
streamParam.desktopView = self.view;
[[EMClient sharedClient].conferenceManager publishConference:self.conference
                                                 streamParam:streamParam
                                                  completion:^(NSString *aPubStreamId, EMError *aError) {
            
}];

因为上面方法的实现原理是不停的截取指定view的快照,并转换成流发送,这种方式的效率并不高,ios10以上用户建议使用系统的replaykit + 自定义输入流的方式实现共享桌面,具体replaykit使用可以参考官方文档

(需要集成3.6.3或以上版本的sdk)

特别指出,如果需要在共享桌面时输入自己的流,需要修改上一步中的设置:

EMStreamParam *streamParam = [[EMStreamParam alloc] init];
streamParam.type = EMStreamTypeDesktop;
streamParam.desktopView = nil; // 使用自定义输入流,此处需要传nil
streamParam.videoResolution = EMCallVideoResolution_Custom;//为了防止共享桌面被裁剪,需要使用自定义分辨率
CGFloat screenX = [UIScreen mainScreen].bounds.size.width;
CGFloat screenY = [UIScreen mainScreen].bounds.size.height;
streamParam.videoWidth = screenY;
streamParam.videoHeight = screenX;

[[EMClient sharedClient].conferenceManager publishConference:self.conference
                                                 streamParam:streamParam
                                                  completion:^(NSString *aPubStreamId, EMError *aError) {
   if(!aError) {
      publishId = aPubStreamId;
   } 
}];

// 输入视频流
[[EMClient sharedClient].conferenceManager inputVideoSampleBuffer:sampleBuffer
                                                         rotation:orientation
                                                       conference:self.conference
                                                publishedStreamId:publishId
                                                       completion:^(EMError *aError) {
            
}];

视频通话时,可以添加图片作为水印,添加时使用[IEMConferenceManager addVideoWatermark]接口,需要指定水印图片的NSUrl,添加位置参见EMWaterMarkOption

清除水印使用[IEMConferenceManager clearVideoWatermark]接口。

显示remoteVideo需要使用EMCallViewScaleModeAspectFit模式,否则对方的水印设在边缘位置可能显示不出来。

/*!
*  \~chinese
*  开启水印功能
*
*  @param option 水印配置项,包括图片URL,marginX,marginY以及起始点
*
*  \~english
*  Enable water mark feature
*
*  @param option the option of watermark picture,include url,margingX,marginY,margin point
 */
- (void)addVideoWatermark:(EMWaterMarkOption*)option;
/*!
*  \~chinese
*  取消水印功能
*
*  \~english
*  Disable water mark feature
*
 */
- (void)clearVideoWatermark;
// 添加水印
NSString * imagePath = [[NSBundle mainBundle] pathForResource:@"watermark" ofType:@"png"];
EMWaterMarkOption* option = [[EMWaterMarkOption alloc] init];
option.marginX = 60;
option.startPoint = LEFTTOP;
option.marginY = 40;
option.enable = YES;
option.url = [NSURL fileURLWithPath:imagePath];
[[EMClient sharedClient].conferenceManager addVideoWatermark:option];

// 清除水印
[[EMClient sharedClient].conferenceManager clearVideoWatermark];

多人音视频支持不同集群区域的人员使用代理,减小延迟、丢包率。使用多集群代理需要音视频后台配置IP及端口的映射文件rtcconfig.json,sdk开启相应开关。 启用多集群代理功能开关如下:

[EMClient sharedClient].options.isUseRtcConfig = YES;//YES为开启,NO或不设置为不开启

多人音视频支持将会议中的音视频流合并成一个流,推送到第三方的cdn直播服务器。整个合流推流过程包括开启cdn推流,更新推流布局,停止推流。

开启cdn推流

会议的创建者在创建会议时使用RoomConfig的接口,可以决定是否开启cdn推流,推流配置LiveConfig是RoomConfig的一个参数,可设置cdn推流的相关信息。开启过程如下:

LiveConfig* liveconfig = [[LiveConfig alloc] init];
CDNCanvas* canvas = [[CDNCanvas alloc] init];
canvas.fps = 18;
canvas.kbps = 900;
canvas.codec = @"H264";
canvas.bgclr = 0x0000ff;
canvas.width = [EMDemoOption sharedOptions].liveWidth;
canvas.height = [EMDemoOption sharedOptions].liveHeight;
liveconfig.canvas = canvas;
liveconfig.cdnUrl = [EMDemoOption sharedOptions].cdnUrl;
liveconfig.layoutStyle = CUSTOM;
roomConfig.liveConfig = liveconfig;

LiveConfig可设置的参数如下:

/*!
*  \~chinese
*  cdn 画布设置,创建会议时使用
*
*  \~english
*  The cdn canvas config
*/
@interface CDNCanvas : NSObject
 /*! \~chinese 画布宽度 \~english The width of canvas */
@property (nonatomic) NSInteger width;
/*! \~chinese 画布高度 \~english The height of canvas */
@property (nonatomic) NSInteger height;
/*! \~chinese 画布的背景色,格式为 RGB 定义下的 Hex 值,不要带 # 号,如 0xFFB6C1 表示浅粉色。默认0x000000,黑色。
 * \~english The bgclr of canvas ,use  interger as 0x112233, 0x11 is red value,0x22 is green value,0x33 is blue value*/
@property (nonatomic) NSInteger bgclr;
/*! \~chinese 推流帧率,可设置范围10-30 \~english The fps of cdn live,valid value is 10-30 */
@property (nonatomic) NSInteger fps;
/*! \~chinese 推流码率,单位kbps,width和height较大时,码率需要提高,可设置范围1-5000 \~english The bps of cdn live. Unit is kbps,valid value is 1-5000 */
@property (nonatomic) NSInteger kbps;
/*! \~chinese 推流编码格式,目前只支持"H264" \~english The codec of cdn live,now only support "H264* codec */
@property (nonatomic) NSString* codec;

@end

/*!
*  \~chinese
*  cdn推流使用的画布类型
*
*  \~english
*  cdn  live layout style
*/
typedef NS_ENUM(NSInteger, LayoutStyle) {
    CUSTOM,
    DEMO,
    GRID
};
/*!
*  \~chinese
*  cdn推流设置
*
*  \~english
*  The cdn push stream config
*/
@interface LiveConfig : NSObject

/*! \~chinese 推流url地址\~english The url address of cdn live*/
@property (nonatomic,strong) NSString *cdnUrl;

/*! \~chinese 推流画布的配置\~english The config of live canvas*/
@property (nonatomic) CDNCanvas* canvas;

/*! \~chinese 推流方式,GRID或者CUSTOM,GRID将由服务器设置位置信息,CUSTOM将由用户自定义流的位置信息\~english The style of cdn live,GRID or CUSTOM.If GRID,server set region of streams.If CUSTOM,user set region of streams*/
@property (nonatomic) LayoutStyle layoutStyle;

@end

更新布局

当用户调用更新布局接口后,cdn推流方式将强制变成CUSTOM模式,所有流的位置信息都由用户自己定义。 更新布局的接口如下:

/**
* \~chinese
* 更新会议的cdn推流视频流布局
*
* @param aCall 会议对象
* @param aReagionList  视频流列表布局信息
* @param aCompletionBlock 回调
*
* \~english
* update stream regions in conference
* @params aCall whether to start
* @params aReagionList the stream regions array
* @param aCompletionBlock The callback function
*/
- (void)updateConference:(EMCallConference*)aCall
             setRegions:(NSArray<LiveRegion*>*)aReagionList
              completion:(void(^)(EMError *aError))aCompletionBlock;

LiveRegion的结构如下:

/*!
*  \~chinese
*  cdn推流的每一路流的模式
*
*  \~english
*  The style of stream in cdn live
*/
typedef NS_ENUM(NSInteger, LiveRegionStyle) {
    /*! \~chinese FIt模式\~english The fit content mode */
    LiveRegionStyleFit,
    /*! \~chinese FIll模式\~english The fill content mode */
    LiveRegionStyleFill
};

/*!
*  \~chinese
*  cdn推流的每一路流的区域位置信息
*
*  \~english
*  The region info of each stream in cdn live
*/
@interface LiveRegion : NSObject

/*! \~chinese 流ID \~english The stream Id */
@property (nonatomic) NSString* streamId;

/*! \~chinese 流的左上角在x轴坐标 \~english The pointX of left top */
@property (nonatomic) NSInteger x;

/*! \~chinese 流的左上角在y轴坐标 \~english The pointY of left top */
@property (nonatomic) NSInteger y;

/*! \~chinese 流的宽度 \~english The width of stream */
@property (nonatomic) NSInteger w;

/*! \~chinese 流的高度 \~english The height of stream */
@property (nonatomic) NSInteger h;

/*! \~chinese 流的图层顺序,越小越在底层,从1开始 \~english The zorder of stream,start from 1,the smaller is under others*/
@property (nonatomic) NSInteger z;

/*! \~chinese 流的显示模式,Fit或Fill \~english The content mode of stream,fit or fill*/
@property (nonatomic) LiveRegionStyle style;

@end

使用方法如下:

NSMutableArray<LiveRegion*>* regionsList = [NSMutableArray array];
LiveRegion* region = [[LiveRegion alloc] init];
region.streamId = _streamId;
region.style = LiveRegionStyleFill;
region.x = 80;
region.y = 60;
region.w = 320;
region.h = 240;
region.z = 9;
[regionsList addObject:region];
[[[EMClient sharedClient] conferenceManager] updateConference:[EMDemoOption sharedOptions].conference setRegions:regionsList completion:^(EMError *aError) {
    }];

停止推流

多人音视频支持停止向某一个地址的推流,停止推流接口如下:

/**
* \~chinese
* 删除直播推流
*
* @param aCall 会议对象
* @param aCompletionBlock 回调
*
* \~english
* stop cdn live in conference
* @params aCall whether to start
* @param aCompletionBlock The callback function
*/
- (void)deleteConferenceLive:(EMCallConference*)aCall
                  completion:(void(^)(EMError *aError))aCompletionBlock;
//Objective-C
// 开启/关闭音频传输
 [[[EMClient shareClient] conferenceManager] updateConference:(EMCallConference *)aCall
                                                       isMute:(BOOL)aIsMute];
 // 开启/关闭启视频传输
 [[[EMClient shareClient] conferenceManager] updateConference:(EMCallConference *)aCall
                                                  enableVideo:(BOOL)aEnableVideo];
 
 PS:以上这四个方法都是修改 stream,群里其他成员都会收到 EMConferenceListener.onStreamUpdate()回调
 
 // 切换摄像头
 [[[EMClient shareClient] conferenceManager] updateConferenceWithSwitchCamera:(EMCallConference *)aCall];
 // 更新展示远端画面的 view
 [[[EMClient shareClient] conferenceManager] updateConference:(EMCallConference *)aCall
                                                     streamId:(NSString *)aStreamId
                                              remoteVideoView:(EMCallRemoteVideoView *)aRemoteView
                                                   completion:(void (^)(EMError *aError))aCompletionBlock];
 
 // 开始监听说话者,参数为间隔时间
 [[[EMClient shareClient] conferenceManager] startMonitorSpeaker:(EMCallConference *)aCall
                                                    timeInterval:(long long)aTimeMillisecond
                                                      completion:(void (^)(EMError *aError))aCompletionBlock];
 
 // 停止监听说话者
 [[[EMClient shareClient] conferenceManager] stopMonitorSpeaker:(EMCallConference *)aCall];

 // 开启统计数据报告,用于debug视频过程中媒体流的具体参数,开启后会在[EMConferenceManagerDelegate conferenceDidUpdate:streamId:statReport]回调中读取到
 // 各项数据参数。
 [[EMClient sharedClient].conferenceManager enableStatistics:YES];