当前位置:网站首页>[Shanda conference] private chat channel webrtc tools
[Shanda conference] private chat channel webrtc tools
2022-06-22 16:04:00 【What does Xiao Li Mao eat today】
List of articles
preface
In the Shanda Conference , We not only need to implement multi person video conferencing , We also need to implement a similar QQ、 WeChat Such instant messaging services . In this one-to-one private chat service , We have added a one-to-one private video call function , One is to increase the diversity of software functions , Second, it also paves the way for the realization of multi person chat , To be familiar with the first WebRTC Operation in actual environment .
logic design
First , We need to design the code logic of private video calls . We were 【 Yamada conference 】WebRTC Underlying peer connections This article introduces the basic WebRTC The process of peer connection . Its establishment is essentially a process of two handshakes :
- The initiator sends... To the receiver
OFFERrequest , And bring your ownSessionDescriptionProtocol( Later abbreviated assdp); - The receiver receives from the sender
sdp, establishANSWER, GeneratesdpReturn to the sender ; - The sender receives... From the receiver
sdpafter , Set up WebRTC Peer connection .
In the private video chat module , We decided to adopt De centralization Of 、P2P Architecture design , The central server only does Signaling forwarding function , Thus, the insufficient bandwidth of the server itself can be bypassed , It is difficult to support high-resolution pictures .
secondly , Because we added session encryption , The initiator can choose whether this call needs to be encrypted , The receiver needs to know whether the other party has enabled encryption , To prompt the user whether to have an encrypted conversation .
Because our server does not SSL certificate , The data sent by the server and the client are in clear text , This means that we cannot pass keys over untrusted channels . therefore , We use a negotiation algorithm to generate a one-time key , The negotiation process also requires a handshake . Final , We simplify the process , The logical flow of the final connection establishment process is obtained :
For the convenience of description , We will The initiative be called A , Passive party be called B .
- A towards B Initiate a session request , Which carries Key agreement Some information needed ;
- B Receive A Request , According to what you carry Negotiation information Judge A Whether to enable encryption , And reply , If you agree to a conversation and A Encryption enabled , Then continue according to Negotiation information To calculate the Public key And Private key , take Public key Send back to A;
- A After receiving the reply message , Judge B Agree to session , If agreed, generate OFFER request , carry sdp Send to B , If encryption is enabled , Then get B Returned Public key , Through the algorithm Private key ;
- B Receive OFFER Ask for something to do with sdp, take sdp Save as Remote descriptor , And create ANSWER Request to get Local descriptor , And send it back to A;
- A Receive ANSWER , Will be one of the sdp Save as Remote descriptor , Both parties add Ice candidates , Establish peer connection .
private WebRTC Tool class code
// ChatRTC.tsx
import {
AlertOutlined,
CheckOutlined,
CloseOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import Modal from 'antd/lib/modal';
import {
globalMessage } from 'Components/GlobalMessage/GlobalMessage';
import {
EventEmitter } from 'events';
import React from 'react';
import {
ChatSocket } from 'Utils/ChatSocket/ChatSocket';
import {
CALL_STATUS_ANSWERING,
CALL_STATUS_CALLING,
CALL_STATUS_FREE,
CALL_STATUS_OFFERING,
ChatWebSocketType,
DEVICE_TYPE,
PRIVATE_WEBRTC_ANSWER_TYPE,
receiverCodecs,
senderCodecs,
} from 'Utils/Constraints';
import eventBus from 'Utils/EventBus/EventBus';
import {
getDeviceStream, getMainContent } from 'Utils/Global';
import {
AUDIO_TYPE, buildPropmt } from 'Utils/Prompt/Prompt';
import {
setCallStatus, setNowChattingId, setNowWebrtcFriendId } from 'Utils/Store/actions';
import store from 'Utils/Store/store';
import {
eWindow } from 'Utils/Types';
import {
setupReceiverTransform, setupSenderTransform } from 'Utils/WebRTC/RtcEncrypt';
interface ChatRtcProps {
socket: ChatSocket;
myId: number;
}
export class ChatRTC extends EventEmitter {
callAudioPrompt: (() => void)[];
answerAudioPrompt: (() => void)[];
socket: ChatSocket;
myId: number;
localStream: null | MediaStream;
remoteStream: null | MediaStream;
sender?: number;
receiver?: number;
peer!: RTCPeerConnection;
answerModal!: null | {
destroy: () => void;
};
offerModal!: null | {
destroy: () => void;
};
candidateQueue: Array<any>;
useSecurity: boolean;
security: string;
constructor(props: ChatRtcProps) {
super();
this.callAudioPrompt = buildPropmt(AUDIO_TYPE.WEBRTC_CALLING, true);
this.answerAudioPrompt = buildPropmt(AUDIO_TYPE.WEBRTC_ANSWERING, true);
this.socket = props.socket;
this.myId = props.myId;
this.localStream = null;
this.remoteStream = null;
this.useSecurity = false;
this.security = '[]';
this.candidateQueue = new Array();
this.socket.on('ON_PRIVATE_WEBRTC_REQUEST', (msg) => {
this.responseCall(msg);
});
this.socket.on('ON_PRIVATE_WEBRTC_RESPONSE', ({
accept, sender, receiver, security }) => {
if (sender === this.sender && receiver === this.receiver) {
this.callAudioPrompt[1]();
if (this.offerModal) {
this.offerModal.destroy();
this.offerModal = null;
}
if (accept === PRIVATE_WEBRTC_ANSWER_TYPE.ACCEPT) {
this.createOffer(security);
} else {
switch (accept) {
case PRIVATE_WEBRTC_ANSWER_TYPE.BUSY:
globalMessage.error({
content: ' The other party is on the phone ',
duration: 1.5,
});
break;
case PRIVATE_WEBRTC_ANSWER_TYPE.OFFLINE:
globalMessage.error({
content: ' The caller is not online ',
duration: 1.5,
});
break;
case PRIVATE_WEBRTC_ANSWER_TYPE.REJECT:
globalMessage.error({
content: ' The other party rejected your call invitation ',
duration: 1.5,
});
break;
}
this.onEnded();
}
}
});
this.socket.on('ON_PRIVATE_WEBRTC_OFFER', (msg) => {
if (msg.sender === this.sender && msg.receiver === this.receiver)
this.createAnswer(msg.sdp);
});
this.socket.on('ON_PRIVATE_WEBRTC_ANSWER', (msg) => {
this.receiveAnswer(msg.sdp);
});
this.socket.on('ON_PRIVATE_WEBRTC_CANDIDATE', (msg) => {
if (msg.sender === this.sender && msg.receiver === this.receiver) {
this.handleCandidate(msg);
}
});
this.socket.on('ON_PRIVATE_WEBRTC_DISCONNECT', (msg) => {
globalMessage.info(' The other party has hung up ');
this.onHangUp(msg);
});
}
callRemote(targetId: number, myName: string, offerModal: any) {
this.useSecurity = localStorage.getItem('securityPrivateWebrtc') === 'true';
this.callAudioPrompt[0]();
store.dispatch(setCallStatus(CALL_STATUS_OFFERING));
store.dispatch(setNowWebrtcFriendId(targetId));
this.sender = this.myId;
this.receiver = targetId;
let pgArr: Array<string> = [];
(async () => {
if (this.useSecurity) {
pgArr = await eWindow.ipc.invoke('DIFFIE_HELLMAN');
}
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_REQUEST,
sender: this.myId,
senderName: myName,
security: JSON.stringify(pgArr),
receiver: targetId,
});
this.offerModal = offerModal;
})();
}
responseCall(msg: any) {
this.sender = msg.sender;
this.receiver = this.myId;
const rejectOffer = (reason: number) => {
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_RESPONSE,
security: '',
accept: reason,
sender: msg.sender,
receiver: msg.receiver,
});
};
if (store.getState().callStatus === CALL_STATUS_FREE) {
eventBus.emit('GET_PRIVATE_CALLED');
this.answerAudioPrompt[0]();
store.dispatch(setNowChattingId(msg.sender));
const pgArr = JSON.parse(msg.security);
const useSecurity = pgArr.length === 3;
this.answerModal = Modal.confirm({
icon: useSecurity ? <AlertOutlined /> : <ExclamationCircleOutlined />,
title: ' Video call invitation ',
content: (
<span>
user {
msg.senderName}(id: {
msg.sender}) Send you a video call request , Accept or not ?
{
useSecurity ? (
<span>
<br />
Be careful : The other party has enabled the private chat video session encryption function , Accepting this session may result in your CPU Occupancy has been greatly increased , Please confirm with the other party and choose whether to accept this session
</span>
) : (
''
)}
</span>
),
cancelText: (
<>
<CloseOutlined />
Refuse to accept
</>
),
okText: (
<>
<CheckOutlined />
Agree to the request
</>
),
onOk: () => {
this.useSecurity = useSecurity;
if (useSecurity) {
eWindow.ipc
.invoke('DIFFIE_HELLMAN', pgArr[0], pgArr[1], pgArr[2])
.then((serverArr) => {
const [privateKey, publicKey] = serverArr;
this.security = privateKey;
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_RESPONSE,
accept: PRIVATE_WEBRTC_ANSWER_TYPE.ACCEPT,
sender: this.sender,
receiver: this.receiver,
security: publicKey,
});
});
} else {
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_RESPONSE,
accept: PRIVATE_WEBRTC_ANSWER_TYPE.ACCEPT,
sender: this.sender,
receiver: this.receiver,
security: '',
});
}
store.dispatch(setCallStatus(CALL_STATUS_ANSWERING));
store.dispatch(setNowWebrtcFriendId(msg.sender));
},
onCancel: () => {
rejectOffer(PRIVATE_WEBRTC_ANSWER_TYPE.REJECT);
this.answerModal = null;
this.sender = undefined;
this.receiver = undefined;
},
afterClose: this.answerAudioPrompt[1],
centered: true,
getContainer: getMainContent,
});
} else rejectOffer(PRIVATE_WEBRTC_ANSWER_TYPE.BUSY);
}
async createOffer(publicKey: string) {
this.peer = this.buildPeer();
this.localStream = new MediaStream();
this.localStream.addTrack(
(await getDeviceStream(DEVICE_TYPE.VIDEO_DEVICE)).getVideoTracks()[0]
);
this.localStream.addTrack(
(await getDeviceStream(DEVICE_TYPE.AUDIO_DEVICE)).getAudioTracks()[0]
);
for (const track of this.localStream.getTracks()) {
this.peer.addTrack(track, this.localStream);
}
// NOTE: encryption
if (publicKey) {
const privateKey = await eWindow.ipc.invoke('DIFFIE_HELLMAN', publicKey);
this.security = privateKey;
this.peer.getSenders().forEach((sender) => {
setupSenderTransform(sender, privateKey);
});
} else {
this.peer
.getTransceivers()
.find((t) => t.sender.track?.kind === 'video')
?.setCodecPreferences(senderCodecs);
}
this.peer
.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
})
.then((sdp) => {
this.peer.setLocalDescription(sdp);
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_OFFER,
sdp: sdp.sdp,
sender: this.sender,
receiver: this.receiver,
});
});
}
async createAnswer(remoteSdp: any) {
this.peer = this.buildPeer();
this.peer.setRemoteDescription(
new RTCSessionDescription({
sdp: remoteSdp,
type: 'offer',
})
);
while (this.candidateQueue.length > 0) {
this.peer.addIceCandidate(this.candidateQueue.shift());
}
this.localStream = new MediaStream();
this.localStream.addTrack(
(await getDeviceStream(DEVICE_TYPE.VIDEO_DEVICE)).getVideoTracks()[0]
);
this.localStream.addTrack(
(await getDeviceStream(DEVICE_TYPE.AUDIO_DEVICE)).getAudioTracks()[0]
);
this.emit('LOCAL_STREAM_READY', this.localStream);
for (const track of this.localStream.getTracks()) {
this.peer.addTrack(track, this.localStream);
}
// NOTE: encryption
if (this.useSecurity) {
this.peer.getSenders().forEach((sender) => {
setupSenderTransform(sender, this.security);
});
} else {
this.peer
.getTransceivers()
.find((t) => t.sender.track?.kind === 'video')
?.setCodecPreferences(senderCodecs);
}
this.peer
.createAnswer({
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
},
})
.then((sdp) => {
this.peer.setLocalDescription(sdp);
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_ANSWER,
sdp: sdp.sdp,
sender: this.sender,
receiver: this.receiver,
});
store.dispatch(setCallStatus(CALL_STATUS_CALLING));
});
}
async receiveAnswer(remoteSdp: any) {
this.peer.setRemoteDescription(
new RTCSessionDescription({
sdp: remoteSdp,
type: 'answer',
})
);
store.dispatch(setCallStatus(CALL_STATUS_CALLING));
this.emit('LOCAL_STREAM_READY', this.localStream);
}
handleCandidate(data: RTCIceCandidateInit) {
this.candidateQueue = this.candidateQueue || new Array();
if (data.candidate) {
// NOTE: Need to wait signalingState Turn into stable To add a candidate
if (this.peer && this.peer.signalingState === 'stable') {
this.peer.addIceCandidate(data);
} else {
this.candidateQueue.push(data);
}
}
}
hangUp() {
this.callAudioPrompt[1]();
this.socket.send({
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_DISCONNECT,
sender: this.sender,
receiver: this.receiver,
target: store.getState().nowWebrtcFriendId,
});
this.offerModal = null;
this.onEnded();
}
onHangUp(data: {
sender: number; receiver: number }) {
const {
sender, receiver } = data;
if (sender === this.sender && receiver === this.receiver) {
if (this.answerModal) {
this.answerAudioPrompt[1]();
this.answerModal.destroy();
}
this.answerModal = null;
this.onEnded();
}
}
/** * establish RTCPeer Connect * @returns After creation RTCPeer Connect */
private buildPeer(): RTCPeerConnection {
const peer = new (RTCPeerConnection as any)({
iceServers: [
{
urls: 'stun:stun.stunprotocol.org:3478',
},
],
encodedInsertableStreams: this.useSecurity,
}) as RTCPeerConnection;
peer.onicecandidate = (evt) => {
if (evt.candidate) {
const message = {
type: ChatWebSocketType.CHAT_PRIVATE_WEBRTC_CANDIDATE,
candidate: evt.candidate.candidate,
sdpMid: evt.candidate.sdpMid,
sdpMLineIndex: evt.candidate.sdpMLineIndex,
sender: this.sender,
receiver: this.receiver,
target: store.getState().nowWebrtcFriendId,
};
this.socket.send(message);
}
};
peer.ontrack = (evt) => {
// NOTE: Decrypt
if (this.useSecurity) setupReceiverTransform(evt.receiver, this.security);
else
peer.getTransceivers()
.find((t) => t.receiver.track.kind === 'video')
?.setCodecPreferences(receiverCodecs);
this.remoteStream = this.remoteStream || new MediaStream();
this.remoteStream.addTrack(evt.track);
if (this.remoteStream.getTracks().length === 2)
this.emit('REMOTE_STREAM_READY', this.remoteStream);
};
// NOTE: Disconnection detection
peer.oniceconnectionstatechange = () => {
if (peer.iceConnectionState === 'disconnected') {
this.emit('ICE_DISCONNECT');
}
};
peer.onconnectionstatechange = () => {
if (peer.connectionState === 'failed') {
this.emit('RTC_CONNECTION_FAILED');
}
};
return peer;
}
changeVideoTrack(newTrack: MediaStreamTrack) {
if (this.localStream && this.peer) {
const oldTrack = this.localStream.getVideoTracks()[0];
this.localStream.removeTrack(oldTrack);
this.localStream.addTrack(newTrack);
this.peer
.getSenders()
.find((s) => s.track === oldTrack)
?.replaceTrack(newTrack);
}
}
/** * Clear the data after ending the call */
onEnded() {
this.sender = undefined;
this.receiver = undefined;
this.useSecurity = false;
this.security = '[]';
store.dispatch(setNowWebrtcFriendId(null));
this.localStream = null;
this.remoteStream = null;
if (this.peer) this.peer.close();
this.candidateQueue = new Array();
store.dispatch(setCallStatus(CALL_STATUS_FREE));
}
}
边栏推荐
- Gbase "library" special training of innovation and application Committee of Beijing fintech Industry Alliance
- A simple understanding of hill ordering
- 中信建投证券是跟启牛学堂存在什么关系?开证券账户安全吗
- Scala语言学习-04-函数作为参数传入函数-函数作为返回值
- js中const定义变量及for-of和for-in
- Scala language learning-05-a comparison of the efficiency of recursion and tail recursion
- 静态断言 static_assert
- 【newman】postman生成漂亮的测试报告
- CVE-2022-0847(提权内核漏洞)
- Scala language learning-06-differences between name passing parameters, value passing parameters and function passing parameters
猜你喜欢

