Is this the moment? The one?

Yes it is! Enjoy!

The Main Page

It is basically the same page as the previous page, we will only add a section to display the two videos (ours and our peer’s). And for convenience, we will add a bit of Javascript to hide the signalling stuff once we established connection. It does like this:

< !doctype html> <html> <head> <meta charset= "UTF-8" > <title> Signalling Server </title> <link rel= "stylesheet" type= "text/css" href= "2_skype-like.css" > </head> <body> <!-- This section is visible only during signalling --> <section id= "signalling" > <h1> Signalling Server </h1> <p> <strong id= "nickname" ></strong> , you are now <span id= "status" ><strong class= "red" > disconnected! </strong></span> <br> <strong id= "calling_status" ></strong> </p> <h2> List of currently connected users </h2> <ul id= "userlist" > </ul> </section> <!-- This section is visible only during a cal --> <section id= "oncall" > <div class= "video_frame" > <!-- Be careful to use 'autoplay'! --> <video id= "localVideo" autoplay ></video> <br> <span> Local Video </span> </div> <div class= "video_frame" > <!-- Be careful to use 'autoplay'! --> <video id= "remoteVideo" autoplay ></video> <br> <span> Remote Video </span> </div> </section> <script src= "2_skype-like.js" ></script> </body> </html>

So, nothing too fancy here. We have the same thing as previously, to display the list of currently connected users, and the new part is the block with id oncall . We included two <video> tags to display both our video and our peer’s, Skype-like!

Please note that the <video> have autoplay attribute. This prevents us from forgetting to call video.play() in Javascript: it can cause headache. Some web devs are against HTML videos to autoplay, and I’m among them: how annoying it is to visit a page and have some video starting somewhere. But in this case, I believe this makes sense.

I’ll include the css below, but there’s very little in it :

.red { color: red ; } .green { color: green ; } .video_frame { text-align: center ; display: inline-block ; vertical-align: top ; } video { border: 1px solid black ; } #oncall { display: none ; } #calling_status { font-size: 1.2em ; color: blue ; }

Okay, the real, interesting part is the Javascript.

So, we wrap up everything in an event, that waits for the DOM to be loaded. It roughly corresponds to jQuery’s main function, although the latter performs more checks. I found that it was a bit overshoot to depend on jQuery for our simple application, so I won’t use it.

