Skip to content

Instantly share code, notes, and snippets.

@Ibadichan
Last active March 12, 2021 09:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ibadichan/f6f7ffba85326196583d09a1a394e449 to your computer and use it in GitHub Desktop.
Save Ibadichan/f6f7ffba85326196583d09a1a394e449 to your computer and use it in GitHub Desktop.
The implementation of WebRTC connection.
import socket from 'shared/sockets/OASocket';
/**
* Class representing a MeetingConnection.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation}
* to understand "The WebRTC perfect negotiation pattern".
*/
class MeetingConnection {
/**
* Creates a meetingConnection.
* @param {Object} params - The params for creating meeting connection.
* @param {MediaStream} params.localStream - The local media stream.
* @param {string} params.participantSocketId - The socket id of other end user.
* @param {string} params.peerConnectionId - The unique string for each connection.
* @param {boolean} params.polite - The type of user.
* @param {function} params.onReceiveRemoteStream - The callback for ontrack event.
* @param {boolean} params.skipNegotiation - The flag to turn on/off perfect negotiation.
*/
constructor(params) {
this.localStream = params.localStream;
this.participantSocketId = params.participantSocketId;
this.peerConnectionId = params.peerConnectionId;
this.polite = params.polite;
this.onReceiveRemoteStream = params.onReceiveRemoteStream;
this.skipNegotiation = params.skipNegotiation;
this.makingOffer = false;
this.ignoreOffer = false;
this.isSettingRemoteAnswerPending = false;
this.isActive = true;
this.handleCallServiceMessage = this.handleCallServiceMessage.bind(this);
socket.on('call-service-message', this.handleCallServiceMessage);
this.initializeConnection();
}
/**
* Destroys a meeting connection, disables camera
* @param {Object} [params={ stopLocalTracks: true }] - The destroy params.
*/
destroy(params = { stopLocalTracks: true }) {
const { peerConnection } = this;
if (peerConnection) {
this.removeEventListenersFromPeerConnection();
if (params.stopLocalTracks) {
this.stopLocalStreamTracks();
}
peerConnection.close();
this.peerConnection = null;
this.isActive = false;
socket.off('call-service-message', this.handleCallServiceMessage);
}
}
/**
* Handles offer/answer/candidate messages.
* if offer is received, set remote description, create answer and send it to other side.
* if answer is received, set remote description
* if candidate is received, add it to peer connection.
* @param {Object} message - The message containing sdp or candidate.
*/
async handleCallServiceMessage(message) {
if (message.peerConnectionId !== this.peerConnectionId) return;
const {
description,
candidate,
} = message;
const {
peerConnection,
} = this;
try {
if (description) {
const readyForOffer = !this.makingOffer
&& (
peerConnection.signalingState === 'stable'
|| this.isSettingRemoteAnswerPending
);
const offerCollision = description.type === 'offer' && !readyForOffer;
this.ignoreOffer = !this.polite && offerCollision;
if (this.ignoreOffer) return;
this.isSettingRemoteAnswerPending = description.type === 'answer';
if (offerCollision) {
await Promise.all([
peerConnection.setLocalDescription({ type: 'rollback' }),
peerConnection.setRemoteDescription(description),
]);
} else {
await peerConnection.setRemoteDescription(description);
}
this.isSettingRemoteAnswerPending = false;
if (description.type === 'offer') {
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit('call-service-message', {
to: this.participantSocketId,
peerConnectionId: this.peerConnectionId,
description: peerConnection.localDescription,
});
}
} else if (candidate) {
try {
await peerConnection.addIceCandidate(candidate);
} catch (error) {
if (!this.ignoreOffer) throw error;
}
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
/**
* Initializes the peer connection.
*/
initializeConnection() {
try {
this.createPeerConnection();
this.attachEventListenersToPeerConnection();
this.addTracksFromLocalStreamToPeerConnection();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
/**
* Creates peer connection
* @returns {RTCPeerConnection} instance of RTCPeerConnection.
*/
createPeerConnection() {
const config = {
iceServers: [
{
urls: ['stun:turn2.l.google.com'],
},
{
urls: [`turn:${process.env.REACT_APP_OA_TURN_DOMAIN}`],
credential: process.env.REACT_APP_OA_TURN_PASSWORD,
username: process.env.REACT_APP_OA_TURN_USERNAME,
},
],
};
const peerConnection = new RTCPeerConnection(config);
this.peerConnection = peerConnection;
return peerConnection;
}
/**
* Attaches event listeners to peer connection.
*/
attachEventListenersToPeerConnection() {
const { peerConnection } = this;
peerConnection.onnegotiationneeded = this.handleNegotiationNeededEvent.bind(this);
peerConnection.onicecandidate = this.handleICECandidateEvent.bind(this);
peerConnection.ontrack = this.handleTrackEvent.bind(this);
peerConnection.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent.bind(this);
}
/**
* Removes event listeners from peer connection
* (useful on destroy peer connection).
*/
removeEventListenersFromPeerConnection() {
const { peerConnection } = this;
peerConnection.onnegotiationneeded = null;
peerConnection.onicecandidate = null;
peerConnection.ontrack = null;
peerConnection.oniceconnectionstatechange = null;
}
/**
* Attaches local media tracks to peer connection.
*/
addTracksFromLocalStreamToPeerConnection() {
const {
localStream,
peerConnection,
} = this;
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream);
});
}
/**
* Stop local media tracks of peer connection.
*/
stopLocalStreamTracks() {
const {
localStream,
} = this;
localStream.getTracks().forEach((track) => track.stop());
}
/**
* Replaces old track of peer connection with a new one.
* @param {MediaStreamTrack} track - The audio/video track to replace old track.
*/
replaceTrackForPeerConnection(track) {
const { peerConnection } = this;
if (!peerConnection) return;
const trackType = track.kind;
try {
const senders = peerConnection.getSenders();
const desiredSender = senders.find((sender) => (
sender.track.kind === trackType
));
if (desiredSender) {
desiredSender.replaceTrack(track);
} else {
throw new Error('Desired sender not found.');
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
/**
* Handles onnegotiationneeded event.
* Tries to create offer and send it to other side.
* @param {Event} options - useful options used in offer creation.
*/
async handleNegotiationNeededEvent(options) {
const {
peerConnection,
skipNegotiation,
} = this;
if (skipNegotiation) {
return;
}
if (peerConnection.signalingState === 'have-remote-offer') return;
if (this.makingOffer) {
return;
}
try {
this.makingOffer = true;
const offer = await peerConnection.createOffer(options);
if (peerConnection.signalingState !== 'have-remote-offer') {
await peerConnection.setLocalDescription(offer);
socket.emit('call-service-message', {
to: this.participantSocketId,
peerConnectionId: this.peerConnectionId,
description: peerConnection.localDescription,
});
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
this.makingOffer = false;
}
}
/**
* Handles ontrack event.
* If onReceiveRemoteStream is present, call it with received remote stream.
* @param {Event} event - Object containing remote stream.
*/
handleTrackEvent(event) {
const { streams } = event;
if (this.onReceiveRemoteStream && streams[0]) {
this.onReceiveRemoteStream({
stream: streams[0],
participantSocketId: this.participantSocketId,
});
}
}
/**
* Handles icecandidate event.
* Sends ICE candidates to other side using signaling server.
* @param {Event} event - Object containing candidate.
*/
handleICECandidateEvent(event) {
const { candidate } = event;
if (candidate) {
socket.emit('call-service-message', {
to: this.participantSocketId,
peerConnectionId: this.peerConnectionId,
candidate,
});
}
}
/**
* Listens for connection state change, try to restart if connection fails.
*/
handleICEConnectionStateChangeEvent() {
const { peerConnection } = this;
switch (peerConnection.iceConnectionState) {
case 'failed':
// eslint-disable-next-line no-console
console.error('iceConnectionState is failed.');
if (peerConnection.restartIce) {
peerConnection.restartIce();
} else {
peerConnection.onnegotiationneeded({
iceRestart: true,
});
}
break;
default:
break;
}
}
}
export default MeetingConnection;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment