前提
已经搭建好websocket
双端通信(可以先模拟),用于实时交换双方信息。交换的信息也就是所谓的信令。实现webrtc
进行多人会议,屏幕共享、摄像头共享。
我这里定义的websocket信息格式如下
{
"body": {},
"code": "10003",//自定义标识(我自定义区分消息来源用的)
"data": {
"description": {
"type": "answer",
"sdp": "v=0\r\no=- 700908093190320106 2 in ip4..."
},//需要交换的信息
"meetid": "852229c8c454453da6e0b5e99a8407c8",//会议id
"pagenum": 0,
"pagesize": 0,
"receiveid": "ed986a7b3dbb407e846f76fad909f07d",//接收人id
"sendid": "c0f1094a363949f88f618f5edb5ecaf8",//发送人id
"type": "answer"//信息分类
},
"msg": "meetingmessage",
"success": true
}
{
"body": {},
"code": "10003",
"data": {
"meetid": "852229c8c454453da6e0b5e99a8407c8",//会议id
"pagenum": 0,
"pagesize": 0,
"sendid": "c0f1094a363949f88f618f5edb5ecaf8",//发送人id
"type": "new"//信息分类
},
"msg": "meetingmessage",
"success": true
}
简单说明逻辑
当用户a
进入会议时,向所有人发送【消息格式all】,通知有人加入了会议,然后其他人(取一人b
代指)将主动与a
取得联系。
b
创建一个专门与a
交流的webrtc
连接(new rtcpeerconnection(undefined)
)。将打开的媒体流流加载到连接中b
创建完这个webrtc
连接后生成一个请求连接的信息通过【消息格式one】发给a
,这里面有b
的sdp
信息,并且自己也存一份,发送建立连接请求webrtc
中叫offer
。- 然后
a
收到offer
时,也创建一个专门与b
交流的webrtc
连接(new rtcpeerconnection(undefined)
)。然后将b
的信息存下来,再生成自己的信息发给b
,这里面有a
的sdp
信息,webrtc
中这个过程叫应答answer
。 - 创建的
webrtc
连接的时候会使用一个监听器,能监听自己的candidate
候选信息有没有制作完,这里面是ice
的信息。a
跟b
都要监听,制作完后发给对方,对方再存到webrtc
连接中,到此双方连接完成。 - 当一方的媒体源改变时(关闭/打开 麦克风/摄像头/共享桌面),通知其他人连接过期,然后进行以上步骤进行重新连接(除了加入的媒体流不一样,其他一样)
代码参考
打开页面告诉其他人加入会议,这个调用的接口,后台用websocket
发给了其他人
onmounted(async () => {
/**打开页面告诉其他人加入*/
meetinginfoapi.sentmessage({
type: 'new',
meetid: props.id,//这个是会议的id,我这是个组件,从父组件传过来的
sendid: data.userinfo.value.id,//这个是获取的登录人的id,作为唯一标识用
})
})
监听websocket
返回,我这里用了一个对象用来存跟会议中其他人沟通的webrtc
连接,如果只是一对一,可以声明一个存连接的变量就行
这个是声明的变量
const cameravideo = ref(null);//video标签的ref引用
const connectlist = ref({}),//用来存跟其他人连接的rtc连接
const mediastream = ref(),//用来存媒体信息
const userslist= ref(),//用来存其他用户信息
工具方法,看connectlist
中有没有请求连接人的专属连接,没有就创建一个
/**有用户请求连接,生成对应的本地连接保存下来,下次直接用*/
getconnection(userid) {
let connection = data.connectlist.value?.[userid];
if (!connection) {
let cof = {
iceservers: [
// 目前免费stun 服务器
{
urls: 'stun:stun.voipbuster.com ',
},
]
}
connection = new rtcpeerconnection();
connection.ontrack = (event) => {
methods.onaddstream(event, userid);
}
console.log("监听ice");
connection.onicecandidate = (event) => {
if (event.candidate) {
//生成完自己的候选信息后发给这个连接对应的人
meetinginfoapi.sentmessage({
type: "candidate",
meetid: props.id,
sendid: data.userinfo.value.id,
receiveid: userid,
label: event.candidate.sdpmlineindex,
sdpmid: event.candidate.sdpmid,
candidate: event.candidate.candidate,
})
} else {
console.log("end of candidates.");
}
}
//加载媒体流
data.mediastream.value?.gettracks()?.foreach(track => {
connection.addtrack(track, data.mediastream.value)
})
data.platformstream.value?.gettracks()?.foreach(track => {
connection.addtrack(track, data.platformstream.value)
})
data.connectlist.value[userid] = connection;
}
return connection;
},
/**有媒体流传过来时在video中播放*/
onaddstream(event, userid) {
if (event && event.streams.length > 0) {
//之后会测试怎么传媒体标识,用来区分是桌面共享还是摄像头,然后显示在不同的位置
cameravideo.value.srcobject = event.streams[0];
}
},
这里是监听websocket
发送消息的,是服务器主动给前端发的
//监听接收消息
window.addeventlistener('receive', function (event) {
let res = json.parse(event.detail)
if (res && res.success && res.code === "10003" && props.drawer) {
let connection = methods.getconnection(res.data.sendid)
//用户列表增加一个人
let send = data.userslist.value?.[res.data.sendid];
if (!send) {
data.userslist.value[res.data.sendid] = {
id: res.data.sendid,
name: res.data.sendname,
};
}
if (connection) {
/**有新用户加入,主动发送offer进行连接*/
if (res.data.type === "new") {
let offeroptions ={
offertoreceiveaudio: true,
offertoreceivevideo: true,
}
connection.createoffer(offeroptions).then((sessiondescription) => {
connection.setlocaldescription(sessiondescription)
meetinginfoapi.sentmessage({
meetid: props.id,
sendid: data.userinfo.value.id,
receiveid: res.data.sendid,
type: 'offer',
description: sessiondescription
})
})
} else if (res.data.type === "offer") {
/**接收到offer,将对方sdp保存到对应的连接中,发送应答信息*/
connection.setremotedescription(new rtcsessiondescription(res.data.description));
connection.createanswer().then((sessiondescription) => {
connection.setlocaldescription(sessiondescription)
meetinginfoapi.sentmessage({
meetid: props.id,
sendid: data.userinfo.value.id,
receiveid: res.data.sendid,
type: 'answer',
description: sessiondescription
})
})
} else if (res.data.type === "answer") {
/**接收到应答信息,保存sdp在本地对应的连接中*/
connection.setremotedescription(new rtcsessiondescription(res.data.description));
} else if (res.data.type === "candidate") {
/**接收到他人的候选信息,保存在本地对应的连接中*/
const candidate = new rtcicecandidate({
sdpmid: res.data.sdpmid,
sdpmlineindex: res.data.label,
candidate: res.data.candidate,
});
connection.addicecandidate(candidate).catch((error) => {
console.log(error);
});
} else if (res.data.type === "leave") {
/**有人离开,关闭他的连接*/
data.connectlist.value?.[res.data.sendid]?.close()
delete data.userslist.value[res.data.sendid]
delete data.connectlist.value[res.data.sendid]
} else if (res.data.type === "change") {
/**有人修改了媒体源,关闭他的连接*/
data.connectlist.value?.[res.data.sendid]?.close()
data.userslist.value[res.data.sendid].mediastream = undefined
delete data.connectlist.value[res.data.sendid]
}
}
}
})
下面是发送媒体示例
当按钮状态发生变化时调用
mediachange(){
let muteclose = data.muteclose.value//麦克风
let cameraclose = data.cameraclose.value//摄像头
let platformclose = data.platformclose.value//桌面共享
//关闭所有连接
if (data.connectlist.value) {
for (let valuekey in data.connectlist.value) {
data.connectlist.value[valuekey]?.close()
}
data.connectlist.value = {}
meetinginfoapi.sentmessage({
type: 'change',
meetid: props.id,
sendid: data.userinfo.value.id,
})
}
//关闭媒体
if ((muteclose || cameraclose) && data.mediastream.value) {
data.mediastream.value.gettracks().foreach(track => {
track.stop()
});
data.mediastream.value = null;
}
if (platformclose && data.platformstream.value) {
data.platformstream.value.gettracks().foreach(track => {
track.stop()
});
data.platformstream.value = null;
}
if (!(muteclose && cameraclose && platformclose)){
if ((!muteclose || !cameraclose) && !data.mediastream.value){
methods.getmedia()
}
if (!platformclose && !data.platformstream.value){
methods.getdisplay()
}
//只要有一个没有关闭,就通知所有人进行重新连接
meetinginfoapi.sentmessage({
type: 'new',
meetid: props.id,
sendid: data.userinfo.value.id,
})
}
},
打开麦克风/摄像头
getmedia() {
let muteclose = data.muteclose.value
let cameraclose = data.cameraclose.value
let cof = {
video: cameraclose ? false : data.enumeratedevicesvideocheck.value ? {exact: data.enumeratedevicesvideocheck.value} : undefined,
audio: muteclose ? false : data.enumeratedevicesaudioinputcheck.value ? {exact: data.enumeratedevicesaudioinputcheck.value} : undefined,
}
navigator.mediadevices.getusermedia(cof)
.then(stream => {
data.mediastream.value = stream;
})
.catch(error => console.log(`无法获取摄像头/麦克风:${error}`));
},
打开屏幕共享
getdisplay() {
navigator.mediadevices.getdisplaymedia({video: true, audio: true})
.then(stream => {
data.platformstream.value = stream;
cameravideo.value.srcobject = data.platformstream.value;
})
.catch(error => console.log(`无法获取屏幕共享:${error}`));
},
根据官方的描述,对等端建立连接后任意一方进行addtrack
时,另一方是可以通过ontrack
监听到的,但是我在实际使用中并没有监听到,如果可以的话,就不用频繁的关闭建立连接,还要再研究下
发表评论