webrtc实现一对多直播模式和弹幕发送功能

在上一篇webrtc实现一对一音视频和类IM即时通讯中已经实现了一对一视频,大体上熟悉了WebRTC的基本用法以及它的会话流程。WebRTC的本质就是 P2P,即点对点的即时通讯,现在学习一下一对多直播模式。

基础构思

在开始之前,先熟悉下这个最简单的讲课场景,首先看下图模拟场景,T 作为老师,需要将自己的画面实时地发送给下面的三个学生,但是学生却不需要将自己的画面同步给老师,而仅仅是在需要反馈的时候给予老师反馈即可。这个场景就是一对多直播模式。

场景很清晰了,现在来构思和实战了。

WebRTC实现 P2P 视频通话以及 类IM即时通讯 都没问题了,就是说 T 和 S-1 、S-2、S-3 单独完成视频通话和普通消息发送都没问题,那怎么实现一次性同时和三个学生建立通话呢?

很简单,老师 T 和他们三个单独建立视频通话后,将关联关系都保存在本地不就可以?

前面反复提到过WebRTC的核心就是PeerConnection对象,任何建立视频通话的双方都离不开这个对象,因为这里面包含连接双方的核心协商数据。所以只要 T 和三个学生建立关联关系时,都维护一份独立的PeerConnection对象即可。

如上图,老师端保存三份独立的PeerConnection对象,而学生端只需要保存自己和老师的关联信息,即一份核心对象。用代码描述如下:

老师端

js 复制代码
// T:9999 S-1:1 S-2:2 S-3:3 分别代表上面流程图中的师生
const RtcPcMaps = new Map()
const TS01= 9999-1
const TS02= 9999-2
const TS03= 9999-3
RtcPcMaps.set(TS01, new PeerConnection()) //维护T-S-1关系
RtcPcMaps.set(TS01, new PeerConnection()) //维护T-S-2关系
RtcPcMaps.set(TS01, new PeerConnection()) //维护T-S-3关系 

学生端

js 复制代码
//S-1:10001 
const RtcPcMaps = new Map()
const S01T= 10001-9999 
RtcPcMaps.set(S01T, new PeerConnection()) //学生维护和老师的关联

//S-2:10002
const RtcPcMaps = new Map()
const S02T= 10002-9999 
RtcPcMaps.set(S02T, new PeerConnection()) //学生维护和老师的关联

//S-3:10003
const RtcPcMaps = new Map()
const S03T= 10003-9999 
RtcPcMaps.set(S03T, new PeerConnection()) //学生维护和老师的关联

可以很清晰地看到,在学生端只需要维护一份和老师的关系即可,在建立关联之后,将老师的直播流拉取,然后在本地展示。

实战

有了上面的大体构思和基础伪代码,接下来思考下从老师直播到学生观看直播,这整个过程架构上的整体设计。

首先,老师讲课学生听课,实际上就是在一个房间,老师在黑板书写,而学生看黑板。也可以说老师在房间内直播,学生在下面看直播。

所以,实际上我们整体的架构设计,就是围绕一个"房间"展开,在房间中学生和老师互动。

但是,考虑一下,同一个房间中有老师和学生,那么如何区分老师和学生的身份呢?这就是我们架构设计上的第一个重点,那就是加入这个房间后,所有用户的身份标识。有了标识,后面加入的学生才知道和有老师身份的用户进行WebRTC关联。

js 复制代码
onMounted(() => {
  // 带有pub表示为发布者,即老师
  // 学生是不带有pub的
  // 一个房间内只能有一个发布者
  const { nickName, roomId, userId, pub } = props;
  init(nickName, roomId, userId, pub);
});

在客户端进入这个房间(即页面)时,会带上用户的身份信息和pub标识,表示该用户是学生还是老师。然后会将这些信息保存到服务端:

老师端作为发布者,在加入房间后就需要发布自己的直播流,此时还没有任何学生和他建立连接。

js 复制代码
const initMeetingRoomPc = async () => {
  if (userInfo.pub) {
    localStream.value = await getLocalUserMedia({ video: true, audio: true });
    //将本地直播流挂到video标签,在自己的页面显示
    setDomVideoStream("video", localStream.value);
  }
  ...
 }

学生端 进入房间后,首先获取用户列表,获取到用户列表后找到老师,和老师建立WebRTC连接。

js 复制代码
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;
  }

  publisher.value = pub;

  // 和发布者建立rtc连接 不发送自己的视频流
  const pcKey = localUserId + "-" + publisher.value.userId;
  let pc = RtcPcMaps.get(pcKey);
  if (!pc) {
    pc = new PeerConnection();
    RtcPcMaps.set(pcKey, pc);
  }

  // sendrecv 表示发送和接收都开启 sendonly 表示只发送不接收 recvonly 表示只接收不发送
  pc.addTransceiver("audio", { direction: "recvonly" });
  pc.addTransceiver("video", { direction: "recvonly" });

  onPcEvent(pc, localUserId, publisher.value);

  // 创建数据通道
  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);
};

老师端 在收到学生的关联意向之后,就是正常的一对一视频了,关联的思路还是之前那张图,忘记了可以回看一下webrtc的会话流和信令服务器搭建这篇,多看几遍,对照代码就能记住了,

