多人音视频会议


多人音视频采用的是媒体流发布和订阅的技术架构。发布是指参会者发布媒体流(即发言,包括视频流和音频流)到服务器,其他人收到发布事件然后去订阅拉取媒体流。

多人音视频里有管理员,主播和观众三种角色。

  • 管理员拥有最高权限,可以发布媒体流,订阅媒体流,设定其他人是主播还是观众
  • 主播可以发布媒体流,订阅媒体流
  • 观众只有订阅媒体流权限

环信多人音视频有2种比较常见的使用模式:多人音视频会议和多人音视频互动直播。2种模式使用的是相同的技术架构,开发者可以根据业务场景设置不同的参数,主要区别是:

  • 多人音视频会议场景中,在创建会议时指定默认角色为主播,即每个参会者加入会议后都可以发言。开发者也可以根据场景需要,设定一些参会者是以观众角色加入会议。
  • 多人音视频互动直播场景中,在创建会议时指定默认角色为观众,除了管理员(同时也可以是主播)以外,其他人默认是观众。管理员可以设置指定观众成主播实现上麦操作;或设置指定主播为观众,实现下麦操作。

多人音视频会议和多人音视频互动直播模式都支持白板、共享桌面。

注意:会议类型将只支持Communication类型,原Large Communication和Live会议类型将弃用。

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

多人音视频会议主要涉及到的环信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没有提供单独的创建接口,只提供了一个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;
    
    /*!
    *  \~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];
}

管理员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];
    }
}

获取会议信息

在会议进行中,可以通过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;

如果用户需要自己采集特定的数据或者对于数据需要先进行一些处理,如变声、美颜等,可以使用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

[[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];

//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];

上一页:1V1实时通话

下一页:多设备