当前位置:网站首页>【山大会议】多人视频通话 WebRTC 工具类搭建
【山大会议】多人视频通话 WebRTC 工具类搭建
2022-06-22 14:41:00 【小栗帽今天吃什么】
前言
山大会议 基于 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.1
this.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 {
// 创建一个sender
let 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);
}
};
// 创建Offer
sender.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 ---> A
if (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创建Receiver
this._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);
}
};
// 创建Offer
receiver.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获取Receiver
const 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');
}
}
}
边栏推荐
- GBASE现身说 “库” 北京金融科技产业联盟创新应用专委会专题培训
- js中const定义变量及for-of和for-in
- Promouvoir l'adaptation compatible et permettre le développement collaboratif du Service Express adaptatif gbase en mai
- Sdvo:ldso+ semantics, direct French Slam (RAL 2022)
- Scala language learning-04-function passed in as parameter function as return value
- #进程地址空间
- ORB_VI思想框架
- Yilian technology rushes to Shenzhen Stock Exchange: annual revenue of RMB 1.4 billion, 65% of which comes from Ningde times
- 【题目精刷】2023禾赛-FPGA
- 排序之归并排序
猜你喜欢

uni开发微信小程序自定义相机自动检测(人像+身份证)

【单片机】【让蜂鸣器发声】认识蜂鸣器,让蜂鸣器发出你想要的声音
![[译文] 弥合开源数据库和数据库业务之间的鸿沟](/img/e5/f89a8f3e2e9034f557ea3e76f37f81.jpg)
[译文] 弥合开源数据库和数据库业务之间的鸿沟

Self inspection is recommended! The transaction caused by MySQL driver bug is not rolled back. Maybe you are facing this risk!

Cve-2022-0847 (privilege lifting kernel vulnerability)

pymssql模块使用指南
New design of databend SQL planner

Cross border integration, creativity and innovation to help improve the influence of cultural tourism night tour

Exploration and practice of dewu app data simulation platform

【VTK】模型旋转平移
随机推荐
How MySQL modifies a field to not null
ORB_VI思想框架
A simple understanding of hill ordering
[译文] 弥合开源数据库和数据库业务之间的鸿沟
Merge sort of sorting
[Shangshui Shuo series] day three - VIDEO
又可以这样搞nlp(分类)
#进程地址空间
MongoDB在腾讯零售优码中的应用
Hello, big guys. Error reporting when using MySQL CDC for the first time
Ultimate efficiency is the foundation for the cloud native database tdsql-c to settle down
蓝桥杯2019年国赛最长子序列
How MySQL modifies the storage engine to InnoDB
FPGA collects DHT11 temperature and humidity
基础版现在SQL分析查询不能用了吗?
pymssql模块使用指南
Ask if you want to get the start of sqlserver_ Is there a good way for LSN?
Exploration and practice of dewu app data simulation platform
Scala语言学习-05-递归和尾递归效率对比
TDengine 连接器上线 Google Data Studio 应用商店