这是本文档旧的修订版!


Web集成多人通话


  • 支持现代浏览器:Chrome/50+、Safari/11+、Firefox;
  • 遵守:UMD通用模块规范,支持require导入;
  • 支持Promise;
  • 支持手机端和 Web 端互通,极大方便开发者的全平台业务;
  • 依赖https站点

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

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

emedia.mgr.ConfrType:多人会议类型

  1. Communication:普通通信会议,最多支持参会者9人,成员都可以自由说话和发布视频,成员角色Speaker
  2. Large Communication:大型通信会议,最多参会者30人,成员都可以自由说话和发布视频,成员角色Speaker
  3. Live:互动视频会议,会议里支持最多9个主播和600个观众
emedia.mgr.ConfrType = {
   COMMUNICATION: 10, //普通会议模式
   COMMUNICATION_MIX: 11, //大会议模式
   LIVE: 12, //直播模式
};
emedia.mgr.Role = {
    ADMIN: 7, // 能创建会议,销毁会议,移除会议成员,切换其他成员的角色,订阅流,发布流
    TALKER: 3, // 能上传自己的音视频,能观看收听其他主播的音视频,即能发布流和订阅流)
    AUDIENCE: 1 // 观众Audience:只能观看收听音视频,即只能订阅流
};
emedia.mgr.StreamType = {
    VIDEO:0,//普通视频
    DESKTOP:1,//共享桌面
    MIXVIDEO:2 //混音视频
};
//conference|confr
{
    confrId:"TS_X296786295944036352C27",
    id:"TS_X296786295944036352C27",
    password: "password123", 
    roleToken:"roleToken",
    ticket:"ticket",
    type:12
};
//member
{
    "ext":{ //emedia.mgr.joinConference(confrId, password, {role: 'admin'})/* 用户可自定义扩展字段*/);
        "role":"admin"
    },
    "id":"MS_X197721744293023744C19M197756407719972865VISITOR",
    "globalName":"easemob-demo#chatdemoui_yss000@easemob.com",
    "name": "yss000",
    "nickName": "yss000" //设置后会生效,不设置默认取name的值
}
//stream
{
    "id":"RTC2__Of_C19M197756407719972865VISITOR",
    "voff":0, //1 视频关闭 
    "aoff":0, //1 音频关闭 
    "memId":"MS_X197721744293023744C19M197756407719972865VISITOR",
    "owner": member ,//member对象
    "rtcId":"RTC1"
}
var islocated = stream.located(); //islocated true 本地媒体流
  
  注意:
  >> 每个人必须调用join接口成功后,才算是加入会议(即成为会议成员)。会议成员才允许进行其他操作比如订阅流、发布流等
  >> 成员如果想改变自己角色,必须想办法通知管理员,只有管理员才能修改

以下是从创建会议到离开会议完整的流程讲解:

引入SDK

// 可以使用easemob-webrtc 或者 easemob-emedia
// 使用easemob-webrtc
import webrtc from 'easemob-webrtc'
const emedia = webrtc.emedia

// 使用easemob-emedia
import emedia from 'easemob-emedia'

初始化SDK

// 初始化一些 sdk 的使用功能
emedia.config({
   restPrefix, //配置服务器域名、必填 比如: 'https://a1-hsb.easemob.com'
   appkey, // 配置appkey、必填
   ... 其他的一些配置
});

设置SDK回调

进入会议之前,设置SDK回调后,可获知成员加入或离开会议,数据流更新等。

