Overview

I'm currently working on a peer-to-peer video chat built with the WebRTC API. As of now, the application has the following functionality:

  • 1st User can initiate a call which, in turn, generates a unique chat room id
  • 2nd User can answer the call by inputting the chat room Id
  • Both have the ability to end the call at any given moment

Application flow

A challenging aspect of this project was organising the business logic and state of the application as it has two users that are interacting with each other in real-time.

From a high-level perspective the application does the following to facilitate a peer connection:

  • On call initiation, the user’s camera feed is added to the peer connection and an empty media stream is added for the remote user. Additionally, event listeners are added to handle the incoming remote stream and update the connection state. (Mirrored for both users).
async function setupStreams() {
    const userMedia = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });
    userMedia.getTracks().forEach((track) => pc.addTrack(track, userMedia));
    setMediaStreams([userMedia]);
    // when a new track has been added to the peer conection
    // set the value to remoteStream
    pc.ontrack = (e) => {
      const incomingStream = new MediaStream();
      e.streams[0]
        .getTracks()
        .forEach((track) => incomingStream.addTrack(track));
      setMediaStreams((current) => current ? [current[0], incomingStream] : current);
    };
    // interface returns a string value describing the state of the signaling process on the local end of the connection.
    pc.onsignalingstatechange = (e) => {
      setConnectionStatus(pc.connectionState);
    };
    // interface indicates the current state of the peer connection by returning one of the following string values:
    // new, connecting, connected, disconnected, failed, or closed.
    pc.onconnectionstatechange = (e) => {
      setConnectionStatus(pc.connectionState);
    };
  }
  • The user that initiates the call creates a peer connection offer that is then stored in the database.
  • The user who answers the call writes their answer to the offer document in firebase.
  • Firebase snapshot events trade potential IP addresses between the two users. When received on each side they will be added as ICE candidates.
// createOffer.ts
// When answered, add ICE candidates list to peer connection
  answerCandidates.onSnapshot((snapshot) => {
    snapshot.docChanges().forEach((change) => {
      if (change.type === "added") {
        const candidate = new RTCIceCandidate(change.doc.data());
        pc.addIceCandidate(candidate);
      }
   });
 });

// acceptOffer.ts
// When a offer candidate is added to db, add it to peerConnection
  offerCandidates.onSnapshot((snapshot) => {
    snapshot.docChanges().forEach((change) => {
      if (change.type === "added") {
        let data = change.doc.data();
        pc.addIceCandidate(new RTCIceCandidate(data));
      }
   });
 });
  • Once a set of ICE candidates is mutually agreed upon a connection will be established and the event set up with the call initiation will trigger and both users will receive their peer’s media stream.

Github repository

Link to repository.