From Zero to Hero with WebRTC in JavaScript and Python in small snippets of code. Part 3

Iftimie Alexandru
7 min readFeb 8, 2022

--

Cytonn Photography via Unsplash

In this article, I will build on top of the code from the previous part.

I will add a Django app as a signaling server so we will finally get rid of all that copy-pasting from the previous articles. In the second part of the article, I will introduce the Perfect Negotiation pattern that will allow both peers to be callers and callees.

Introducing the signaling server

Demo

Unlike other tutorials on the internet that use WebSockets for communicating with the signaling server, I decided in this article to use just the HTTP protocol. I believe that using this approach, will reduce the noise, and the code required to understand the logic of WebRTC, although it will have some limitations that you will see later.

Here is the directory in my GitHub repository for the code in this section of the article.

Updating the JS code

We’ll now need a way to differentiate the two peers on the server-side. I added a new variable for the username . In this case, a peer will be either user1 or user2 .

When a peer connects, he will first try to publish his offer. If on the server-side, there is already an offer from the other peer, it will be returned, and the current peer will become polite.

If there isn’t already an offer on the server-side, he becomes impolite and waits for an answer from the other peer that will be in the state mentioned above.

Diff part 2 vs part 2 index.js

In the screenshot below, you can see that one peer waits for an answer from the other peer, by subscribing to an HTTP Server-Side Event (SSE) stream so that the SDP exchange between the 2 peers happens in real-time.

Diff part 2 vs part 3 index.js

I also updated the common.js file where I added a way to invalidate the offers in case the connection fails, or in case the page is refreshed.

Diff part 2 vs part 3 common.js

Django app as signaling server

Our signaling server will have the following routes. The /offer and /answer are for SDP exchange. The /clear is for invalidating the SDP stored on the server.

As I mentioned earlier, in order to keep the interaction in real-time, I used SSE for notifying the peers about answers. For Django, there is a cool library called django-eventstream. It is easy to configure and has a very simple API.

I declared a route for SSE where the two peers can subscribe for the answers.

And finally, the home route is for having two pages with different usernames which will be user1 and user2 .

For the implementation details, I will briefly explain the offer handler. It is used by both peers to send their offers. Their offers are stored in a global variable.

Notice! this is not a very good approach, since in production we will have multiple processes. To deal with this, the offers could be stored in a database and that will only increase the load on the database.

There is also another aspect related to race conditions. Both peers can send their offers at the same time, and because the function is not synchronized, they can end up being the impolite peer at the same time, since the returned SDP will be empty for both.

There are many solutions to this problem, but to keep things simple for now, I used this approach.

Regarding the answer handler, once the polite peer is publishing its SDP, the signaling server will send it to the impolite peer using an SSE.

The rest of the code contains mostly just configurations for Django and it can be found on GitHub.

Adding Perfect Negotiation

In this section, I will simplify the Django app and I will update the Javascript code above with the Perfect Negotiation pattern. This pattern will allow us to avoid race conditions when both peers will be callers and callees, and it will also allow us to easily add even more features in the next article.

Here is the directory in my GitHub repository for the code in this section of the article.

Compared to the previous section, I randomly set the username for the two peers as user1 and user2 just to differentiate between the two when storing the SDP on the server.

This time, we won’t store anything on the server. Same as previously both will be able to be either callers or callees, except that now I will introduce the concept of polite and impolite peer and I will store these properties as their usernames. In case there is an offer collision, these properties will help us avoid the race condition.

The code is mostly adapted from MDN, so if you want to get a better understanding of the pattern, there is a very detailed explanation.

Updating the JS code

In the screenshot below, compared to the previous version, I added a function called withPerfectNegociationHandler that returns a makingOffer variable that will serve as a lock and will help us reject or not a new offer for avoiding collisions.

The beCaller and beCallee functions will have a few more parameters, and the main function will sendOfferSDPjust after the handler has been called. The sendOfferSDP will acquire the lock, and will do well-known steps of creating the offer, gathering the ICE candidates and publishing the SDP to the signaling server

The beCalle the branch is very much similar to the previous version. However, now it includes a condition for creating a new RTCPeerConnection instance in case other peer disconnects.

If this condition wouldn’t exist, the initial instance of RTCPeerConnection couldn’t receive new data channels, even if the offers and answers are exchanged. In this situation, the callee would not receive any new messages from the other peer that refreshes the page.

The caller is very much unchanged, and it no longer needs to wait for answers since the SSE message subscription has been moved in the withPerfectNegociationHandler .

Finally, the new functions that have been added are here

Simplified Django app

We now have fewer routes. The offer and answer SDPs are now sent on the same endpoint, and it publishes them on the SSE channel.

The home endpoint takes the name of the peer which can be polite or impolitefrom the URL and sets it for the Javascript code as a variable.

urlpatterns = [
path('sdp', views.sdp, name='sdp'),
path('events/', include(django_eventstream.urls), {
'channels': ['testchannel']
}),
path('home/<user>', views.home, name='home'),
]

views.py

@csrf_exempt
def sdp(request):
if request.method == "POST":
request_body = json.loads(request.body)
received_offer = json.dumps(request_body['sdp'])
user = request_body['user']
send_event('testchannel', 'message', {"sdp": received_offer, "user": user})
return HttpResponse("ok")
def home(request, user):
return render(request, 'mainapp/index.html', {"user": user})

There is still one more tiny issue with this approach, that I am not particularly proud of, but this is mainly because of my choice of using SSE.
If the impolite peer sent first an offer, but the offer was lost because the polite peer was not yet subscribed to the SSE, the polite peer’s offers will always be rejected. The only way to get out of this situation would be if the impolite peer would send a new offer, in this case, by refreshing the page.

The other way around doesn’t happen (if the polite peer sends an offer while the impolite peer is not yet connected).

A better solution might be if the last SSE from the impolite peer would be stored on the server just to be sent to the polite peer.

Or even better, the use of WebSockets might solve the problem by exchanging the offers only when both are connected.

Conclusions

It took me a lot of time to write this part. Mainly because as I was writing, I was discovering new bugs and edge cases.

My goal was to slowly build up the complexity and to share with you some of the edge cases that I ran into.

The next parts should be easier to follow.

--

--