The Final Act: Streaming Video and Receiving it in a WebRTC Video Conference (Part 3/3)

baliachbryan

Brian Baliach

Posted on May 19, 2023

The Final Act: Streaming Video and Receiving it in a WebRTC Video Conference (Part 3/3)

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 part 1 and part 2.

Here's the link to the repo.

Here's the original post.

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>
Enter fullscreen mode Exit fullscreen mode

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 = {};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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});
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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});
});
Enter fullscreen mode Exit fullscreen mode

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));
  }
});
Enter fullscreen mode Exit fullscreen mode

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));
  }
});
Enter fullscreen mode Exit fullscreen mode

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];
  }
});
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

πŸ’– πŸ’ͺ πŸ™… 🚩
baliachbryan
Brian Baliach

Posted on May 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related