The Final Act: Streaming Video and Receiving it in a WebRTC Video Conference (Part 3/3)
Published: 2023-05-19 22:34:51

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](https://www.balysnotes.com/setting-up-a-peer-to-peer-connection-video-stream) and [part 2](https://www.balysnotes.com/setting-up-a-signaling-server-client-video-streaming).

Here's the link to the [repo](https://github.com/TylerMutai/peer-to-peer-video-conferencing).

Here's the [original post](https://www.balysnotes.com/streaming-video-between-clients).

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.

```html
<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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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:

```json
{
  "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.