文章目录

  • 前言
  • 系统架构
    • Mesh 架构
    • MCU 架构
    • SFU 架构
  • 具体代码
    • RTC.ts
    • SFU.ts

前言

山大会议 基于 WebRTC 技术实现多人同时在线的视频会议功能。但是 WebRTC 技术是一项针对 P2P 实现的实时通讯技术,这意味着我们无法直接使用 WebRTC 实现多人的视频会议,因此,在对 WebRTC 技术有一定程度的熟悉后,我将 WebRTC 技术封装为了一组能够支持多人在线的视频会议工具类。

系统架构

目前,要使用 WebRTC 实现支持多人的视频聊天功能,主流的架构有三种:

  • Mesh
  • MCU
  • SFU

Mesh 架构

Mesh 架构对流量和带宽的要求极大,它本质上就是在每一个与会者之间建立起完全图网络,每个用户之间互相进行 P2P 通信。这种架构的好处是实现起来比较基础,且服务器负载较小。但是由于连接的流较多,因此对客户端的资源占用也非常大。

MCU 架构

MCU 架构是一种重后端服务器的架构,它将编码、转码、解码、混合的任务都交给了后端的 MCU 服务器。其优点是所需要的带宽少,每个用户与服务器只需要建立一条双向流即可,但是对服务器压力极高,服务器成本也会增高。

SFU 架构

SFU 是一种折中的架构,它允许用户只需上传一个流至服务器即可,由服务器对流进行转发。SFU架构看似和MCU一样都有一个中心化的服务器,但是SFU的服务器只负责转发媒体或者存储媒体;不直接做编码、转码、解码、混合这些算力要求较高的工作;SFU服务器接到RTP包后直接转发,因此SFU架构服务端压力相对较小

考虑到服务器成本等一系列问题,我们最终选择采用 SFU 架构进行实现。

具体代码

RTC.ts

