webrtc实现多房间多用户的简单架构会议系统

从点对点视频通话到小型一对多直播,逐步加深了webrtc的了解和使用。现在再进一步,实现一个简单的多房间多用户的简单架构会议系统。

无论是点对点音视频还是直播,都是通过 P2P 的方式形成关联关系,而接下来的多对多会议也是一样,无非就是每个客户端都去和会议室中的每个用户建立关联,从而拿到对方的媒体信息。

过程分析

假设有A、B、C、D四个人需要参会,但是WebRTC仅支持 P2P ,那么 A 如果要和剩下的三个人视频通话,就必须和他们三个人都建立关联,也就是形成A-BA-CA-D的关联关系

看上图所示,只有建立关联关系之后,A 才能将自己的视频流发给 B、C、D 三个人,同时也才能收到其余三个人的视频画面。但是注意看,A 和其余三个人形成视频通话,但是其余三人之间并没有相互关联。

也就是说,现在这个方案只是达成了 1 对多 的场景(前面所说的直播场景)。那么如何让其余三个人之间也形成互通呢?

同理,看上图的 B 和 A 一样重新建立关联关系,只不过变成 B 为主体,剩下的A、C、D为被关联对象进而形成B-AB-CB-D的关联关系。但是这时你会发现,A-B 和 B-A 不是重复了吗? 这里就需要额外处理了,后面会提出解决方案。

重复A关联其他三个用户的过程,B,C,D都分别和其他用户建立起关联关系了。可以理解为就是多个一对多直播模式的关联过程,只是其中代码有些变化而已。

WebRTC的核心载体PeerConnection上来讲,上述的关联关系代表的就是这个核心载体,一对关联关系代表一个核心对象,A-B这一关系从代码层面上来表示,就是创建一个PeerConnection对象,这个对象中维护的是 A 和 B 之间的sdp信令 和媒体信息

用图来表示就如下所示:

首先 A 客户端需要和其余三个客户端通信,按照上述关系和WebRTC的会话流程,我们就需要创建三个PeerConnection对象来作为会话信令的载体,然后客户端还需要储存下这三个变量。这里为了操作方便,我们将其放置在Map数据结构中,key按照用户的 ID 组合成变量名,那么 A 和 B、C、D 关联关系储存方式如下:

js 复制代码
// A:10001 B:10002 C:10003 D:10004 分别代表四个客户端的用户ID
const RtcPcMaps = new Map()
const ABKeys = 10001-10002
const ACKeys = 10001-10003
const ADKeys = 10001-10004
RtcPcMaps.set(ABKeys , new PeerConnection()) //维护A-B关系
RtcPcMaps.set(ACKeys , new PeerConnection()) //维护A-C关系
RtcPcMaps.set(ADKeys , new PeerConnection()) //维护A-D关系

同理,B,C,D也应该是按照上述流程进行的。按照这个流程,就会出现上面所说的关联关系重复的问题。现在来思考下如何解决。

还是先再看一遍AB的关联过程,注意其中的一些小细节点:

  1. 首先 A 呼叫 B,呼叫之前我们一般通过实时通信协议WebSocket即可,让对方能收到信息。
  2. B 接受应答,A 和 B 均开始初始化PeerConnection 实例,用来关联 A 和 B 的SDP会话信息。
  3. A 调用createOffer创建信令,同时通过setLocalDescription方法在本地实例PeerConnection中储存一份(图中流程①)。
  4. 然后调用信令服务器将 A 的SDP转发给 B(图中流程②)。
  5. B 接收到 A 的SDP后调用setRemoteDescription,将其储存在初始化好的PeerConnection实例中(图中流程③)。
  6. B 同时调用createAnswer创建应答SDP,并调用setLocalDescription储存在自己本地PeerConnection实例中(图中流程④)。
  7. B 继续将自己创建的应答SDP通过服务器转发给 A(图中流程⑤)。
  8. A 调用setRemoteDescription将 B 的SDP储存在本地PeerConnection实例(图中流程⑤)。
  9. 在会话的同时,从图中我们可以发现有个ice candidate,这个信息就是 ice 候选信息,A 发给 B 的 B 储存,B 发给 A 的 A 储存,直至候选完成。

注意看,首先A设置本地offer,然后再将offer发送给B。B接收到offer之后将其设置为远程描述后,再创建answer发送给A这里B并没有先设置本地offer,而是直接将接收到的offer设置为远程描述 。也就是说,在AB关联过程中,有一方其实是不需要设置本地描述的(花了我一天才想明白!!!)。

