当前位置: 代码网 > it编程>前端脚本>Vue.js > Vue中webSocket+webRtc实现多人会议,webRtc实现

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

2024年08月06日 Vue.js 我要评论
已经搭建好`websocket`双端通信(可以先模拟),用于实时交换双方信息。交换的信息也就是所谓的信令。实现`webRtc`进行多人会议,屏幕共享、摄像头共享。

前提

已经搭建好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,这里面有bsdp信息,并且自己也存一份,发送建立连接请求webrtc中叫offer
  • 然后a收到offer时,也创建一个专门与b交流的webrtc连接( new rtcpeerconnection(undefined))。然后将b的信息存下来,再生成自己的信息发给b,这里面有asdp信息,webrtc中这个过程叫应答answer
  • 创建的webrtc连接的时候会使用一个监听器,能监听自己的candidate候选信息有没有制作完,这里面是ice的信息。ab都要监听,制作完后发给对方,对方再存到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监听到的,但是我在实际使用中并没有监听到,如果可以的话,就不用频繁的关闭建立连接,还要再研究下
在这里插入图片描述

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com