webrtc的会话流和信令服务器搭建

webrtc基础摄像头操作中,介绍了如何通过浏览器的 API 去操控电脑上的摄像头、麦克风、屏幕分享桌面,这些是实现会议系统必备的基础知识。接下来就要去思考如何实现一个会议系统,以及如何将学到的基础 API 和WebRTC组合

首先,看下面这张图:

这张图介绍了一对一会议系统的实现思路,接下来要根据这张图来一个一个实现。

核心对象 PeerConnection

首先必须得知道WebRTC是如何将远端的两个浏览器关联起来的,因为只有建立关联关系,接下来才有多媒体通信的基础。那么要将两个浏览器关联起来,必须是存在一个通道,类似于桥梁,连接两头。而两个浏览器之间的通道就是PeerConnectionPeerConnection是整个WebRTC通话的载体,如果没有这个对象,那么后面所有流程都是没法进行的。

有一点要注意的是,在不同的浏览器中,WebRTC兼容性不一样,虽然前面开篇词提到它的相关 API 已经成为 W3C 的基础标准,但并不是所有的浏览器都满足这些标准的。WebRTC最先开始是谷歌体系,那么兼容性而言,谷歌浏览器就是首选。国内很多的浏览器也是基于谷歌内核的,因此WebRTC在很大程度上也是兼容的,这里先说几个常用且兼容WebRTC的浏览器:Chrome、360、edge、火狐、Safari。

因此为了尽可能地兼容不同浏览器,获取到有效的PeerConnection对象,可以通过如下方式获取:

js 复制代码
 const PeerConnection = window.RTCPeerConnection ||
        window.mozRTCPeerConnection ||
        window.webkitRTCPeerConnection;

以及它的一些核心方法:

  • addIceCandidate():保存ICE候选信息,即双方协商信息,持续整个建立通信过程,直到没有更多候选信息。
  • addTrack():添加音频或者视频轨道。
  • createAnswer():创建应答信令。
  • createDataChannel():创建消息通道,建立WebRTC通信后,就可以p2p的直接发送文本消息,无需中转服务器。
  • createOffer():创建初始信令。
  • setRemoteDescription():保存远端发送给自己的信令。
  • setLocalDescription():保存自己端创建的信令。

以上就是PeerConnection对象的主要方法了,除了这些核心方法之外,还有一些事件监听函数,这些监听函数用于监听远程发送过来的消息。

假如 A 和 B 建立连接,如果 A 作为主动方即呼叫端,则需要调用的就是上述核心方法 去创建建立连接的信息,而 B 则在另一端使用上述部分核心方法 创建信息再发送给 A,A 则调用事件监听函数去保存这些信息。常用的事件监听函数如下:

  • ondatachannel:创建datachannel后监听回调以及p2p消息监听。
  • ontrack:监听远程媒体轨道即远端音视频消息。
  • onicecandidate:ICE候选监听。

WebRTC的会话流程

知道了两个浏览器之间关联需要上述对象,然后通过该对象的核心方法和事件就可以完成从 A 到 B 两个浏览器的关联。那么关联的具体过程是什么呢?那就是接下来要详细解释的。首先看这个流程图:

对照这个流程图,我们再来捋一捋顺序,上图中 A为caller(呼叫端),B为callee(被呼叫端)

  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 储存,直至候选完成。

这里新的名词 SDP ,这玩意实际就是WebRTC会话的信令,完成以上过程就相当于建立了WebRTC的会话基础,然后你才可以借助这个通道去添加和监听双方的音视频流信息。

信令服务器的搭建

从上述整个流程来看,信令服务器为A、B两者中转信令起了很重要的角色,直白地讲,就是串通A和B的媒介。假如说我的手机是A,你的手机是B,那么我俩联系就需要通过运营商,而运营商的服务器替我们中转呼叫、接听、挂断等操作,在这里,运营商的服务器就是信令服务器

信令服务器听上去很高大上,但实际上,它在不做复杂操作的时候就是个即时通讯服务器,用来转发通话双方需要交换的信息或会话的信息。因此,可以直接写个websocket服务端来完成信令服务器的使命。

当然,要完成信令服务器,也需要有针对性。仅仅只是为了webRTC,那么针对的肯定就是webRTC会话过程中需要的转发逻辑。所以可以设想一下服务端应该具备那些功能呢?看下图:

为了完成上面这个构思,我们可以尝试写出来一个最基本的信令服务器。记住主要目的是什么?一个会议系统 。所以设计的东西一定要满足会议的基本条件,即:用户单独标识和集体标识,也就是一开始必须区分的关键信息userIdroomId,但是怎么存会议室中的用户信息呢? 这里就用到Redis的一种数据结构Hash,存放的大体结构如下图所示:

我是前端,所以肯定是使用nodejs来实现这个信令服务器。

具体代码

js 复制代码
const { hSet,hGetAll,hDel,hDelAll } = require('./redis');
const { getMsg,getParams } = require('./common');