// RTC.ts
import { EventEmitter } from 'events';
import { receiverCodecs, senderCodecs } from 'Utils/Constraints';const ices = 'stun:stun.stunprotocol.org:3478'; // INFO: 一个免费的 STUN 服务器export interface RTCSender {pc: RTCPeerConnection;offerSent: boolean;
}
export interface RTCReceiver {offerSent: boolean;pc: RTCPeerConnection;id: number;stream?: MediaStream;
}export default class RTC extends EventEmitter {_sender!: RTCSender;_receivers!: Map<number, RTCReceiver>;constructor(sendOnly: boolean) {super();if (!sendOnly) this._receivers = new Map();}getSender() {return this._sender;}getReceivers(pubId: number) {return this._receivers.get(pubId);}createSender(pubId: number, stream: MediaStream): RTCSender {let sender = {offerSent: false,pc: new RTCPeerConnection({iceServers: [{ urls: ices }],}),};for (const track of stream.getTracks()) {sender.pc.addTrack(track);}if (localStorage.getItem('gpuAcceleration') !== 'false')sender.pc.getTransceivers().find((t) => t.sender.track?.kind === 'video')?.setCodecPreferences(senderCodecs);this.emit('localstream', pubId, stream);this._sender = sender;return sender;}createReceiver(pubId: number): RTCReceiver {const _receiver = this._receivers.get(pubId);// INFO: 阻止重复建立接收器if (_receiver) return _receiver;try {const pc = new RTCPeerConnection({iceServers: [{ urls: ices }],});pc.onicecandidate = (e) => {// console.log(`receiver.pc.onicecandidate => ${e.candidate}`);};// 添加收发器pc.addTransceiver('audio', { direction: 'recvonly' });pc.addTransceiver('video', { direction: 'recvonly' });pc.ontrack = (e) => {if (localStorage.getItem('gpuAcceleration') !== 'false')pc.getTransceivers().find((t) => t.receiver.track.kind === 'video')?.setCodecPreferences(receiverCodecs);// console.log(`ontrack`);const receiver = this._receivers.get(pubId) as RTCReceiver;if (!receiver.stream) {receiver.stream = new MediaStream();// console.log(`receiver.pc.onaddtrack => ${receiver.stream.id}`);this.emit('addtrack', pubId, receiver.stream);}receiver.stream.addTrack(e.track);};let receiver = {offerSent: false,pc: pc,id: pubId,stream: undefined,};// console.log(`createReceiver::id => ${pubId}`);this._receivers.set(pubId, receiver);return receiver;} catch (e) {// console.log(e);throw e;}}closeReceiver(pubId: number) {const receiver = this._receivers.get(pubId);if (receiver) {this.emit('removestream', pubId, receiver.stream);receiver.pc.close();this._receivers.delete(pubId);}}
}

SFU.ts

// SFU.ts
import { EventEmitter } from 'events';
import { globalMessage } from 'Utils/GlobalMessage/GlobalMessage';
import RTC, { RTCSender } from './RTC';export default class SFU extends EventEmitter {_rtc: RTC;userId: number;userName: string;meetingId: number;socket: WebSocket;sender!: RTCSender;sfuIp: string;sendOnly: boolean;constructor(sfuIp: string, userId: number, userName: string, meetingId: string) {super();// this.sendOnly = false;this.sendOnly = userId < 0;this._rtc = new RTC(this.sendOnly);this.userId = userId;this.userName = userName;this.meetingId = Number(meetingId);// const sfuUrl = 'ws://localhost:3000/ws';// const sfuUrl = 'ws://webrtc.aiolia.top:3000/ws';// const sfuUrl = 'ws://121.40.95.78:3000/ws';// TOFIX: 巩义的代码有问题,会返回 127.0.0.1this.sfuIp = sfuIp === '127.0.0.1:3000' ? '121.40.95.78:3000' : sfuIp;console.log(this.sfuIp);const sfuUrl = `ws://${this.sfuIp}/ws`;this.socket = new WebSocket(sfuUrl);this.socket.onopen = () => {// console.log('WebSocket连接成功...');this._onRoomConnect();};this.socket.onmessage = (e) => {const parseMessage = JSON.parse(e.data);// if (parseMessage && parseMessage.type !== 'heartPackage') console.log(parseMessage);switch (parseMessage.type) {case 'newUser':this.onNewMemberJoin(parseMessage);break;case 'joinSuccess':// console.log(parseMessage);this.onJoinSuccess(parseMessage);break;case 'publishSuccess':// 这里是接到有人推流的信息this.onPublish(parseMessage);break;case 'userLeave':// 这里是有人停止推流if (!this.sendOnly) this.onUnpublish(parseMessage);break;case 'subscribeSuccess':// 这里是加入会议后接到已推流的消息进行订阅this.onSubscribe(parseMessage);break;case 'chatSuccess':this.emit('onChatMessage', parseMessage.data);break;case 'heartPackage':// 心跳包// console.log('heartPackage:::');break;case 'requestError':globalMessage.error(`服务器错误: ${parseMessage.data}`);break;default:console.error('未知消息', parseMessage);}};this.socket.onerror = (e) => {// console.log('onerror::');console.warn(e);this.emit('error');};this.socket.onclose = (e) => {// console.log('onclose::');console.warn(e);};}_onRoomConnect = () => {// console.log('onRoomConnect');this._rtc.on('localstream', (id, stream) => {this.emit('addLocalStream', id, stream);});this._rtc.on('addtrack', (id, stream) => {if (id < 0 && id !== -this.userId) {this.emit('addScreenShare', id, stream);} else {this.emit('addRemoteStream', id, stream);}});this.emit('connect');};join() {// console.log(`Join to [${this.meetingId}] as [${this.userName}:${this.userId}]`);let message = {type: 'join',data: {userName: this.userName,userId: this.userId,meetingId: this.meetingId,},};this.send(message);}// 新成员入会onNewMemberJoin(message: any) {this.emit('onNewMemberJoin', message.data.newUserInfo);}// 成功加入会议onJoinSuccess(message: any) {this.emit('onJoinSuccess', message.data.allUserInfos);if (this.sendOnly) return;for (const pubId of message.data.pubIds) {console.log(`${this.userId} 准备接收 ${pubId}`);this._onRtcCreateReceiver(pubId);}}send(data: any) {this.socket.send(JSON.stringify(data));}publish(stream: MediaStream) {this._createSender(this.userId, stream);}_createSender(pubId: number, stream: MediaStream) {try {// 创建一个senderlet sender = this._rtc.createSender(pubId, stream);this.sender = sender;// 监听IceCandidate回调sender.pc.onicecandidate = async (e) => {if (!sender.offerSent) {const offer = sender.pc.localDescription;sender.offerSent = true;this.publishToServer(offer, pubId);}};// 创建Offersender.pc.createOffer({offerToReceiveVideo: false,offerToReceiveAudio: false,}).then((desc) => {sender.pc.setLocalDescription(desc);});} catch (error) {// console.log('onCreateSender error =>' + error);}}publishToServer(offer: RTCSessionDescription | null, pubId: number) {let message = {type: 'publish',data: {jsep: offer,pubId,userId: this.userId,meetingId: this.meetingId,},};// console.log('===publish===');// console.log(message);this.send(message);}onPublish(message: any) {const pubId = message['data']['pubId'];// 服务器返回的Answer信息 如A ---> Offer---> SFU---> Answer ---> Aif (this.sender && pubId === this.userId) {// console.log('onPublish:::自已发布的Id:::' + message['data']['pubId']);this.sender.pc.setRemoteDescription(message['data']['jsep']);return;}if (this.userId > 0 && pubId !== this.userId && pubId !== -this.userId) {// 服务器返回其他人发布的信息 如 A ---> Pub ---> SFU ---> B// console.log('onPublish:::其他人发布的Id:::' + pubId);// 使用发布者的userId创建Receiverthis._onRtcCreateReceiver(pubId);}}onUnpublish(message: any) {// console.log('退出用户:' + message['data']['leaverId']);const leaverId = message['data']['leaverId'];this._rtc.closeReceiver(leaverId);if (leaverId > 0) {this.emit('removeRemoteStream', leaverId);} else {this.emit('removeScreenShare', leaverId);}}_onRtcCreateReceiver(pubId: number) {try {let receiver = this._rtc.createReceiver(pubId);receiver.pc.onicecandidate = () => {if (!receiver.offerSent) {const offer = receiver.pc.localDescription;receiver.offerSent = true;this.subscribeFromServer(offer, pubId);}};// 创建Offerreceiver.pc.createOffer().then((desc) => {receiver.pc.setLocalDescription(desc);});} catch (error) {// console.log('onRtcCreateReceiver error =>' + error);}}subscribeFromServer(offer: RTCSessionDescription | null, pubId: number) {let message = {type: 'subscribe',data: {jsep: offer,pubId,userId: this.userId,meetingId: this.meetingId,},};// console.log('===subscribe===');// console.log(message);this.send(message);}onSubscribe(message: any) {// 使用发布者的Id获取Receiverconst receiver = this._rtc.getReceivers(message['data']['pubId']);if (receiver) {// console.log('服务器应答Id:' + message['data']['pubId']);if (receiver.pc.remoteDescription) {console.warn('已建立远程连接!');} else {receiver.pc.setRemoteDescription(message['data']['jsep']);}} else {// console.log('receiver == null');}}
}

【山大会议】多人视频通话 WebRTC 工具类搭建相关推荐

  1. 【山大会议】私人聊天频道 WebRTC 工具类

    文章目录 序言 逻辑设计 私人 WebRTC 工具类代码 序言 在山大会议中,我们不仅要实现多人视频会议,我们还需要实现一个类似 QQ.微信 这样的即时通讯服务.在这个一对一的私人聊天服务中,我们添加 ...

  2. 【山大会议】注册页的编写

    文章目录 渲染进程代码 index.jsx App.jsx 修改登录页代码 渲染进程代码 在 src/Views 文件夹下,我们新建一个 Register 文件夹,其中是我们的注册页面. index. ...

  3. java ppt转html_word,ppt,excel转pdf,pdf转html工具类搭建

    我看到很多需求要求word,excel,ppt,pptx转pdf等工具类.还有就是pdf转图片转html这里介绍一个这个工具类. 引入pom.xml com.aspose aspose-pdf 11. ...

  4. Jedis连接池:JedisPool及连接池工具类搭建

    文章目录 Jedis连接池 连接池建立步骤 代码案例 JedisPoolUtils工具类 创建配置文件 编写工具类 编写测试代码 Jedis连接池 连接池建立步骤 JedisPool的配置参数大部分是 ...

  5. python 下载大文件,断点续传 | Python工具类

    目录 前言 依赖 工具代码 总结 前言 实用python进行大文件下载的时候,一旦出现网络波动问题,导致文件下载到一半.如果将下载不完全的文件删掉,那么又需要从头开始,如果连续网络波动,是不是要头秃了 ...

  6. 山大计算机毕业生去向,985就业:山东大学的毕业生们都被哪些单位录取了?19届就业情况...

    原标题:985就业:山东大学的毕业生们都被哪些单位录取了?19届就业情况 明年,#山东大学#即将迎来120周年校庆!这座教育大省的985高校也一直都被全国各地的学子们所"注视".那 ...

  7. WebRTC现状以及多人视频通话分析

    1.WebRTC 概述 WebRTC(网页实时通信技术)是一系列为了建立端到端文本或者随机数据的规范,标准,API和概念的统称.这些对等端通常是由两个浏览器组成,但是WebRTC也可以被用于在客户端和 ...

  8. webRtc+websocket多人视频通话

    webRTc+ websocket实现多人视频通话,目前此demo只支持crome浏览器, 版本仅仅支持:ChromeStandalone_46.0.2490.80_Setup.1445829883 ...

  9. 山大青岛计算机学院郑雯,山东大学自招700余人过线 面试将刷掉20%考生

    前天下午5点至昨天凌晨,"北约""华约""卓越"准时开通了自主招生成绩查询入口,发布了2014年自主招生笔试结果.首次将省内外考生同时纳入& ...

最新文章

  1. Cannot get Python include directory. Is distutils installed
  2. 细数Ajax Control Toolkit 34个服务器端控件
  3. 【翻译】TCP backlog在Linux中的工作原理
  4. 3211: 花神游历各国
  5. python3.6.0安装步骤
  6. Codeforces Round#310 div2
  7. 2022年快手磁力金牛服饰行业营销洞察报告
  8. MySQL sql_model问题研究
  9. Go -- 调用C/C++
  10. Git修改提交的用户名和Email
  11. Windows 软件管理
  12. 阿里巴巴Xin公益大会揭示个人参与公益事业的力量
  13. 前端基础:通过 《砸金蛋》小游戏实践CSS的id选择器和class选择器
  14. 文件夹下载器案例实战
  15. suspense源码分析
  16. 连接超时与读取超时概述
  17. 14期《掬水月在手,弄花香满衣》1月刊
  18. oracle窗口设置,ORACLE安装DISPLAY变量设置 go with
  19. 在linux下运用mutt和msmtp发邮件
  20. NOIWC 2019 冬眠记【游记】

热门文章

  1. python爬取qq空间说说
  2. WVGA,QVGA,VGA,HVGA区别
  3. C++ lambda递归
  4. 写给‘真‘零经验的童鞋学习编程的建议
  5. 本体李俊|区块链的实际业务场景需要哪些技术模块?
  6. 计算机三级相当于什么水平,【catti笔译三级证书相当于什么水平?】- 环球网校...
  7. 跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等
  8. c语言第二单元测试,知到计算机程序设计C语言第二单元章节测试答案
  9. 第十二章 牛市股票还亏钱—外观模式
  10. 隼鸟2号着陆“龙宫”并采集样品