微信小程序头像挂件制作

Simulation of vector

让pycharm项目里面的文本模板支持jinjia2语法

英国考虑基于国家安全因素让Arm在伦敦上市

Cve-2022-0847 (privilege lifting kernel vulnerability)

On the routing tree of gin

Research on ICT: domestic databases focus on the ICT market, and Huawei Gauss is expected to become the strongest

Trust level of discover

(pytorch进阶之路二)word embedding 和 position embedding

(pytorch advanced path 2) word embedding and position embedding
随机推荐
Hello, big guys. Error reporting when using MySQL CDC for the first time
The bank card identification function of Huawei machine learning service enables bank card identification and binding with one click
【山大会议】WebRTC基础之对等体连接
Quickly play ci/cd graphical choreography
B树和B+树
Ask if you want to get the start of sqlserver_ Is there a good way for LSN?
84. (cesium chapter) movement of cesium model on terrain
School enterprise alliance is on the way! Huawei cloud gaussdb has come to universities again
String的模拟实现
小白操作Win10扩充C盘(把D盘内存分给C盘)亲测多次有效
Focus on creating a net red product. The xinjietu x70s is newly launched, starting from 87900
Trust level of discover
Turn to: jackwelch: strategy is to think less and be quick to act
希尔排序的简单理解
The difference between nvarchar and varchar
【一起上水硕系列】Day Three - video
[single chip microcomputer] [make buzzer sound] know the buzzer and let it make the sound you want
【山大会议】多人视频通话 WebRTC 工具类搭建
jmeter关联登录302类型的接口
C language learning -18-makefile file writing examples and how to generate and call dynamic libraries