WebRTC Demystified — Signaling with Firebase
A convenient way to signal users to each other
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.
npm install 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 ofpeerConnection
. 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
ofpeerConnection
. From this offer, we shall create anofferSDP
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. IfanswerSDP
is ready and we didn’t setremoteDescription
ofpeerConnection
, we setanswerSDP
asremoteDescription
in the form ofRTCSessionDescription
. - 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 ourremoteDescription
. - By setting the
offerSDP
as peer connection’sremoteDescription
, we can now create an answer and set it aslocalDescription
. - 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…