Vue中webSocket+webRtc实现多人会议,webRtc实现
叶落_无秋 2024-06-18 15:33:04 阅读 90
前提
已经搭建好websocket
双端通信(可以先模拟),用于实时交换双方信息。交换的信息也就是所谓的信令。实现webRtc
进行多人会议,屏幕共享、摄像头共享。
我这里定义的websocket信息格式如下
发给某个人,下面会用【消息格式one】指代
{ "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}
发给会议中所有人,下面会用【消息格式all】指代
{ "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
监听到的,但是我在实际使用中并没有监听到,如果可以的话,就不用频繁的关闭建立连接,还要再研究下
上一篇: 解决前端笔记本电脑屏幕显示缩放比例125%、150%对页面大小的影响问题--数据可视化大屏
下一篇: Stable-diffusion-WebUI 的API调用(内含文生图和图生图实例)
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。