总结一下,一对多拆开来就是多个一对一,重点就是在进入房间后,要在所有用户中筛选出发布者并与之建立联系,之后的流程就是一对一了。

弹幕实现

这里使用了danmaku这个npm包,在本地直播流挂载到video标签后,再进行初始化弹幕容器,代码如下:

js 复制代码
  linkSocket.value.on("connect", () => {
    console.log("链接成功");

    const timer = setTimeout(async () => {
      if (roomUserList.value.length) {
        await initMeetingRoomPc();
        initDanmuContainer();
      }
      clearTimeout(timer);
    }, 2000);
  });
  
  // 初始化弹幕容器
const initDanmuContainer = () => {
  if (userInfo.pub === "pub") {
    danmaku.value = new Danmaku({
      container: videoWrap.value,
      speed: 30,
    });
  } else {
    danmaku.value = new Danmaku({
      container: videoWrap.value,
      speed: 30,
    });
  }

  // 首条弹幕
  danmaku.value.emit({
    text: "欢迎进入直播间,发个弹幕试试",
    style: {
      color: "red",
      fontSize: "16px",
      marginTop: "20px",
    },
  });
};

这里还可以分别实现老师端学生端的弹幕容器,首次初始化后会发送一条弹幕。

如何实现学生端 发送弹幕,其他所有学生都能看见呢?考虑一下,在这个房间内,谁具有所有学生的关联关系?当然是老师老师端关联了所有其他学生,他可以广播信息给所有其他学生。那么就可以这样考虑实现弹幕功能: 学生先把弹幕发送给老师,再由老师进行广播发送给所有学生。

第一步,先实现学生把弹幕发送给老师,代码如下:

js 复制代码
// 指定数据通道发送数据
const clientDataChannelMsg = (userId, targetUserId, msg) => {
  const channel = dataChannelMap.get(userId + "-" + targetUserId);
  if (channel) {
    channel.send(msg);
  }
};

// 发送弹幕
const sendMsgToPub = () => {
  //  给发布者发送消息  发布者收到再广播给其他客户端
  clientDataChannelMsg(userInfo.userId, publisher.value.userId, barrage.value);
  barrage.value = "";
};

根据学生id和老师id找出两者之间的消息通道,再通过消息通道把消息发送出去给老师。

第二步,老师收到消息后广播信息给所有学生,代码如下:

js 复制代码
pc.ondatachannel = (e) => {
console.log("收到数据通道", e);

e.channel.onopen = () => {
  console.log("通道打开");
};
e.channel.onclose = () => {
  console.log("通道关闭");
};
e.channel.onmessage = (data) => {
  console.log("收到消息", data.data);
  // 弹幕发送到屏幕上
  onAllMessage(data.data);
};


// 广播消息
const onAllMessage = (msg) => {
  danmuForRoller(msg);
  if (userInfo.pub === "pub") {
    // 如果是发布者 则遍历所有数据通道给每个客户端发送消息
    dataChannelMap.forEach((value, key) => {
      if (value.readyState === "open") {
        value.send(msg);
      } else {
        // 处理通道未打开的情况,可以等待通道打开后再发送数据
        value.onopen = () => {
          value.send(msg);
        };
      }
    });
  }
};

实现起来还是挺简单的,捋清楚关系后就容易实现了。

项目操作

项目地址 前端: gitee.com/yoboom/webr... 后端:gitee.com/yoboom/webr...

js 复制代码
# 主播进入 房间号:10012 用户ID:1001 用户昵称:suke001
https://xx/#/demo4?userId=123223444&roomId=小程直播间&nickName=JAVA&pub=pub
# 非主播进入
https://xx/#/demo4?userId=12&roomId=小程直播间&nickName=Jack
# 非主播进入
https://xx/#/demo4?userId=123&roomId=小程直播间&nickName=Rose

下一章节会结合人工智能模型来实现虚拟背景

另外, 推荐一下我的另一个开源项目:问卷平台

使用的技术栈为react + ts + echarts + 高德地图 + webrtc 目前正在持续开发中。有想要学习的小伙伴可以加入进来,一起交流学习

相关推荐
风无雨3 分钟前
react antd 项目报错Warning: Each child in a list should have a unique “key“prop
前端·react.js·前端框架
人无远虑必有近忧!4 分钟前
video标签播放mp4格式视频只有声音没有图像的问题
前端·video
安分小尧5 小时前
React 文件上传新玩法:Aliyun OSS 加持的智能上传组件
前端·react.js·前端框架
编程社区管理员5 小时前
React安装使用教程
前端·react.js·前端框架
小小鸭程序员5 小时前
Vue组件化开发深度解析:Element UI与Ant Design Vue对比实践
java·vue.js·spring·ui·elementui
拉不动的猪5 小时前
vue自定义指令的几个注意点
前端·javascript·vue.js
yanyu-yaya5 小时前
react redux的学习,单个reducer
前端·javascript·react.js
陌路物是人非5 小时前
SpringBoot + Netty + Vue + WebSocket实现在线聊天
vue.js·spring boot·websocket·netty
skywalk81635 小时前
OpenRouter开源的AI大模型路由工具,统一API调用
服务器·前端·人工智能·openrouter
Liudef065 小时前
deepseek v3-0324 Markdown 编辑器 HTML
前端·编辑器·html·deepseek