Vue中webSocket+webRtc实现多人会议,webRtc实现

04-13 阅读 0评论

前提

已经搭建好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监听到的,但是我在实际使用中并没有监听到,如果可以的话,就不用频繁的关闭建立连接,还要再研究下

    Vue中webSocket+webRtc实现多人会议,webRtc实现


免责声明
本网站所收集的部分公开资料来源于AI生成和互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,人围观)

还没有评论,来说两句吧...

目录[+]