//有人加入会议,其他人调用joinXX等方法,如果加入成功,已经在会议中的人将会收到
emedia.mgr.onMemberJoined = function (member) {
    
};
//有人退出会议
emedia.mgr.onMemberExited = function (member) {
    
};
//有媒体流添加;比如 自己调用了publish方法(stream.located() === true时),或其他人调用了publish方法。
emedia.mgr.onStreamAdded = function (member, stream) {

};
//有媒体流移除
emedia.mgr.onStreamRemoved = function (member, stream) {

};
//角色改变
emedia.mgr.onRoleChanged = function (role) {

};
//会议退出;自己主动退 或 服务端主动关闭;
emedia.mgr.onConferenceExit = function (reason, failed) {
    reason = (reason || 0);
    switch (reason){
        case 0:
            reason = "正常挂断";
            break;
        case 1:
            reason = "没响应";
            break;
        case 2:
            reason = "服务器拒绝";
            break;
        case 3:
            reason = "对方忙";
            break;
        case 4:
            reason = "失败,可能是网络或服务器拒绝";
            if(failed === -9527){
                reason = "失败,网络原因";
            }
            if(failed === -500){
                reason = "Ticket失效";
            }
            if(failed === -502){
                reason = "Ticket过期";
            }
            if(failed === -504){
                reason = "链接已失效";
            }
            if(failed === -508){
                reason = "会议无效";
            }
            if(failed === -510){
                reason = "服务端限制";
            }
            break;
        case 5:
            reason = "不支持";
            break;
        case 10:
            reason = "其他设备登录";
            break;
        case 11:
            reason = "会议关闭";
            break;
    }
};
//管理员变更
emedia.mger.onAdminChanged = admin => {} //admin 管理员信息

用户A创建会议

// confrType: 会议类型;password: 会议密码;rec:是否录制;recMerge:是否合并录制;supportWechatMiniProgram:会议是否支持微信小程序
emedia.mgr.createConference(confrType, password, rec, recMerge, supportWechatMiniProgram).then(function(confr){
  console.log(confr.confrId);
}).catch(function(error){
})
// 支持对象形式的参数
let params = {
     confrType, 
     password, 
     rec, 
     recMerge, 
     supportWechatMiniProgram,
     maxTalkerCount:2,//会议最大主播人数
     maxVideoCount:1, //会议最大视频数
     maxPubDesktopCount:1, //会议最大共享桌面数
     maxAudienceCount:100 //会议最大观众数
}

const confr = await emedia.mgr.createConference(params)
  注意: 如果A只单纯的create,没进行join操作,则A不是Conference的成员,没有相应的角色,不能进行其他操作
  

用户A进入会议

emedia.mgr.joinConferenceWithTicket(confrId, ticket, ext).then(function(confr){

}).catch(function(error){

})

用户也可以使用下列接口加入房间。可以指定房间的名称,并且在房间不存在时会自动创建。 另外可以在加入会议时指定角色。

/**
 * 创建房间并加入
 * @method joinRoom
 * @param {Object} option
 * @param {string} option.roomName - 房间名称
 * @param {string} option.password - 房间密码
 * @param {string} option.role - 进入房间时的角色
 * @param {object} option.config - 扩展能力 可设置以下参数
 * config:{ 
                nickName,//进入会议的昵称
                ext: {}, //扩展字段 用于自定义
                rec: true, // 开启录制
                recMerge: true, //开启录制合并
                supportWechatMiniProgram: true, //是否支持微信小程序
                maxTalkerCount:3 //自定义会议最大主播人数
                maxVideoCount:2 //自定义会议最大视频数
                maxAudienceCount:100 //自定义会议最大观众数
                maxPubDesktopCount: 1 //自定义会议共享屏幕最大数

           }
 */



        try {
            const user_room = await emedia.mgr.joinRoom(option);
            
            user_room: 
            {
                confrId: "IM3U9Z0AHDYQTF8KNDAAD00C147" 会议ID
                id: "IM3U9Z0AHDYQTF8KNDAAD00C147"
                joinId: "IM3U9Z0AHDYQTF8KNDAAD00C147M2" 在会议中的唯一ID
                mixed: false
                role: 7 //角色
                roleToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlYXNlbW9iLWRlbW8j..."
                ticket: "{\"tktId\":\"IM3U9Z0AHDYQTF8KNDAAD00C147TK1\",..."
                type: 10 //会议类型
             }
                       
        } catch (error) { 
            
        }

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

SDK 不提供邀请接口。邀请的形式,完全可以由用户自行定义,可以是条文本消息,也可以是个控制消息等等。SDK 不做限制。实现方式可以参考官方 demo,通过群组消息邀请,具体代码 可查询 demo/src/components/webrtc/AddAVMemberModal.js 中的 70-89 行。

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

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

//javascript
emedia.mgr.joinConference(confr, password, ext).then(function(confr){

}).catch(function(error){

})

用户B成功加入会议后,会议中其他成员会收到回调 [emedia.mgr.onMemberJoined(memberB)]

成员A发布音视频流

成员A和成员B都有发布流的权限

