WebRTC Demystified — Signaling with Firebase

A convenient way to signal users to each other

Furkan Can Baytemur
Orion Innovation techClub
8 min readOct 7, 2022

--

In the previous article, we have done the following:

  • Project Setup
  • Base UI for Video Chat
  • Firebase App Setup

In this article, we will establish communication with the help of Firebase. At the end of this article, we will have an application allowing us to see the remote peer’s video and sound.

Import Firebase

Now that we have everything ready for Firestore, we can finally import it to our project. To do this, we should install the node package of Firebase.

After installing it, let’s import the Firestore with credentials that we saw in “Project Settings”.

Firebase should be imported to get our Firestore database. Also, we imported a bunch of methods from firebase/firestore . We won’t use all of them now but they will be required sooner or later.

As we have discussed in the Signaling Demo, the call's initiator must create an offer and send it to the remote peer. The remote peer will create an answer from it and send it back to the initiator.

Create an Offer

That is the function of creating an offer. Don’t be intimidated by the length. We will go over each of them.

  • We are referencing the “calls” object to save data and track changes. Every time we make a call, we will create a new call object with a unique ID. These IDs are determined by the Firestore algorithm and they work similarly to UUID algorithms. We will put this unique ID to input which is just below the buttons.
  • In addition to the “calls” object, we make references to the “offerCandidates” and the “answerCandidates” collections.
  • To add ICE candidates to Firestore, we should listen to onicecandidate event of peerConnection . Every time a new ICE candidate is available, we will upload them to the “offerCandidates” collection in JSON format.
  • To initiate the call, we should create an offer description. We set this description as localDescription of peerConnection . From this offer, we shall create an offerSDP object and upload it directly to our “call” object.
  • Now that we have, uploaded both offerSDP and our ICE candidates, we will be tracking incoming data from the peer data.
  • We are tracking our “call” object and checking if there is a answerSDP ready. If answerSDP is ready and we didn’t set remoteDescription of peerConnection , we set answerSDP as remoteDescription in the form of RTCSessionDescription .
  • In addition, we also track “answerCandidates” collection. If there is a change in this collection, we check if this candidate is a new one by the “added” type. Then we add this candidate to our peerConnection .

Go ahead and bind this function to the button with ID callButton in main.ts file like shown in the code block above. If we are to test the app by turning on the camera and making a call, the result will look like this.

After clicking on the “Make a Call” button, the app will show us an ID. This is the ID of the call that we have made. Let’s check it from the Firestore database.

We can check this call from the Firestore page of our project in Firebase. As we can see we sent our offerSDP object successfully and created the “offerCandidates” collection. This collection consists of more documents which consist of candidates.

We have written a lot of code so it would be good to check your progress from the branch below. We will continue with making an answer to initiated call.

Answer the call

Now that a peer has initiated a call, the remote peer’s task is to get the required credentials from the server and create an answer.

  • Instead of getting the whole document, we are just getting a specific call from Firestore by callID . Like we did in the other peer, we are creating references to “answerCandidates” and “offerCandidates”.
  • To create an answer, we need to have offerDescription ready at our disposal. For this purpose, we are getting SDP data from the incoming call.
  • This incoming call just consists of a offerSDP . We set it as our remoteDescription .
  • By setting the offerSDP as peer connection’s remoteDescription , we can now create an answer and set it as localDescription .
  • Let’s format our answer as SDP as we did for offerSDP and upload it to the Firestore database.
  • All left to do is listen to ICE candidates. It is the same as offer creation but we are listening for the “offerCandidates” collection for this peer.

To execute this function, add an event listener to answerCall button.

To answer the call we need a callID . The user will get this callID from the other peer. After getting it, the remote peer will enter it to offerInput and press the answerCall button. The connection should be successful and the result will look like this from the point of User B:

Congratulations! You have established a WebRTC connection and you are sending and receiving camera streams. As soon as we open the camera, we already add camera and microphone streams to the connection. All left to do is give the callID to the remote peer. By looking up to the Firestore, peers will get required ICE candidates and SDP objects. Knowing the IP addresses of both peers, WebRTC will establish a connection between them and they can communicate directly. Let’s see what is happening in the Firestore database.

As you can see, both candidates and SDP objects are uploaded. Since peers listen to candidates continuously, there is no need to refresh the connection by giving new candidates manually. The only thing users need to do is exchanging callID between them.

Let’s see what is happening behind the scenes. We will examine these from the point of User A. Open up your Chrome or any Chromium browser and enter this URL:

chrome://webrtc-internals/

Here you can see every application that is using WebRTC in your browser. If the project is running in your browser, you will see something like this:

Our application is http:localhost:5173 . When we first open the app, we just create the connection. Just creating the connection will give us these states:

Right now, most of the states are new since we didn’t do anything to RTCPeerConnection apart from creating it. Ignore data channels for now, we will be meddling with them when we create text chat and file sharing.

Go ahead and start the webcam. You will notice more logs popping up.

Here, we can see the tracks we are sending to RTCPeerConnection . These tracks include both video and audio. Once we established the connection, we will see incoming tracks too. Let’s see what is happening when we initiate the call:

If we look at events, we will see a lot of events firing up. Most of them are fired up by ICE candidates. Once the app gathers every ICE candidate it can gather from STUN servers, it will stop and change icegatheringstatechange to complete . Open up a new tab for the app and answer the call.

If the connection is successful, connectionstatechange will change to be connected . We will use this state to add features like hanging up and erasing data from Firestore. You can also see that signalingstatechange has become stable now. This page is very crucial when you need to debug an application using WebRTC since it shows every state for an RTCPeerConnection .

You can access the working project from the branch below.

Hang up the Call

Now that we have established the connection and created a video communication, let’s add the feature to close the connection. To achieve this, add a hang-up button.

We added this button after the offerInput where we provided the callID value. It is disabled in the beginning. Once the call is established, we can turn on the hang-up button. To detect this, we will use connectionstatechange what we have seen in the WebRTC console.

When the call is established, connectionstatechange becomes connected . At this point, we can make enable the hang-up button.

If we add this snippet of code right after we create RTCPeerConnection , the event listener for connected state will enable the hang-up button. Right now the button doesn’t do anything. Let’s add the functionality:

If we add this code to the end of main.ts file, the connection will be closed and the remote camera will be closed. We also disable the hang-up button again for further connections. However, the remote peer will get a frozen camera since it doesn’t do anything if the other user disconnects. In addition, User B might close the application and User A will still get a frozen camera. We need to add an event listener for this.

Let’s see what happens when user B closes the app behind the scenes.

As you can see when the remote peer closes the app, connectionstatechange first becomes disconnected . In this state, it tries to reconnect for a couple of seconds. However, the app could not connect and it fails to connect again. We will be checking for disconnected state to prevent the frozen camera.

In addition to our connected event, we check for disconnected event. Since we created a function for hang-up button, we can use the same function by importing it. You check the progress so far from the branch below:

And that is it for video chatting and signaling with Firebase. In the next articles, we will implement text chat and file sharing features. Stay tuned for more!

Until next time…

--

--