The Final Act: Streaming Video and Receiving it in a WebRTC Video Conference (Part 3/3)
- Brian Baliach
- 19 May 2023
Welcome back, fellow video conferencing enthusiasts! In the previous parts of our blog series, we've covered accessing the webcam's video stream and setting up a signalling server. Now it's time for the grand finale: setting up the client to stream their video and receive video from other clients. Let's dive in!
Here's the link to the repo.
Our HTML file includes a local video element and a remoteVideos div where we'll add video elements for each connected peer. We're also including the socket.io library and a script that contains the main logic.
<video class="video" id="localVideo" autoplay playsinline muted></video>
<div id="remoteVideos"></div>
<script src="/socket.io/socket.io.js"></script>
We start off by defining some constants and an empty object called peers
to store the PeerConnections. We'll also need a function called createPeerConnection
that takes the sender's ID and an ontrack event handler as arguments. This function sets up a new RTCPeerConnection with the specified configuration, including the Google STUN server.
const socket = io();
const roomId = 'test-room';
const localVideo = document.getElementById('localVideo');
const remoteVideos = document.getElementById('remoteVideos');
const configuration = {iceServers: [{urls: 'stun:stun.l.google.com:19302'}]};
const peers = {};
The createPeerConnection
function sets up the icecandidate and ontrack event listeners for the connection. It then returns the new connection.
function createPeerConnection(senderId, ontrack) {
const pc = new RTCPeerConnection(configuration);
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('icecandidate', {receiverId: senderId, candidate: event.candidate});
}
};
pc.ontrack = ontrack;
return pc;
}
Next up, we have the createOffer
function. This will create an offer for the specified sender ID. We first get the user media stream and set the local video source. We then add the stream's tracks to the PeerConnection, create an offer, and set the local description. Finally, we send the offer to the receiver through the signalling server.
function createOffer(senderId) {
const pc = peers[senderId];
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then((stream) => {
localVideo.srcObject = stream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
pc.createOffer()
.then(offer => pc.setLocalDescription(offer))
.then(() => {
socket.emit('offer', {receiverId: senderId, offer: pc.localDescription});
});
});
}
Now, let's wire up our socket event listeners. When a new peer connects, we create a new video element for them, add it to the remoteVideos div, and set up a new PeerConnection. Then, we call the createOffer
function for that peer.
socket.on('peer-connected', (data) => {
const clientId = data.clientId;
const remoteVideo = document.createElement('video');
remoteVideo.id = `remoteVideo_${clientId}`;
remoteVideo.classList.add("video")
remoteVideo.autoplay = true;
remoteVideo.playsInline = true;
remoteVideos.appendChild(remoteVideo);
peers[clientId] = createPeerConnection(clientId, (event) => {
remoteVideo.srcObject = event.streams[0];
});
createOffer(clientId);
});
On receiving an offer, we set the remote description of the PeerConnection and create an answer. We then send the answer back to the sender through the signalling server.
socket.on('offer', async (data) => {
const senderId = data.senderId;
const pc = peers[senderId];
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', {receiverId: senderId, answer});
});
When we receive an answer, we set the remote description of the PeerConnection.
socket.on('answer', async (data) => {
const senderId = data.senderId;
const pc = peers[senderId];
if (pc) {
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
}
});
On receiving an icecandidate event, we add the candidate to the PeerConnection.
socket.on('icecandidate', (data) => {
const senderId = data.senderId;
const pc = peers[senderId];
if (pc) {
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
}
});
Finally, on a peer-disconnected event, we remove the video element and close the PeerConnection.
socket.on('peer-disconnected', (data) => {
const clientId = data.clientId;
const videoElement = document.getElementById(`remoteVideo_${clientId}`);
if (videoElement) {
videoElement.remove();
}
if (peers[clientId]) {
peers[clientId].close();
delete peers[clientId];
}
});
To see this in action, within your package.json
, include this piece of code if it's not there already:
{
"scripts": {
"start": "ts-node server.ts"
}
}
lastly, run npm start
and open tabs in different profiles (i.e. guest mode and incognito mode) to see the different video streams.
And there you have it! Our video conferencing masterpiece is complete. Give yourself a pat on the back. With this setup, you can now stream video and receive video from other clients using WebRTC and socket.io.