/**
 * constaints: {audio: true, video: true}
 * videoTag 可缺失,如果有 此次publish的媒体数据将会在这个video上显示 将会与stream绑定
 * ext 用户自定义扩展,其他成员可以看到这个字段
 * 
 */
emedia.mgr.publish(constaints, videoTag, ext).then(function(pushedStream){
    //stream 对象
}).catch(function(error){

});

/**
 * videoTag 可缺失时
 *
 */
emedia.mgr.publish(constaints, ext).then(function(pushedStream){
    //stream 对象
    //如果需要将这个stream对象 显示,需要 emedia.mgr.streamBindVideo(videoTag, pushedStream)
}).catch(function(error){

});
  • 注意:A推流成功后,onStreamAdded 将被回调 *

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

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

//其他成员
emedia.mgr.onStreamAdded = function (member, stream) {
   if(!stream.located()){ //stream.located() === true,         自己发送的数据流
      emedia.mgr.subscribe(member, stream, true, true, video)
   }
};

成员B取消订阅流

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

//emedia.mgr.hungup(stream);
emedia.mgr.unsubscribe(stream);

成员A取消发布流

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

//emedia.mgr.hungup(pushedStream);
emedia.mgr.unpublish(pushedStream);
emedia.mgr.onStreamRemoved = function (member, stream) {
   if(stream.located(){ //自己发布流
   }else{ //会议其他人发布的流
   }
};

成员B离开会议

成员B调用SDK接口exitConference离开会议,会议中的其他成员会收到回调[emedia.mgr.onMemberExited:member:]

emedia.mgr.exitConference();

共享桌面,仅支持PC Chrome浏览器或electron平台。

/**
 * let params = { 
 *       videoConstaints, 
 *       withAudio, 
 *       videoTag, 
 *       ext, 
 *       confrId,
 *       stopSharedCallback
 *  }
 */

/**
 * videoConstaints {screenOptions: ['screen', 'window', 'tab']} or true
 * withAudio: true 携带语音,false不携带  如携带语音,需自己调用关闭流,不会执行 stopSharedCallback 回调
 * ext 用户自定义扩展,其他成员可以看到这个字段
 * stopSharedCallback 共享插件 点击【停止共享】的回调函数,做相应的处理(比如删除流...)
 */
emedia.mgr.shareDesktopWithAudio(params).then(function(pushedStream){
    //stream 对象
}).catch(function(error){

});

//electron平台 默认选择第一个屏幕,如果需要选择其他,需要重写方法
emedia.chooseElectronDesktopMedia = function(sources, accessApproved){
    var firstSources = sources[0];
    accessApproved(firstSources);
}

注意: 在chrome浏览器中使用时,需要从chrome store 或者从环信服务器 中下载插件,解压后在chrome浏览器中输入 chrome://extensions/, 选择“Load unpacked” 选择解压后的文件夹中的1.0_0文件夹,加载插件。

获取加入会议ticket

emedia.mgr.getConferenceTkt(confrType, password).then(function(confr){
    
}).catch(function(error){

})

销毁会议

emedia.mgr.destroyConference(confrId).then(function(){
    
}).catch(function(error){

})

踢人

emedia.mgr.kickMembersById(confr, memberNames).then(function(){
    
}).catch(function(error){

})

获取会议信息

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

// confrId: 会议Id
// password: 会议密码
emedia.mgr.selectConfr(confrId, password).then(function(){
    
}).catch(function(error){

})

授权

emedia.mgr.grantRole(confr, memberNames, role).then(function(){
    
}).catch(function(error){

})

使用用户名密码加入会议,可自定义ext

emedia.mgr.joinConference(confrId, password, ext).then(function(){
    
}).catch(function(error){

})

使用ticket加入会议,可自定义ext

emedia.mgr.joinConference(confrId, ticket, ext).then(function(){
    
}).catch(function(error){

})

退出会议

emedia.mgr.exitConference();

publish 媒体流

/**
 * constaints: {audio: true, video: true}
 * videoTag 可缺失,如果有 此次publish的媒体数据将会在这个video上显示 将会与stream绑定
 * ext 用户自定义扩展,其他成员可以看到这个字段
 * 
 */
emedia.mgr.publish(constaints, videoTag, ext).then(function(pushedStream){
    //stream 对象
}).catch(function(error){

});

/**
 * videoTag 可缺失时
 *
 */
emedia.mgr.publish(constaints, ext).then(function(pushedStream){
    //stream 对象
    //如果需要将这个stream对象 显示,需要 emedia.mgr.streamBindVideo(videoTag, pushedStream)
}).catch(function(error){

});

取消publish

emedia.mgr.unpublish(pushedStream);
或
emedia.mgr.hungup(pushedStream);

订阅

/**
 * subSVideo 看视频
 * subSAudio 看音频
 * videoTag 可缺失,如果有 将会与stream绑定,并播放stream
 */
emedia.mgr.subscribe(member, stream, subSVideo, subSAudio, videoTag).then(function(){
    //stream 对象
}).catch(function(error){

});
emedia.mgr.triggerSubscribe(videoTag, subSVideo, subSAudio).then(function(){
    //stream 对象
}).catch(function(error){

});
也可以
//仅订阅音频
emedia.mgr.triggerResumeAudio(videoTag).then(function(){
}).catch(function(error){
});
//仅暂停订阅音频
emedia.mgr.triggerPauseAudio(videoTag).then(function(){
}).catch(function(error){
});

//仅订阅视频
emedia.mgr.triggerResumeVideo(videoTag).then(function(){
}).catch(function(error){
});
//仅暂停订阅视频
emedia.mgr.triggerPauseVideo(videoTag).then(function(){
}).catch(function(error){
});

stream video 绑定

//stream 与 video绑定后,video才可以播放stream;并且可以通过触发/监听video上的事件,来达到对stream的便捷操作
emedia.mgr.streamBindVideo(stream, videoTag);

获取stream绑定的video

emedia.mgr.getBindVideoBy(stream);

切换摄像头

// 随机切换摄像头
emedia.mgr.changeCamera().then(function(){
      // 无参数
}).catch(function(){

})
// 切换手机前后摄像头
emedia.mgr.switchMobileCamera().then(function(){
     // 无参数
}).catch(function(){

})

指定设备打开音视频

const devices = await emedia.mgr.mediaDevices(); //获取设备列表

    // 设备信息
    device: Object { 
        deviceId: "529a6fe76467d****9498ab22f5f362cd" // 设备ID
        groupId: "2b74c9b9ab99*****d513fbabc1e86b3c5d99f7f8a0c16"
        kind: "audioinput" | audiooutput | videoinput | videooutput // 设备类型
        label: "Internal Microphone (Built-in)"
    }
    constraints: { // 选择设备, 然后指定设备
        audio: {deviceId: deviceId ? {exact: deviceId} : undefined},
        video: {deviceId: deviceId ? {exact: deviceId} : undefined}
    },
    
const stream = await emedia.mgr.publish(constraints) // 推流

暂停/恢复自己的视频

emedia.mgr.pauseVideo(pubS).then(function(){

}).catch(function(){

})
等价于
emedia.mgr.triggerPauseVideo(localVideoTag).then(function(){

}).catch(function(){

})
emedia.mgr.resumeVideo(pubS).then(function(){

}).catch(function(){

})
等价于
emedia.mgr.triggerResumeVideo(localVideoTag).then(function(){

}).catch(function(){

})

抓取 video图像,并保存

emedia.mgr.captureVideo(videoTag, true, filename)
等价于
emedia.mgr.triggerCaptureVideo(videoTag, true, filename);

控制远程视频(手机端)定格

emedia.mgr.freezeFrameRemote(stream);
等价于
emedia.mgr.triggerFreezeFrameRemote(videoTag).catch(function(){
    alert("定格失败");
});

控制手机闪光灯打开/关闭

/**
 * torch true 打开,否则 关闭; 可缺失
 */
emedia.mgr.torchRemote(stream, torch);
等价于
emedia.mgr.triggerTorchRemote(videoTag, torch).catch(function(){
    alert("Torch失败");
});

控制手机截屏

emedia.mgr.capturePictureRemote(stream);
等价于
emedia.mgr.triggerCapturePictureRemote(videoTag).catch(function(){
    alert("抓图失败");
});

控制手机摄像头放大缩小

emedia.mgr.zoomRemote(stream, multiples);
等价于
emedia.mgr.triggerZoomRemote(videoTag, multiples).catch(function(){
    alert("zoom失败");
});

控制手机摄像头聚焦曝光

/**
 * clickEvent 为 videoTag的点击事件。通过event计算点击的坐标传给sdk进行控制
 *
 */
emedia.mgr.focusExpoRemote(stream, videoTag, clickEvent).catch(function(){
    alert("focusExpoRemote失败");
});
等价于
/**
 * event string. 如点击 “click”
 * fail 失败回调;success成功回调
 */
emedia.mgr.onFocusExpoRemoteWhenClickVideo(videoTag, event, fail, success);

取消在videoTag上的事件

//用来对onFocusExpoRemoteWhenClickVideo的撤销
emedia.mgr.offEventAtTag(videoTag);

video监听的事件

//video的音频视频开关,将会触发
emedia.mgr.onMediaChanaged(videoTag, function (constaints) {
    $div.find("#aoff").html(constaints.audio ? "有声" : "无声");
    $div.find("#voff").html(constaints.video ? "有像" : "无像");

    //可以通过video标签 得知发布方 是否关闭 音频 视频
    console.warn($(videoTag).attr("easemob_stream"), "aoff", $(videoTag).attr("aoff"));
    console.warn($(videoTag).attr("easemob_stream"), "voff", $(videoTag).attr("voff"));
});
//video的声音大小出发,比如谁在说话
emedia.mgr.onSoundChanaged(videoTag,, function (meterData) {
    var $volume = $div.find('#volume');

    $volume.html(meterData.instant.toFixed(2)
        + " " + meterData.slow.toFixed(2)
        + " " + meterData.clip.toFixed(2)
        + " " + (meterData.trackAudioLevel ? parseFloat(meterData.trackAudioLevel).toFixed(4) : "--")
        + " " + (meterData.trackTotalAudioEnergy ? parseFloat(meterData.trackAudioLevel).toFixed(4) : "--")
    );
});
//视频收发数据统计
emedia.mgr.onMediaTransmission(videoTag, function notify(trackId, type, subtype, data) {
    var $iceStatsShow = $div.find("#iceStatsShow");
    var $em = $iceStatsShow.find("#"+subtype);
    if(!$em.length){
        $em  = $("<em></em>").appendTo($iceStatsShow).attr("id", subtype);
    }

    $em.text(subtype + ":" + (data*8/1000).toFixed(2));
});
//连接状态变化
emedia.mgr.onIceStateChanged(videoTag, function (state) {
    console.log(state);
});

支持会议属性

// 用来自定义一些属性,广播给会议中的成员
    // 有人设置会议属性,所有的成员都能收到
	let options = {
            key:username,
            val:'request_tobe_speaker'
        }

      // a. 设置会议属性 
	emedia.mgr.setConferenceAttrs(options)
      // b. 删除会议属性 
	emedia.mgr.deleteConferenceAttrs(options)
      // c. 会议属性变更回调 
	emedia.mgr.onConfrAttrsUpdated = attrs => {} //attrs 会议属性集合 Array

房间内动作消息

// 以下方法非回调函数均为异步函数
  try {
    await emedia.mgr.xxx;
  } catch(error) { }
     
// 以下出现的参数注解
  confrId: 会议id
  memberId: 与会人员的id,member 中的id
  nickName: 昵称

1.上麦申请

// 观众上麦申请方法
emedia.mgr.requestToTalker(confrId) 

// 主持人收到上麦申请的回调 (主播不会收到这个回调)
emedia.mgr.onRequestToTalker = function(applicat, agreeCallback, refuseCallback) {
   /*
    * applicat { memberId, nickName } object 申请者信息
    * agreeCallback 主持人同意的回调 示例:agreeCallback(memberId) memberId 申请者 id 必需
    * refuseCallback 主持人拒绝的回调 示例:refuseCallback(memberId) memberId 申请者 id必需
   */
}
   
// 观众收到 上麦申请的回复 
emedia.mgr.onRequestToTalkerReply = function(result) {
   // result 0: 同意 1: 拒绝
} 

2.主播申请主持人

// 主播申请主持人
emedia.mgr.requestToAdmin(confrId);
 
//主持人收到申请主持人的回调 (主播不会收到这个回调)
emedia.mgr.onRequestToAdmin = function(applicat, agreeCallback, refuseCallback) {
   /*
    * applicat { memberId, nickName } object 申请者信息
    * agreeCallback 主持人同意的回调 示例:agreeCallback(memberId) memberId 申请者 id 必需
    * refuseCallback 主持人拒绝的回调 示例:refuseCallback(memberId) memberId 申请者 id必需
   */
}

// 主播收到 申请主持人的回复 
emedia.mgr.onRequestToAdminReply = function(result) {
   // result 0: 同意 1: 拒绝
}

3.主持人执行全体静音

// 只有主持人可操作,其他角色操作不生效,主持人不会被静音 

// 主持人静音全体
await emedia.mgr.muteAll(confrId);

// 主播收到回调 
emedia.mgr.onMuteAll = () => { }

4.主持人执行取消全体静音

// 只有主持人可操作,其他角色操作不生效

// 主持人取消全体静音 
await emedia.mgr.unmuteAll(confrId);

//主播收到回调 
emedia.mgr.onUnMuteAll = () => { }

5.主持人静音单个主播

// 主持人静音单个主播
emedia.mgr.muteBymemberId(confrId, memberId);// memberId 被静音主播的memberId

// 单个主播被管理员静音的回调(只他自己收到回调)
emedia.mgr.onMuted = () => { }

6.主持人取消静音 某个主播

// 主持人取消静音单个主播
emedia.mgr.unmuteBymemberId(confrId, memberId);// memberId 被取消静音主播的memberId

// 单个主播被管理员取消静音的回调 (只他自己收到回调)
emedia.mgr.onUnmuted = () => { }

7.角色降级接口

// 只能角色降级 从主持人到主播、从主播到观众、从主持人到观众,不可逆向操作 

//[memName] 被降级人员的memName、 toRole 想要 达到的角色
emedia.mgr.degradeRole(confrId, [memName], toRole); 

集群代理

//开启集群部署,SDK内部会自动请求代理,实现通话最优

//开启方式
emedia.config({
   ...
   useDeployMore: true // 默认:false 不开启
   ...
})

合流推流 CDN

1.创建CDN

可以通过单独API创建 CDN 、也可以在创建会议、加入房间时指定推流CDN

CDN推流参数 liveCfg 必需

let liveCfg = {
    cdn:'', //推流地址、字符串;必需	
    layoutStyle: 'GRID' | 'CUSTOM', // 格子显示 | 自定义,必需
    canvas :{// canvas 参数在 layoutStyle == 'CUSTOM' 必填
           bgclr : 0x980000,//背景色 980000 为 十六进制色值
           w : 640, //宽度
           h : 480, //高度
           fps: 20, //输出帧率
           bps: 1200000,  //输出码率
           codec: "H264" //视频编码,现在必须是H264
    }
}

1.1通过单独API创建 CDN(可多路推流)

// confrId: 会议id, 必需
// liveCfg: cdn 配置,必需
// 只有管理员,可创建 CDN
emedia.mgr.addLive(confrId, liveCfg);

1.2创建会议时指定 CDN推流

let option = {
    confrType, 
    password, 
    ...
    liveCfg // 创建CDN推流参数 
}
emedia.mgr.createConference(option)

1.3加入房间时 指定CDN推

// 只有第一个加入房间的人才能创建 CDN、以后加入的人指定CDN也无效
let params = {
       roomName,
       password,
       ...
       config:{ 
          ...
          liveCfg // 创建CDN推流参数
       }
}
emedia.mgr.joinRoom(params);

2.更新CDN布局

// 只有管理员才能 更新布局。更新布局会 将layoutStyle 变为 CUSTOM 而且不可逆
emedia.mgr.updateLiveLayout(confrId, liveId, regions)

// confrId 会议id 必需
// liveId 推流CDN id, 必需 可通过 emedia.config.liveCfgs 获取 Array 

regions:[ // 希望定义视频流 显示的配置集合
    {
       "sid": stream_id,//视频流的id
       "x": 320,//距离 x 轴的距离 Number
       "y": 240,//距离 y 轴的距离 Number
       "w": 960,//宽度 Number
       "h": 720,//高度 Number
       "style": "fill" | "AspectFit" //视频显示模式 fill:铺满、AspectFit:原比例显示
    },				
    .... 其他视频流配置(数组有几个项,就显示几个视频流)
]

3.删除CDN

// 只有管理员可操作
//confrId 会议id 必需
// liveId 推流CDN id, 必需 可通过 emedia.config.liveCfgs 获取 Array 
emedia.mgr.deleteLive(confrId, liveId)