Today we will look at how to do a conference call between 2 users using WebRTC
First we will create a Room where the users will see each other, It will display the streams. For this we will create a Room.js file
Room.js
=======
//Import a utils file for webRTC where we will keep all webRTC related calls
import * as webRTCHandler from "../utils/webRTCHandler";
//This function will handle the incoming and outgoing streams
webRTCHandler.getLocalPreviewAndInitRoomConnection();
//This is where we will finally show the streams.
return (
<div id=videos_container”>
</div>
);
This file will contain all webRTC functionality, Here we get our camera stream and store it in global variable
WebRTCHandler.js
================
const defaultConstraints = {
audio: true,
video: {
width: "480",
height: "360",
},
};
let localStream; //Stream coming from our camera
export const getLocalPreviewAndInitRoomConnection = async () => {
//This step gets the camera stream
navigator.mediaDevices
.getUserMedia(defaultConstraints)
.then((stream) => {
console.log(“Got local stream");
localStream = stream; //save this stream in global variable
})
.catch((err) => {
console.log(
"error occurred when trying to get an access to local stream"
);
console.log(err);
});
};
Sockets for communication between clients
Now we will open a communication channel between clients and server, The Clients will send and receive messages from server through sockets, and server is going to be responsible for routing the messages between clients. Sockets client file is wss.js Server file is server.js
Client code
This is the first file which loads and opens a channel to server
App.js
=======
import { connectWithSocketIOServer } from "./utils/wss";
useEffect(() => {
connectWithSocketIOServer();
}, []);
This is how to open a scoket from client. All client socket code is in wss.js
wss.js
=======
import io from "socket.io-client";
const SERVER = "http://localhost:5002";
let socket = null;
export const connectWithSocketIOServer = () => {
socket = io(SERVER);
socket.on("connect", () => {
console.log("connected with socket io server");
});
};
On Server side we will accept the connection
Server.js ========== //First create the server const express = require("express"); const http = require("http"); const PORT = process.env.PORT || 5002; const app = express(); const server = http.createServer(app); //Then accept the connection const io = require("socket.io")(server, { cors: { origin: "*", methods: ["GET", "POST"], }, }); io.on("connection", (socket) => { console.log(`user connected ${socket.id}`); });
Step 1: Prepare a connection
User 1 creates a session (a room) and notifies the server. The server will keep a room with this name.
wss.js
=======
// Client emits
socket.emit("create-session”, name);
On Server we maintain a list of all users and the room they are in. We identify the user with his socket Id. We then send a message to all other users that a new user just joined and ask them to handle the event.
Server.js
=========
let connectedUsers = [];
io.on("connection", (socket) => {
...
socket.on("create-session”, (name) => {
const newUser = {
name,
socketId: socket.id,
};
connectedUsers = [...connectedUsers, newUser];
//Send conn-prepare to other users in this session
connectedUsers.forEach((user) => {
if (user.socketId !== socket.id) {
const data = {
justJoinedUserSocketId: socket.id,
};
io.to(user.socketId).emit("conn-prepare", data);
}
});
});
The other user gets the conn-prepare event from the server, so on client side we need to handle this event for the second user.
wss.js
======
export const connectWithSocketIOServer = () => {
……
socket.on("conn-prepare", (data) => {
const { justJoinedUserSocketId } = data;
//We are sending false as second parameter here,
//it signifies if this is an initiator of this call.
//Since we are getting called from User 1,
//so we are not initiating this call, and we will pass false.
webRTCHandler.prepareNewPeerConnection(justJoinedUserSocketId, false);
});
}
Put the handling in webRTC file. We are going to use simple-peer library to handle the event
webRTCHandler.js
================
import Peer from "simple-peer";
//We add the ice servers (remember my SIP tutorial for NAT support)
const getConfiguration = () => {
return {
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
};
};
//We keep a list of all the peers who have sent us their info in a list peers.
let peers = {};
const prepareNewPeerConnection = (peerSocketId, isInitiator) => {
const configuration = getConfiguration();
peers[peerSocketId] = new Peer({
initiator: isInitiator,
config: configuration,
stream: localStream. //This is stream from this user Not Peer
});
}
Step 2: Initialize the connection
Second user will now reply back that User 2 is now prepared to accept the connection.
wss.js
======
export const connectWithSocketIOServer = () => {
socket.on("conn-prepare", (data) => {
...
// inform the user which just join the room that we have prepared for incoming connection
//User 2 will send back conn-init to User 1 through Server
socket.emit("conn-init", { connUserSocketId: connUserSocketId });
});
}
Server will hand the conn-init event and forward the event data back to User 1
Server.js
=========
io.on("connection", (socket) => {
...
socket.on("conn-init", (data) => {
initializeConnectionHandler(data, socket);
});
}
// information from clients which are already in room that
// they have prepared for incoming connection
const initializeConnectionHandler = (data, socket) => {
const { connUserSocketId } = data;
const initData = { connUserSocketId: socket.id };
io.to(connUserSocketId).emit("conn-init", initData);
};
Now User 1 will handle conn-init event, and call prepare connection with isInitiator = true.
wss.js
=======
socket.on("conn-init", (data) => {
const { connUserSocketId } = data;
// This is User 1 and will call peer connection on this side
// it will pass isInitiatiot = true now,
// since this is the client whcih started the calls
webRTCHandler.prepareNewPeerConnection(connUserSocketId, true); });
Step 3: Write the Signaling handlers
When connection is setup, we will add a handler on the peer object. This will send the data receiver to server as a conn-signal event.
webRTCHandler.js
================
const prepareNewPeerConnection = (peerSocketId, isInitiator) => {
...
//Handle Signals for simple-peer for connection
peers[peerSocketId].on("signal", (data) => {
// webRTC offer, webRTC Answer (SDP informations), ice candidates
const signalData = {
signal: data,
connUserSocketId: connUserSocketId,
};
wss.signalPeerData(signalData);
});
}
Socket file is sending the data here to the sever
wss.js
======
export const signalPeerData = (data) => {
socket.emit("conn-signal", data);
};
Server handles it and emits it to the peer
Server.js
=========
io.on("connection", (socket) => {
...
socket.on("conn-signal", (data) => {
signalingHandler(data, socket);
});
}
const signalingHandler = (data, socket) => {
const { peerSocketId, signal } = data;
const signalingData = { signal, connUserSocketId: socket.id };
io.to(peerSocketId).emit("conn-signal", signalingData);
};
The second user / peer gets the conn-signal event
wss.js
======
export const connectWithSocketIOServer = () => {
...
socket.on("conn-signal", (data) => {
webRTCHandler.handleSignalingData(data);
});
}
It uses webRTC handler to pass on the information to the peer.
webRTCHandler.js
================
export const handleSignalingData = (data) => {
//add signaling data to peer connection
peers[data.connUserSocketId].signal(data.signal);
};
Finally: How to show streams on browser
Once the connection is established, its time to display the streams
Showing stream from own camera
In the webRTC handler where we got the handle to stream, we call a function to display it (its our own stream)
webRTCHandler.js
================
export const getLocalPreviewAndInitRoomConnection = async () => {
navigator.mediaDevices
.getUserMedia(defaultConstraints)
.then((stream) => {
...
localStream = stream;
//Displays the stream
showLocalVideoPreview(localStream);
})
.catch((err) => {
...
});
};
We create new div elements where our video will show up, and add it to the videos_div element in Room.js
webRTCHandler.js
================
const showLocalVideoPreview = (stream) => {
const videoElement = document.createElement("video");
const videoContainer = document.createElement(“video_div");
videoContainer.appendChild(videoElement);
const videosContainer = document.getElementById(“videos_div);
videosContainer.appendChild(videoContainer);
videoElement.autoplay = true;
videoElement.muted = true; //Mute yourself
videoElement.srcObject = stream;
videoElement.onloadedmetadata = () => {
videoElement.play();
};
};
Showing the stream from the other person
We maintain a list of incoming streams.
On the peer when we get a stream (this is a predefined handler like signal above), we mapped this stream when we created the peer object) we simply take the stream, and show it in the videos_div by adding a new element video_div with a new Id for which we can use the socketId to keep it distinct in case of a conference call.
webRTCHandler.js
================
let streams = [];
const prepareNewPeerConnection = (peerSocketId, isInitiator) => {
...
peers[peerSocketId].on("stream", (stream) => {
console.log("new stream came from “peer);
addStream(stream, peerSocketId);
streams = [...streams, stream];
});
}
const addStream = (stream, peerSocketId) => {
//display incoming stream
const videoElement = document.createElement("video");
videoElement.id = `${connUserSocketId}-video`;
const videoContainer = document.createElement(“video_div");
videoContainer.id = peerSocketId;
videoContainer.appendChild(videoElement);
const videosContainer = document.getElementById("videos_div);
videosContainer.appendChild(videoContainer);
videoElement.autoplay = true;
videoElement.srcObject = stream;
videoElement.onloadedmetadata = () => {
videoElement.play();
};
}
Thats it. There is lot of back and forth messaging happening, and some parts are inbuilt in simple-peer like signalling and stream handling. Its a lot of code today, but this is the minimal to get this working.
Thanks for visiting.
Cheers – Amit Tomar