document . addEventListener ( "DOMContentLoaded" , function (event) { // Everything (Javascript-related) will be placed here, from now on }

So we begin by defining some variables that we will use all along:

var nickname = prompt ( "Enter a name for the contact list" ) ; if (nickname === null || nickname === "" ) { alert ( "You must enter a name, to be identified on the server" ) ; return ; } // Will hold our peer's nickname var peer = null ; // Here we are using Google's public STUN server, and no TURN server var ice = { "iceServers" : [ { "url" : "stun:stun.l.google.com:19302" } ] }; var pc = null ; // This variable will hold the RTCPeerConnection document . getElementById ( 'nickname' ). innerHTML = nickname ; var constraints = { video : "true" , audio : "true" }; // Prevent us to receive another call or make another call while already in one var isInCall = false ; // Specify if we have to create offers or answers var isCaller = false ; var receivedOffer = null ;

The nickname is important, because this is the name that will be sent to the server, to maintain a contact list.

Warning: as you can see here, I only check if the user actually entered a nickname, but I don’t check for some kind of format validity nor unicity in the server.

It is obvious that you should perform both of these checks in any serious application.

We then define a configuration variable, ice , which holds the host information about the STUN server that we will use. As you can see this is Google’s public STUN server. This is fine for tests. But I advise that you use one of your own for production (besides, there are several easy-to-implement solutions; a STUN server is really not much).

The variable pc is our entry point to manipulate WebRTC. This is our socket/handle to send and talk to the other peer.

And then we define a set of contraints for requesting the media, here, I’ll keep it simple by using both audio and video (again, Skype-like). You can obviously play a bit with these settings, but keep in mind that Firefox and Chrome treat constraints diffently. By the time I am writing this article, Chrome has been updated and now complies with the documentation and will try to honour your constraints, but Firefox still doesn’t. To be clear, Firefox is okay with true and false , but you can’t chose the video’s width and height.

Then isCaller will specify if we are the one who initiated the call or if we received the call. This has some importance in the the order in which we call functions.

At last, receivedOffer will contain the offer sent by the caller (this is used when you are the callee), you’ll see in a minute why.

The next piece of code is temporary (hopefully). As of now, WebRTC is still a new technology and browser manufacturers still use prefixed functions rather than the names given by the documentation. So the next few lines are here to make the calls portable accross browsers:

// For portability's sake navigator . getUserMedia = navigator . getUserMedia || navigator . webkitGetUserMedia || navigator . mozGetUserMedia ; window . RTCPeerConnection = window . RTCPeerConnection || window . mozRTCPeerConnection || window . webkitRTCPeerConnection ; window . RTCSessionDescription = window . RTCSessionDescription || window . mozRTCSessionDescription || window . webkitRTCSessionDescription ; window . RTCIceCandidate = window . RTCIceCandidate || window . mozRTCIceCandidate || window . webkitRTCIceCandidate ;

getUserMedia is to request media (video and sound) to the user, we’ve talked about this earlier.

is to request media (video and sound) to the user, we’ve talked about this earlier. RTCPeerConnection is to create the WebRTC connection with the other peer.

is to create the WebRTC connection with the other peer. RTCSessionDescription is to handle the remote and local session description, we’ll see it in use in a few moment.

is to handle the remote and local session description, we’ll see it in use in a few moment. RTCIceCandidate will be used when we deal with the ICE Agent, to send our candidates to our peer.

From now on, rather than explaining the code in the order it appears in the file, I’ll describe it the way I have built it, following logic. I find it makes much more sense and helps to focus. A the tend of the article, I’ll provide a link to the code in the repo, so you don’t have to worry about the order.

So, first, let’s open a WebSockets connection to our signalling server:

// Open a connection to our server var socket = new WebSocket ( 'ws://192.168.1.35:4444' ) ; // Display an error message if the socket fails to open socket . onerror = function (err) { alert ( "Failed to open connection with WebSockets server.

Either the server is down or your connection is out." ) ; return ; }; // Provide visual feedback to the user when he is disconnected socket . onclose = function (evt) { document . getElementById ( "status" ). innerHTML = "<strong class= \" red \" >disconnected!</strong>" ; }; // When the connection is opened, the server expects us to send our nickname right away socket . onopen = function () { document . getElementById ( "status" ). innerHTML = "<strong class= \" green \" >connected!</strong>" ; socket . send ( JSON . stringify ( { "nickname" : nickname } )) ; };

As you are probably aware, WebSockets hosts begin with ws:// . The onerror handler event occurs if the connection is refused. The onclose event is fired when the connection is closed at some point. Note that onerror does call onclose . I simply write a visual feedback for when we are disconnected.

When the connection is opened successfully, the servers expects us to send our nickname immediately, this is what is done here.

So we have our conneciton opened to the server and we have sent our nickname, we will get registered and the server will send us (and every connected user) the contact list, so this is the first piece of code we’ll write for when receiving a message:

// Parse message, JSON is used for all message transfer try { var dat = JSON . parse ( msg . data ) ; } catch (e) { console . log ( "ERROR - Received wrong-formatted message from server:

" + e) ; socket . close () ; isInCall = false ; isCaller = false ; return ; } // Process userlist : display the names in the contact list if ( dat . userlist ) { var l = dat . userlist ; var domContent = "" ; // Add each user on the list and register a callback function to initiate the call l . forEach ( function (elem) { // Filter out our name from the list: we don't want to call ourselve! if (elem !== nickname) { domContent += "<li><button onclick='navigator.callUser( \" " + elem + " \" );'>" + elem + "</button></li>" ; } } ) ; // Add the generated user list to the DOM document . getElementById ( "userlist" ). innerHTML = domContent ; }

We use JSON for all data exchange, so the first thing we do when we receive a message is try to parse it (this is done in the try / catch block).

First test in the onmessage handler is if we received the userlist from the server. In this user list, there is quite simply the list of all nicknames currently connected (including ours, hence the condition to exclude ourselves from the list), then we build a HTML list ( <ul> ) in which we add the contacts with a button. On each contact button, the navigator.callUser() event is bound.

So the logical next step now is to see what this callUser() function does:

// Initiate a call to a user navigator . callUser = function (who) { document . getElementById ( 'calling_status' ). innerHTML = "Calling " + who + " ..." ; isCaller = true ; peer = who ; startConv () ; };

One particular thing I want to emphasize is that in our design, the page (hence the Javascript code) is the same for the caller and the callee; but the functions to call (especially their orders) are different for the caller and the callee. For this to work, and to write beautiful, elegant code, we will make extensive use of functions and conditions on whether we are the caller (checked with isCaller ) or not.

So what do we do here ?

First we simply give the user some visual feedback that something is happening by writing “Calling XXX…”. Then, since we are the one to call, we set the boolean isCaller to true (it is false by default).

After that we simply register our peer’s name in the peer variable, which is available globally: we do that because we will need it later.

And then we call startConv() , the function which will, obviously, start the conversation.

Let’s take a look at that function. This function (as many others) will actually be called by both peers, so we have to check if we’re calling it because we are initiating a call or because we are answering one:

// Start a call (caller) or accept a call (callee) function startConv () { if (isCaller) { console . log ( "Initiating call..." ) ; } else { console . log ( "Answering call..." ) ; } // First thing to do is acquire media stream navigator . getUserMedia (constraints , onMediaSuccess , onMediaError) ; }; // end of 'startConv()'

What we need to do to start a conversation is create a channel between the peers (the RTCPeerConnection ), acquire media and transmit it.

WebRTC dirt here

It turns out that for your WebRTC application to work, you have to add your media stream (with pc.addStream() ) BEFORE setting your local description (and idem for the callee). I call this dirty because I have yet to find a good explanation of why this is needed, and because I don’t recall the documentation to ever specify that…

Anyway, back to our code sample. the first few lines are just debugging stuff, you can omit them. It simply outputs on the console if we are making a call or answering one. As usual, it might be a good idea to display visual (or audio) feedback to the user, like a phone ringing or a picture of a phone shaking; universal signals that we are placing a call.

The last line is where the real fun begins: since I’ve told you we had to add the stream before doing anything else, we call getUserMedia() to get the media stream. In case of error, onMediaError() is called:

function onMediaError (err) { alert ( "Media was denied access: " + err) ; document . getElementById ( 'calling_status' ). innerHTML = "" ; socket . close () ; isCaller = false ; isInCall = false ; return ; };

which simply notifies the user that the access to the webcam was denied (or it failed for whatever else reason).

When it (hopefully) succeeds, onMediaSuccess is called. Again, this process of acquiring stream is a common task between the caller and the callee; this is why

we define an external function, called onMediaSuccess and use it as a callback rather than using an anonymous function inside this callback, we check on isCaller

So here is the caller part of onMediaSuccess :

function onMediaSuccess (mediaStream) { // Hide the contact list and show the screens document . getElementById ( "signalling" ). style . display = "none" ; document . getElementById ( "oncall" ). style . display = "block" ; // Display our video on our screen document . getElementById ( "localVideo" ). src = URL . createObjectURL (mediaStream) ; // Create the RTCPeerConnection and add the stream to it pc = new window . RTCPeerConnection (ice) ; // Stream must be added to the RTCPeerConnection **before** creating the offer pc . addStream (mediaStream) ; pc . onaddstream = onStreamAdded ; pc . onicecandidate = onIceCandidate ; if (isCaller) { // Calling 'createOffer()' will trigger ICE Gathering process pc . createOffer ( function (offerSDP) { pc . setLocalDescription ( new RTCSessionDescription (offerSDP) , function () { console . log ( "Set local description" ) ; }, function () { console . log ( "Failed to set up local description" ) ; } ) ; }, function (err) { console . log ( "Could not build the offer" ) ; }, constraints) ; }

As we’ve seen in the mirror example, the success callback is passed the MediaStream object.

First, there are a couple of common tasks to do for both the caller and the callee: we first hide the HTML part that displayed the user list (since we are in a call, we won’t be calling someone else, so we might as well hide it) and show the “on call” part of the window, the one with the two screens.

Then, since we now have our local stream: our pretty face (or your sister under the shower, if you have a wireless webcam… and you’re a pervert - don’t do that by the way), we can just show it. This step was already discussed in the mirror example so there should not be anything new.

It is now the time to instance our RTCPeerConnection and make store it in the globally available pc variable. Remember our STUN server’s IP address (stored in the ice variable)? Well if you want to use it, you shall pass it as a parameter.

Okay now that the RTCPeerConnection is created, the very first thing we do now is add our stream (since WebRTC silently requires so); this is done with pc.addStream() .

Then we define the callbacks for pc.onaddstream() and pc.onicecandidate() events. The former will be triggered when our peer will itself call pc.addstream() and the latter is fired whenever our ICE Agent will gather candidates.

If you’re like me and usually write a few lines of code/ functions (with debugging console.log() calls), then try it to see what’s happening (in your console), do not stop here. I tried it, and obviously spent time trying not to bang my head against the wall. I believe I said it earlier but nothing will happen before we called pc.setLocalDescription() ! Now see how ironic this is? In order to get some results, you would be tempted to create your RTCPeerConnection , then register the callback and call setLocalDescription() ? You would have something on the console now, but it would eventually fail because you need to add your stream beforehand.

Now comes the dependant part. We are the caller in this case, so what we need to do now, is create the offer, this is done with pc.createOffer() . Since the recent WebRTC 1.0 review, the API slightly changed. Most of the functions now take a success and an error callback (which are mandatory; well stricly speaking they are not yet, but calling these functions without the callbacks triggers the browser to display a warning in the console, telling you that soon, it would result in an error, thus breaking yor code. This is quite new actually, so you might be surprised if you have already read some WebRTC code example before and did not see callbacks). In addition to the success and error callbacks, some functions take an additional MediaConstraints object as the third parameters. Quite frankly, I don’t really understand why, since the contraints are already available inside the RTCPeerConnection .

The function createOffer() follows that rule. Our error callback is simply a log on the console.

The success callback is passed the offer that was successfully just created.

If you want to inspect it, you can call console.log (JSON.stringify (offerSDP)) . You would see that it is a JSON-formatted object which contains two fields: “type” which can be “offer” or “answer” and “sdp” which contains the actualy SDP information, namely the IP address + port of your interface, the codecs available, etc. It would be pretty small for now, since we haven’t gathered any candidates yet.

It is time to actually start the whole WebRTC procedure by calling setLocalDescription() . You need to give him a RTCSessionDescription which you can build inline from a the SDP offer we have just created. This function, too, comes with a success and error callback (which we only use here to notify the user on the console.)

If you read some WebRTC examples elsewhere, you might see some people sending the offer in the success callback of setLocalDescription() . It is called trickle ICE. Let me explain quickly the difference:

What we are going to do in this application might be summarized like this:

create the offer and set it as the local description (it will then trigger the ICE Agent which will start gathering candidates) monitor this ICE Agent; when it is done gathering candidates, we will send our peer our local description which will contain everything in one batch: the codecs we support, the media we are ready to transmit and receive as well as all our candidates (remember a candidate is bound to our network interface, and contains protocol (UDP or TCP), IP address, port number, etc) our peer will then receive that offer, inspect it, create its answer from that and send it back to us, then the conversation will begin with the best parameters that can be supported by both peers.

This is text-book WebRTC. This is what the documentation specifies and this is what is supported by all browsers. But this is not the best way to do WebRTC: you have to wait for the ICE Agent to finishing gathering every candidates before you can send your offer and, then, you need to wait for your peer to gather himself all its candidates. This takes time.

What you might see is “trickle ICE”. The principle is different:

create the offer, set it as the local description and immediately send that almost empty offer to our peer. when our peer receives the offer, it will do the same and immediately send his answer. during that time, the ICE Agent would have started gathering candidates, everytime one candidate is found, we send it to our peer. This new candidate will be inspected by the WebRTC engine and if it is better, the communication will be transparently switched to use that new candidate (for instance if the first one was TCP, pretty long delays so bad quality and the new candidate uses UDP, which is faster, the conversation will then use this new UDP candidate) otherwise it will stay the same.

People who use trickle ICE usually send their offer as soon as it is created, in the success callback.

This trickle ICE procedure has some benefits: the conversation can start immediately, usually in low quality / low bitrate and can be then enhanced when better candidates are found.

The downside of this is that this is not defined in the documentation, so browser don’t have to implement trickle ICE. Back before some recent updates, I believe Firefox did not support trickle ICE. I am unsure now. So for this article, I prefer to stick to the documentation: this is why we don’t do anything in the success callback.

Okay so what now? We have covered everything in that onMediaSuccess() callback. Well since we called setLocalDescription() , the ICE Agent has started gathering candidates, and everytime it finds a candidate, it calls our onIceCandidate() callback. Let’s inspect it:

function onIceCandidate (evt) { // Wait for all candidates to be gathered, and send our offer to our peer if ( evt . target . iceGatheringState === "complete" ) { console . log ( "ICE Gathering complete, sending SDP to peer." ) ; // Haven't found a way to use one-line condition to substitute "offer" and "answer" if (isCaller) { var offerToSend = JSON . stringify ( { "from" : nickname , "offer" : pc . localDescription } ) ; socket . send ( JSON . stringify ( { "target" : peer , "sdp" : offerToSend } )) ; console . log ( "Sent our offer" ) ; }

As said previously, we don’t use trickle ICE here, so we need to wait for the ICE Agent to finish gathering all the candidates. The state of the gathering process, if you recall from an earlier paragraph, is given by the iceGatheringState properties of the ICE Agent.

The callback is given an Event (here evt ). So in order to access the ICE Agent, we need to call target , which is the object that is responsible for the event. I mention it here because it is easy to forget it and write evt.iceGatheringState and get insulted by Javascript.

So we simply wait for the last candidate to be gathering, which is indicated by the state being on complete .

That gathering process will be done by the callee too, so here is the time to check if we are the caller or the callee. As the caller here, we will send our offer.

Remember how we built our signalling server? We send the server an JSON object which contains exactly two fields: “target” to say whom we send our data to, and “sdp” which contains our data. I admit now that “sdp” is ill-chosen: we don’t always send an SDP, but it is just a matter or name. We build our data, since it will be transmitted to our peer, we need to JSON-format it too (the signalling server will simply fetch whatever is inside the “sdp” field and send it to our peer, so what is inside the “sdp” field must be JSON-formatted).

In that data, we indicate that the data comes from us with the “from” field (this is actually the first time the callee will know it is being called, and by whom) and the actual data which is a SDP (really SDP this time) offer. This offer is obtained from our RTCPeerConnection , with the localDescription attribute.

Now what happens?

Well it is time to go check what happens to our peer. It will receive our offer, so let’s inspect a second chunk of code from the onmessage WebSockets handler (remember, the first one was to check if we received the userlist form the server):

if ( dat . userlist ) { // We already did that part, this is just to remind the context } // If the message is from a peer else if ( dat . from ) { // When we receive the first message form a peer, we consider ourselved in a call if ( ! isInCall) { isInCall = true ; peer = dat . from ; document . getElementById ( 'calling_status' ). innerHTML = peer + " is calling..." ; } if ( dat . offer ) { receivedOffer = dat . offer ; startConv () ; } else if ( dat . answer ) { // We will see that in a few moments } } // Otherwise, this is an error else { alert ( "Received a non-intended message." ) ; socket . close () ; isInCall = false ; isCaller = false ; return ; }

If the message doesn’t contain the userlist attribute (which would indicate it comes from the signalling server) it must contain the from attribute, which indicated it comes from a peer, this is our else if , otherwise, simply fail.

Well first thing we do is store our peer’s name! We make it available in the peer variable and set the isInCall boolean to true. Then.. visual feedback: so we now we are being called.

Okay, time to pay attention here. We just received an offer from our peer, so we have to start all the WebRTC mechanism (remember: we are on the callee’s side now). But the same rule applies: we need to add our stream to the RTCPeerConnection before we do anything else. This is why we store the offer we just received in the global receivedOffer variable. Then we call startConv() , like we did when the caller initiated the call. If you remember, that function was not much different for the caller and the calle, we just had a test case to write “Answering call…” rather than “Initiating call…”

function onMediaSuccess (mediaStream) { // Create the RTCPeerConnection and add the stream to it pc = new window . RTCPeerConnection (ice) ; // Stream must be added to the RTCPeerConnection **before** creating the offer pc . addStream (mediaStream) ; pc . onaddstream = onStreamAdded ; pc . onicecandidate = onIceCandidate ; if (isCaller) { // We alreayd saw that part for the caller } else { pc . setRemoteDescription ( new RTCSessionDescription (receivedOffer) , function () { pc . createAnswer ( function (answerSDP) { pc . setLocalDescription ( new RTCSessionDescription (answerSDP) , function () { console . log ( "Set local description" ) ; }, function () { console . log ( "Failed to set up local description" ) ; } ) ; }, function (err) { console . log ( "Could not build the answer" ) ; }, constraints) ; }, function () { console . log ( "Failed to set up remote description" ) ; } ) ; } }; // end of 'onMediaSuccess()'

First part is common, we already saw it: we have the user’s webcam stream available, we created our RTCPeerConnection , we added our stream to it (ah?! now that will trigger the onaddstream() event in our caller, I’ll come back to this in a minute) and we registered our callback for the ICE Agent. Now what happens in that else case?

Well now that we added our stream to the RTCPeerConnection , we can set our remote description.

Just to be sure everybody follows: we are on the callee’s side now, so the description we just received from the caller (which was his local description) is for us, the remote description. Similarly, our local description will become his remote description when we will have sent it, right?

Okay now that it is clarified, let’s move on. setLocalDescription() , as we saw it takes callbacks and constraints. In our success callback, we can do something, now that we are on the callee’s side. It is time we create our SDP answer, done with pc.createAnswer() . Exactly like seen previously, if you inspect the object passed to the success callback, you will find the “type” field set to “answer” and an almost empty “sdp” field.

Inside that callback (yes, it’s Inception here: we are inside setRemoteDescription() ’s sucess callback, we called createAnswer() and we are now inside its own callback, and now there will be a third success callback…

With our answer successfully created, we need to set our own local description. Phew!

What now? Before continuing with the callee, let’s not forget that since we called pc.addStream() , the onaddstream() event will be triggered in our caller, let’s take a quick look:

function onStreamAdded (evt) { console . log ( "Remote stream received" ) ; document . getElementById ( "remoteVideo" ). src = URL . createObjectURL ( evt . stream ) ; }; // end of 'onStreamAdded()'

This event is common to both the caller and the callee, so it is quite simple: we simply take our media stream, transform it into something that can be plugged into a HTML <video> src attribute and it’s done. (Don’t forget to call play() if you omitted the autoplay attribute).

Oh and by the way, on the callee’s side, the onaddstream() event was fired as soon as we called setRemoteDescription() : this is what actually connected our RTCPeerConnection with the caller’s.

Okay, back to the callee. Since we called setLocalDescription() , the callee’s ICE Agent started to work, let’s take a look at the callee’s part of the ice callback:

function onIceCandidate (evt) { // Wait for all candidates to be gathered, and send our offer to our peer if ( evt . target . iceGatheringState === "complete" ) { console . log ( "ICE Gathering complete, sending SDP to peer." ) ; // Haven't found a way to use one-line condition to substitute "offer" and "answer" if (isCaller) { // already covered in the caller part } else { var answerToSend = JSON . stringify ( { "from" : nickname , "answer" : pc . localDescription } ) ; socket . send ( JSON . stringify ( { "target" : peer , "sdp" : answerToSend } )) ; console . log ( "Sent our answer" ) ; // Once we sent our answer, our part is finished and we can log out from the signalling server socket . close () ; } } };

This should comme as no surprise: this is exactly the same code than we used for the caller, except that we now send a JSON object with the name “answer”.

If you followed precisely when I was talking about the generated SDP that contained a “type” field whose value was either “offer” or “answer”, you would note that we could simplify this code by creating only one JSON object, and call the field “data” or “sdpData” rather than “offer” and “answer”. Then, in the onmessage() handler, the test would not be performed directly on dat.offer / dat.answer but rather dat.sdpData.type .

Well done if you picked this. Oh and don’t think “Yeah I would’ve picked it”, either you did, or you did not.

Last but not least, once the callee sent his answer, there is nothing else that will be transmitted through the signalling server, so we just disconnect from it. From now on (well after a small remaining step on the caller’s side), everything will be transmitted peer-to-peer, with WebRTC (so, securely).

Now it’s time to go see that onmessage() handler in the caller, because we just received an answer!

// Process incomming messages from the server, can be the user list or messages from another peer socket . onmessage = function (msg) { // Parse message, JSON is used for all message transfer try { var dat = JSON . parse ( msg . data ) ; } catch (e) { // error is not JSON-formatted } // Process userlist : display the names in the contact list if ( dat . userlist ) { // we already saw the case for when we receive the user list } // If the message is from a peer else if ( dat . from ) { // When we receive the first message form a peer, we consider ourselved in a call if ( ! isInCall) { // not relevant here } if ( dat . offer ) { // this is the callee } else if ( dat . answer ) { pc . setRemoteDescription ( new RTCSessionDescription ( dat . answer ) , function () { console . log ( "Set remote description - handshake complete." ) ; // As we are now in a call, log out from the signalling server socket . close () ; }, function () { console . log ( "Failed to set remote description - handshake failed." ) ; } ) ; } } // Otherwise, this is an error else { // error if not one of the two cases that we handle } }; // end of 'socket.onmessage()'

It should also come as no surprise: we receive an offer from our callee, it contains the SDP that we need to use in setRemoteDescription() . Once this is done, the WebRTC handshake is officially completed!

Same as the callee: we have no business anymore with the signalling server, so we might as well close it.