当前位置: 首页 > article >正文

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

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

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

基础构思

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

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

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

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

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

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

老师端

// 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关系 

学生端

//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关联。

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

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

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

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

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

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标签后,再进行初始化弹幕容器,代码如下:

  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",
    },
  });
};

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

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

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

// 指定数据通道发送数据
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找出两者之间的消息通道,再通过消息通道把消息发送出去给老师。

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


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);
        };
      }
    });
  }
};

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

项目操作

项目地址

# 老师进入 
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

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓


http://www.kler.cn/a/272676.html

相关文章:

  • FPGA车牌识别
  • 计算机网络 (47)应用进程跨越网络的通信
  • c语言第一天
  • AI在SEO中的关键词优化策略探讨
  • 数字化时代,传统代理模式的变革之路
  • DNS介绍与部署-Day 01
  • 【机器学习】无监督学习:解锁数据中的潜在结构与关系
  • rsa数据加密无大小限制——golang实现
  • 华为认证大数据工程师(HCIA-Big Data)--填空题
  • 回到街头 - 数字时尚嘉年华:Web3的时尚未来,4月香港兰桂坊盛大启幕
  • SSM框架,MyBatis-Plus的学习(下)
  • 代码+视频,R语言使用BOOT重抽样获取cox回归方程C-index(C指数)可信区间
  • 闯关升级游戏特点,闯关小程序游戏开发
  • acwing算法提高之搜索--剪枝
  • Verilog中`include的用法
  • 网络面试题整理
  • VisualStudio的使用
  • java数据结构与算法刷题-----LeetCode55. 跳跃游戏
  • 组件化开发
  • 视频桥接芯片#LT8912B适用于MIPIDSI转HDMI+LVDS应用方案,提供技术支持。
  • 算法——贪心
  • 中霖教育好吗?口碑怎么样?
  • JavaWeb:vue、AJax、ELement、maven、SpringBoot、、Http、Tomcat、请求响应、分层解耦
  • Tailwind CSS如何使用
  • 探寻未来之路:计算机行业发展趋势与机遇
  • 可视化搭建一个智慧零售订单平台