这是本文档旧的修订版!
多人音视频会议
产品简介
多人音视频采用的是媒体流发布和订阅的技术架构。发布是指参会者发布媒体流(即发言,包括视频流和音频流)到服务器,其他人收到发布事件然后去订阅拉取媒体流。
多人音视频里有管理员,主播和观众三种角色。
- 管理员拥有最高权限,可以发布媒体流,订阅媒体流,设定其他人是主播还是观众
- 主播可以发布媒体流,订阅媒体流
- 观众只有订阅媒体流权限
环信多人音视频有2种比较常见的使用模式:多人音视频会议和多人音视频互动直播。2种模式使用的是相同的技术架构,开发者可以根据业务场景设置不同的参数,主要区别是:
- 多人音视频会议场景中,在创建会议时指定默认角色为主播,即每个参会者加入会议后都可以发言。开发者也可以根据场景需要,设定一些参会者是以观众角色加入会议。
- 多人音视频互动直播场景中,在创建会议时指定默认角色为观众,除了管理员(同时也可以是主播)以外,其他人默认是观众。管理员可以设置指定观众成主播实现上麦操作;或设置指定主播为观众,实现下麦操作。
多人音视频会议和多人音视频互动直播模式都支持白板、共享桌面。
注意:会议类型将只支持Communication类型,原Large Communication和Live会议类型将弃用。
产品特性
- 支持现代浏览器:Chrome/50+、Safari/11+、Firefox;
- 遵守:UMD通用模块规范,支持require导入;
- 支持Promise;
- 支持手机端和 Web 端互通,极大方便开发者的全平台业务;
- 依赖https站点
音视频通信的简要步骤
SDK 能够支持音频和视频通信。创建和操作音视频通信的过程简单来说,可以分为以下几步:
- 初始化 SDK,设置监听代理
- create: 创建会议
- join: 加入会议
- pub: 发布音视频数据流
- sub: 订阅并播放音视频数据流
- 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
// 初始化一些 sdk 的使用功能
emedia.config({
restPrefix, //配置服务器域名、必填 比如: 'https://a1-hsb.easemob.com'
appkey, // 配置appkey、必填
useDeployMore:true //开启多集群部署
... 其他的一些配置
});
设置SDK回调
引入SDK
// 可以使用easemob-webrtc 或者 easemob-emedia
// 使用easemob-webrtc
import webrtc from 'easemob-webrtc'
const emedia = webrtc.emedia
// 使用easemob-emedia
import emedia from 'easemob-emedia'
进入会议之前,设置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){
})
注意: 如果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, //开启录制合并
maxTalkerCount:3 //会议最大主播人数
maxVideoCount:2 //会议最大视频数
maxAudienceCount:100 //会议最大观众数
}
*/
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不携带
* videoTag 可缺失,如果有 此次publish的媒体数据将会在这个video上显示 将会与stream绑定
* ext 用户自定义扩展,其他成员可以看到这个字段
* stopSharedCallback 共享插件 点击【停止共享】的回调函数,做相应的处理(比如删除流...)
*/
// 如果停止共享存在音频的桌面,需自己手动挂断流 执行 emedia.mgr.unpublish(stream)
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(){
})
暂停/恢复自己的视频
emedia.mgr.pauseVideo(pubS).then(function(){
}).catch(function(){
})
等价于
emedia.mgr.triggerResumeVideo(localVideoTag).then(function(){
}).catch(function(){
})
emedia.mgr.pauseVideo(pubS).then(function(){
}).catch(function(){
})
等价于
emedia.mgr.triggerPauseVideo(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