上一篇分析了多人音视频的实现方案,其中最简单、最容易实现的就是Mesh方案了。 Mesh方案的实现思路是每个用户都与其他用户建立连接,在客户端直接传输媒体流,不走服务器。具有低延迟、画质高、难度低的优势。当然在4-5个人之内能很好的控制,再多的话带宽成本就很高了。下面就来看看Mesh方案是如何实现多人音视频的。
注意:该小节是在一对多的基础上升级的!!
分析
上一篇就说过,它的实现方案就是一对多直播模式的升级版,即每个人都是主播,都与其他用户建立起连接。那么应该如何实现呢?很简单,当一个用户进入到房间后,就将这个用户打上newUser
的标识。然后房间内的已有用户获取到最新的列表,找出那个带有newUser
的用户并主动建立连接。而那个新用户则啥也不用干,等着被连接就行。然后,当又有一个新的用户进来,也加上newUser
,并将之前的新用户去掉newUser
标识。如此,便实现了每个用户都能相互建立连接且只连接一次。
注意,当两个用户建立两个连接时是会报错的,虽然看起来不会影响使用,但后续实现其他功能时就会很难搞。
OK,根据上面描述的过程,来验证上一篇说的连接次数。
- 当A进入,只有一个人不操作。
- 当B再进入,A则与B连接,1次。
- 当C再进入,AB都与C连接,2次。加上之前的1次,共3次。
- 当D再进入,ABC都与D连接,3次。加上之前的3次,共6次。
上一篇说的连接次数如下:
- 当只有A和B,1 次
- 当有ABC三个人,2 + 1 = 3 次
- 当有ABCD四个人,3 + 2 + 1 = 6 次
- 当有ABCDE五个人, 4 + 3 + 2 + 1 = 10 次
- 当有N个人, 有 N * (N - 1) / 2 次
这样是不是就对上了。可以很明显的看到当人数增多时,连接次数几乎是垂直增长。它的增长图如下,可以直观的感受一下:
所以,该方案的优势和劣势都非常明显。简单高效,但只限于4-5人。
实操
上面说了它是一对多直播模式的升级版,所以代码也是在一对多的基础上进行简单改动即可。
其实只需要改动两个地方即可,一个是获取用户列表的地方,另一个是连接发送offer
的地方,代码如下:
这个是在获取用户列表的地方改动
// 一对多
linkSocket.value.on("roomUserList", async (data) => {
roomUserList.value = data;
if (roomUserList.value.length) {
await initMeetingRoomPc();
initDanmuContainer();
}
});
// ------------------------------------------------------
// 多对多
linkSocket.value.on("roomUserList", (data) => {
roomUserList.value = data.filter((e) => e.userId !== String(userId));
console.log("房间成员列表", data);
// 拿到房间成员列表后 初始化pc
newUser.value = data.find((e) => e.isNew) ?? undefined; // 找到新用户
// 如果新用户是自己 则啥也不用干 等着被连接就行
// 不是自己 则要去连接新用户的pc
if (newUser.value?.userId !== userId) {
initMeetingRoomPc();
}
});
这个是连接时发送offer
的地方改动
// 一对多
const initMeetingRoomPc = async () => {
if (userInfo.pub) {
localStream.value = await getLocalUserMedia({ video: true, audio: true });
//将本地直播流挂到video标签,在自己的页面显示
setDomVideoStream("video", localStream.value);
}
const localUserId = userInfo.userId;
// 找到当前房间的视频流发布者,即主播
const pub = roomUserList.value.find((item) => item.pub === "pub");
if (!pub) {
return;
}
// 如果是自己 且 自己是主播 则直接返回
if (pub.userId === localUserId) {
return;
}
publisher.value = pub;
// 和发布者建立rtc连接 不发送自己的视频流
const pcKey = localUserId + "-" + publisher.value.userId;
let pc = RtcPcMaps.get(pcKey);
if (!pc) {
pc = new PeerConnection();
RtcPcMaps.set(pcKey, pc);
}
console.log(RtcPcMaps, "RtcPcMaps-------------");
// sendrecv 表示发送和接收都开启 sendonly 表示只发送不接收 recvonly 表示只接收不发送
pc.addTransceiver("audio", { direction: "recvonly" });
pc.addTransceiver("video", { direction: "recvonly" });
onPcEvent(pc, localUserId, publisher.value.userId);
// 创建数据通道
await createDataChannels(pc, localUserId, publisher.value.userId);
// 创建offer
const offer = await pc.createOffer();
// 设置offer为本地描述
await pc.setLocalDescription(offer);
// 发送offer给远端
const params = {
userId: localUserId,
targetUserId: publisher.value.userId,
offer,
};
linkSocket.value.emit("offer", params);
};
// ------------------------------------------------------
// 多对多
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,
});
};
仔细对比看看,其实改动很小。看多对多的initMeetingRoomPc
函数,其实就是一对一里面的initCallerInfo
,忘记了的话可以回头看看。
一对多的本质上还是一对一,只是连接次数变了,变成了一个人连接多个人。然后多对多在一对多的基础上升级,变成多个一对多,但本质上还是一对一。所以,不管是一对多还是多对多,连接方式都还是一对一,代码都没啥区别。主要是连接次数的变化,一对多是 N 次,多对多是 N * (N - 1) / 2 次。
小节
本小节介绍了如何使用Mesh方案来实现多人音视频通话。该方案的优点是延迟低、画质好、数据传输全在客户端,不经过服务器。但缺点也很明显,只限于4-5个人,当人数过多时,成本会飞速上升。然后分析了如何实现,以及它和一对多音视频、一对一音视频的相同点和不同点,最后给出了具体实现代码。
下一小节内容还没想好,等周末再更新了0.0