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

文章介绍了如何利用WebSocket实现实时通信,并结合webRTC技术进行多人会议,包括消息格式设计、用户加入会议的流程、媒体流共享和断开重连机制。重点讲解了如何通过SDP协商和ICE候选者来建立和维护WebRTC连接。
摘要由CSDN通过智能技术生成

前提

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

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Vue WebSocket 可以使用 WebRTC 技术来实现语音通话功能。具体实现步骤包括:1、使用 Vue 提供的 WebSocket 进行数据通信;2、使用 WebRTC 技术建立 P2P 连接;3、使用 WebRTC 实现语音编码解码、流媒体传输和处理等功能。需要注意的是,语音通话功能可能涉及到音频数据的处理和传输,需要具备一定的技术基础和相关知识。 ### 回答2: 实现Vue Websocket语音通话功能需要以下几个步骤: 1. 服务器端准备:为了实现语音通话功能,需要用到一种支持实时语音传输的技术,如WebRTC。首先,在服务器端搭建一个WebRTC服务器,并提供相应的API接口供客户端调用。 2. 客户端准备:使用Vue框架搭建前端页面。在Vue组件引入WebRTC相关的JavaScript库,如webrtc-adapter和vue-webrtc等。这些库将提供语音通话所需的API和功能。 3. 创建连接:在Vue组件,使用WebSocket建立与服务器的连接。可以使用Vue框架提供的插件vue-native-websocket来简化这一过程。 4. 启动语音通话:当WebSocket连接建立成功后,通过调用WebRTC库提供的API,启动语音通话功能。可以使用getUserMedia函数来获取用户的音频流,并通过WebRTC库提供的PeerConnection对象建立与对方的连接。 5. 实时通话:一旦与对方建立连接,即可实时进行语音通话。通过WebRTC库提供的API,使用RTCDataChannel实时传输音频数据。 6. 结束通话:当通话结束后,断开WebSocket连接和PeerConnection连接,释放资源。 在实现这个过程,还要注意以下几点: - 需要处理一些错误和异常情况,如网络断开、音频流无法获取等。 - 可以使用Vue的状态管理机制,如Vuex,来管理语音通话过程的状态和数据。 - 需要进行一定的音频编解码处理,以保证语音的质量和实时性。 总之,通过Vue框架结合WebRTC技术,我们可以实现基于Websocket的语音通话功能,为用户提供良好的通话体验。 ### 回答3: Vue.js是一个非常流行的JavaScript框架,提供了轻量级的前端开发解决方案。虽然Vue.js本身并不直接支持语音通话,但结合WebSocket协议可以实现这个功能。 首先,需要在Vue.js项目安装WebSocket库。可以选择一些流行的WebSocket库,例如"socket.io"或"Vue-Socket.io"。使用npm安装: ``` npm install socket.io-client ``` 然后,在Vue.js组件导入WebSocket库并连接到服务器: ```javascript import io from 'socket.io-client'; export default { data() { return { socket: null, }; }, mounted() { this.socket = io('http://your_server_address'); // 在合适的时机监听WebSocket连接成功的事件 this.socket.on('connect', () => { console.log("WebSocket连接成功"); // 可以在这里触发语音通话的初始化逻辑 }); }, methods: { startVoiceCall() { // 开始语音通话的逻辑 }, endVoiceCall() { // 结束语音通话的逻辑 }, // 其他语音通话相关的逻辑 }, destroyed() { this.socket.disconnect(); // 在组件销毁时断开WebSocket连接 }, }; ``` 在WebSocket连接成功后,可以通过WebSocket发送和接收语音数据。语音通话的具体实现和逻辑取决于服务器端的架构和技术选择。 在开始语音通话的方法,可以触发语音通话的初始化逻辑。这包括向服务器发送语音通话请求,获取服务器返回的语音通话相关信息,并启动语音采集和播放设备。 在结束语音通话的方法,可以触发语音通话的结束逻辑。这包括向服务器发送结束语音通话的请求,并停止语音采集和播放设备。 总之,要在Vue.js实现语音通话功能,需要通过WebSocket连接服务器并发送和接收语音数据。具体的实现和逻辑可以根据项目的需求和服务器架构进行调整和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶落_无秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值