const http = require('http')
const express = require('express')
const app = express()

const server = http.createServer(app);
const io = require('socket.io')(server,{ allowEIO3: true });

//自定义命令空间  nginx代理 /mediaServerWsUrl { http://xxxx:18080/socket.io/ }
// io = io.of('mediaServerWsUrl')

server.listen(18080,async () => {
  console.log('服务启动成功 *:18080');
})

io.on('connection',async (socket) => {
  await onListener(socket)
})


const userMap = new Map() // user - > socket
const roomKey = "meeting-room::"

const getUserDetailByUserId = (userId,roomId,nickName,pub) => {
  return JSON.stringify({
    userId,
    roomId,
    nickName,
    pub
  })
}

const onListener = async (socket) => {
  const url = socket.client.request.url;
  const userId = getParams(url,'userId');
  const roomId = getParams(url,'roomId');
  const nickName = getParams(url,'nickName');
  const pub = getParams(url,'pub');

  userMap.set(userId,socket); // 将用户和socket绑定

  if (roomId) {
    await hSet(roomKey + roomId,userId,await getUserDetailByUserId(userId,roomId,nickName,pub));
    oneToRoomMany(roomId,getMsg('join',nickName + ' 加入房间',200,{ userId,nickName }));
  }

  // 发消息
  socket.on('msg',async (data) => {
    await oneToRoomMany(roomId,data);
  })

  // 断开连接
  socket.on('disconnect',() => {
    userMap.delete(userId);
    if (roomId) {
      hDel(roomKey + roomId,userId);
      oneToRoomMany(roomId,getMsg('leave',nickName + ' 离开房间',200,{ userId,nickName }));
    }
  })

  // 房间用户列表
  socket.on('roomUserList',async (data) => {
    socket.emit('roomUserList',await getRoomOnlyUserList(data.roomId));
  })

  // 呼叫对方
  socket.on('cell',async (data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('cell','远程呼叫',200,data))
  })

  // 候选信息
  socket.on('candidate',(data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('candidate','候选信息',200,data))
  })

  // offer信令监听
  socket.on('offer',(data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('offer','offer信令',200,data))
  })

  // answer信令监听
  socket.on('answer',(data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('answer','answer信令',200,data))
  })

  // applyMic  申请麦克风
  socket.on('applyMic',(data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('applyMic','申请麦',200,data))
  })

  // acceptApplyMic 接受麦克风
  socket.on('acceptApplyMic',(data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('acceptApplyMic','接受麦克风',200,data))
  })

  // refuseApplyMic 拒绝麦克风
  socket.on('refuseApplyMic',(data) => {
    const targetUserId = data.targetUserId
    oneToOne(targetUserId,getMsg('refuseApplyMic','拒绝麦克风',200,data))
  })
}


// 一对一
const oneToOne = (userId,msg) => {
  const s = userMap.get(userId);
  if (s) {
    s.emit('msg',msg);
  } else {
    console.log(userId + ':该用户不在线');
  }
}

// 一对多
const oneToRoomMany = async (roomId,msg) => {
  const list = await hGetAll(roomKey + roomId);
  if (list) {
    for (const i of list) {
      oneToOne(i,msg);
    }
  }
}

// 获取房间用户列表
const getRoomOnlyUserList = async (roomId) => {
  const resList = []
  const list = await hGetAll(roomKey + roomId);

  for (const i of list) {
    const obj = JSON.parse(userMap[i]);
    resList.push(obj);
  }
  return resList
}

代码看不懂没关系,先运行起来看看。

js 复制代码
npm run start ## 启动 
---------------------打印如下则代表信令服务器启动成功--------------------------------- 
> socket-server@1.0.0 start 
> node app.js 
服务器启动成功 *:18080 
redis 连接成功

在看完WebRTC会话流程之后,你会发现,在整个核心会话中,并没有出现媒体信息交换(比如:摄像头、麦克风媒体流的发送和接收)。所以很明显,WebRTC不只可以用来音视频通话。

确实如此,在无需视频通话的时候,我们可以用WebRTC这个桥梁当作是一种新的数据双向传输方案,现阶段已经有网站用这种方式上传用户数据或其他加密消息媒介了,而且因为WebRTC中数据传输协议非HTTP或者WebSocket协议请求,很多探测工具也就没法察觉。

下一节,将利用搭建好的信令服务器,去具体实现最简单的 P2P 音视频通话,同时也为了给大家演示下,WebRTC除音视频场景之外,利用WebRTC已完成会话这个桥梁,去实现无需服务端的点对点 IM 通信。

代码地址: 前端 gitee.com/yoboom/webr... 后端 gitee.com/yoboom/webr...

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

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

相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
福大大架构师每日一题6 小时前
mediamtx v1.21.0 发布:Media-over-QUIC、RTSP、HLS、WebRTC 与安全能力全面升级
安全·webrtc
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端