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 目前正在持续开发中。有想要学习的小伙伴可以加入进来,一起交流学习

相关推荐
梅秃头2 分钟前
vue2+elementUI实现handleSelectionChange批量删除-前后端
前端·javascript·elementui
请叫我欧皇i5 分钟前
el-form动态标题和输入值,并且最后一个输入框不校验
前端·javascript·vue.js
工业互联网专业6 分钟前
毕业设计选题:基于ssm+vue+uniapp的驾校预约管理系统小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
工业互联网专业21 分钟前
毕业设计选题:基于ssm+vue+uniapp的面向企事业单位的项目申报小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
小小李程序员32 分钟前
css边框修饰
前端·css
我爱画页面1 小时前
使用dom-to-image截图html区域为一张图
前端·html
忧郁的西红柿1 小时前
HTML-DOM模型
前端·javascript·html
bin91531 小时前
【油猴脚本】00010 案例 Tampermonkey油猴脚本,动态渲染表格-添加提示信息框,HTML+Css+JavaScript编写
前端·javascript·css·bootstrap·html·jquery
花花鱼1 小时前
vue3 本地windows下的字体的引用
vue.js