可以思考如何利用这点,将A关联B和B关联A这两个PeerConnection对象去掉一个。比如A已经发送offer了,B就不需要再主动关联A了。如何才能判断A是否发送offer了呢?

  1. 假设房间内只有A时,是不会进行任何操作的。
  2. 当B进来后,将B标记为新人。即A和B拿到房间内成员信息时,B对象的isNewtrue,而A对象的isNewfalse
  3. 对于A来说,B是新人,可以直接进行关联。此时A首先创建offer,设置为本地描述,然后发送offer给B。对于B来说,他是新人,不需要进行任何操作,等着被关联就行。所以B不需要初始化,直接接收A的offer,然后设置为远程描述,再创建answer发送给A即可。到此,AB关联结束,只有A一方创建了PeerConnection对象,就是页面中只有一个PeerConnection对象。
  4. 此时再进来C,将C标记为新人。即ABC拿到房间内成员信息时,AB对象的isNewfalse,C对象的isNewtrue
  5. 对于AB来说,C是新人,可以直接进行关联。而对于C来说,他是新人,不需要进行任何操作,等着被关联就行。所以,AB分别创建一个PeerConnection对象。此时页面中有两个PeerConnection对象,而房间内有ABC三个人。
  6. 再进来一个新人,过程同上。

将过程简化就是,房间内的成员依次关联新进来的成员,新进来的成员不需要任何操作,乖乖等着被关联就行。这样就很好的解决了重复关联的问题。如下图所示:

代码实现

根据上述过程描述,用以下代码进行表示:

  1. A进入会议:获取本地媒体流并将其展示在已初始化好的 DOM 节点;初始化信令服务器连接,然后初始化房间,找出房间内的新人,发现新人是自己,不进行任何操作
linkSocket.value.on("roomUserList", 复制代码
    roomUserList.value = data.filter((e) => e.userId !== String(userId));
    console.log("房间成员列表", data);

    // 拿到房间成员列表后 初始化pc
    newUser.value = data.find((e) => e.isNew) ?? {}; // 找到新用户

    // 如果新用户是自己 则啥也不用干 等着被连接就行
    // 不是自己 则要去连接新用户的pc
    if (newUser.value?.userId !== userId) {
      initMeetingRoomPc();
    }
  });
  1. B进入会议:获取本地媒体流并将其展示在已初始化好的 DOM 节点;初始化信令服务器连接,然后初始化房间, 找出房间内的新人,发现新人是自己,不进行任何操作。但A发现了B为新人,则进行关联:
js 复制代码
const initMeetingRoomPc = async () => {
  if (!newUser.value) return;
  const localUserId = props.userId;
  const NUser = newUser.value;

  const pcKey = localUserId + "-" + NUser?.userId;
  let pc = RtcPcMaps.get(pcKey);
  if (!pc) {
    pc = new PeerConnection();
    RtcPcMaps.set(pcKey, pc);
  }

  console.log(RtcPcMaps, "RtcPcMaps-----------------");

  for (const track of localStream.value.getTracks()) {
    const sender = pc.getSenders().find((s) => s.track === track);
    if (sender) {
      // 如果发送器已经存在,则更新发送器的轨道
      sender.replaceTrack(track);
    } else {
      // 否则,添加新的轨道
      pc.addTrack(track);
    }
  }

  onPcEvent(pc, localUserId, NUser?.userId);
  // 创建offer
  const offer = await pc.createOffer();
  // 设置本地描述
  await pc.setLocalDescription(offer);
  // 发送offer给被呼叫端
  linkSocket.value.emit("offer", {
    offer,
    targetUserId: NUser?.userId,
    userId: localUserId,
  });
};
  1. B接收到offer,进行被动关联:
js 复制代码
// 处理远端offer
const onRemoteOffer = async (fromUserId, offer) => {
  const localUserId = props.userId;
  const pcKey = fromUserId + "-" + localUserId;
  let pc = new PeerConnection();
  RtcPcMaps.set(pcKey, pc);

  onPcEvent(pc, localUserId, fromUserId);

  for (const track of localStream.value.getTracks()) {
    const sender = pc.getSenders().find((s) => s.track === track);
    if (sender) {
      // 如果发送器已经存在,则更新发送器的轨道
      sender.replaceTrack(track);
    } else {
      // 否则,添加新的轨道
      pc.addTrack(track);
    }
  }

  await pc.setRemoteDescription(offer);

  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);
  linkSocket.value.emit("answer", {
    userId: localUserId,
    targetUserId: fromUserId,
    answer,
  });
};
  1. C进入会议:获取本地媒体流并将其展示在已初始化好的 DOM 节点;初始化信令服务器连接,然后初始化房间, 找出房间内的新人,发现新人是自己,不进行任何操作。但AB发现了C为新人,则进行关联,过程同上。
  2. 至此,房间内每个人都和其他人建立了关联关系,可以互相视频了。

项目操作

项目地址:

打开下面这个,

下一节写有关通话过程的相关功能

相关推荐
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税6 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore
Cobyte6 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc