WebRTC Demo: JavaScript

This section describes the JavaScript part of the WebRTC demo. The JavaScript file is named webrtc.js and is referenced by the index.htm web page. By convention, JavaScript programs have a .js filename extension, although they will still work with other extensions. The program uses event driven programming. This is a style of programming where one event, such as the click of the mouse, triggers another, which then triggers another, and so on until the whole task is complete. Here, the task is to connect two users via WebRTC so that each sees the webcam video and sound of the other, as well as their own. I refer to the two participants in the conversation as the caller (the one who initiates the call) and the responder (the one who answers it).

The program proceeds through the following steps, each triggering the next:

  1. The caller and responder get the local video stream from their webcams and display the footage in the top-left of the window.
  2. The caller creates an offer of a call which causes the creation of a Session Description Protocol (a description of their video feed), known as their local sdp. Meanwhile, ice candidates (possible network routes to the caller) are gathered.
  3. The caller stores their local sdp (which includes the gathered ice candidates) in the database for the responder to retrieve.
  4. The responder retrieves and processes the caller's sdp (known as the remote sdp).
  5. Once the remote sdp has been successfully set, the responder creates an answer to the caller's offer, storing the generated local sdp in the database for retrieval by the caller.
  6. The caller retrieves and processes the responder's sdp.

The connection should now be established.

Read through the program listing, pressing any line you don't understand to display an explanation of what it does. You will see many hypertext links in the explanations. The best way to open these links is to click them whilst holding down the Ctrl key, so they open in a new tab and don't disrupt your main reading flow.

JavaScript listing

[toggle comments]

var pc;

var is short for variable, which is a token or string of characters that represents other things like numbers, in the same way that in algebra you use x and y and other letters in equations to stand for a number. It is called a variable because its value can vary. This line declares the names of the global variables, which are variables that can be used anywhere in the program. The line ends in a semicolon (;). Every complete statement (instruction) in JavaScript must end in a semicolon(;) so that the browser knows where the statement ends and the next one begins.

var callNo = 0; // 0 indicates callNo not initialised yet

The callNo variable is given an initial value of 0 using the equals operator. The double forward slashes (//) mark the beginning of a comment, an explanation to help readers understand the program. A comment lasts to the end of a line.

var caller = (location.search == '');

Every WebRTC call has the person who makes the call (the caller who makes the offer of a call) and the one who responds with an answer (the responder). location.search is the query portion (?n=) of the url in the address box at the top of the browser. For the responder of the call, who has received the url from the caller, this will contain the call number, but for the caller, there will be no query portion so location.search will be an empty string. The condition in brackets is a comparison, which compares one value with another. With comparisons, the equals operator is a double equals sign (location.search == ''), whereas with an assignment, where a variable is made equal to a value, the operator is a single equals sign (caller =). The result of a comparison is either true or false. The caller variable will be given a value of true if this is the caller, or false if this is the responder. The caller variable is boolean; it can have one of only two values, either true or false.


if (caller) {

An if statement executes a particular instruction or block of instructions if what appears in brackets (the condition) is true. The condition must always appear in brackets.

  document.getElementById('remoteVideo').style.display = 'none';

getElementById is a method of the document object. An object in computing is much like an object in the real world. It has properties, which could be things like size or color, and methods, which are ways you can get the object to perform particular tasks. The document object is the web page itself. To refer to a property or method of an object in JavaScript, we use the dot notation, so: object.property or object.method. We use thegetElementById method to refer to the element object whose id is remoteVideo. 'remoteVideo' is enclosed in apostrophes because the getElementById function accepts a string argument. You can also use double-quote marks (") for strings instead. The .style returns the style object for the element. We make the display property of the style object equal to 'none'. This hides the element. Notice how we drilled down through a whole series of object, methods and properties using the dot notation, starting with the document object and finally reaching the display property.

  document.getElementById('container').style.display = 'block';

Reveals the container div that was hidden by the css.

  }

Ends the if block.


navigator.getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;

The navigator object refers to the web browser. Different types of browser can have different names for the same thing. This statement makes the name of the getUserMedia method the same no matter the browser, by making it equal to whichever name is recognized by the browser. It tries each alternative in turn, separated by an or logical operator (||), which returns only the one that is true.

navigator.getUserMedia({audio: true, video: true}, onUserMediaSuccess, onUserMediaError);

This statement calls (starts, runs or executes) the getUserMedia method of the navigator object. The method starts getting audio and video from the user's microphone and webcam, after asking for the user's permission via a dialog. It is passed three arguments given in brackets and separated by commas. Arguments are data that are passed to a method. The first argument here tells the method that we want audio and video. It is enclosed in curly braces {} because it is an object (called MediaStreamConstraints), two of whose properties are audio and video, the values of which we set to true. The second and third arguments give the names of the functions to call if the method is successful (onUserMediaSuccess) or if it fails (onUserMediaError). Such functions are called callback functions. Functions are like methods, but are not tied to objects.


function onUserMediaSuccess(stream) {

A function is a block of code designed to perform a task and/or calculate a value. This function, named onUserMediaSuccess, is called when the getUserMedia function above completes successfully. You will see many functions in this JavaScript program. They are easy to spot as they begin with the word function. A callback function like this is called an event handler, because it runs when a particular event occurs. By convention, the name of an event handler begins with on. This function has one argument passed to it by the getUserMedia function. The argument is passed to a local variable of the function, which is called a parameter. Local variables, unlike global variables, cease to exist once the function has run. In this case the stream parameter will contain the video stream (flowing data) from the webcam. The curly brackets at the end of the line starts the block of statements or code for the function. Lots of short functions are the key to good programming, as they divide a program into easily maintainable, testable, and readable chunks, each performing a specific task.

  document.getElementById('localVideo').src = URL.createObjectURL(stream);

Connects the video stream from the local webcam to the localVideo element in the html page, so the user can see themselves. The URL.createObjectURL method creates a URL object from the stream.

  var PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;

Using the same technique as with the getUserMedia function above, this assigns to the PeerConnection local variable whichever name for the peer connection object is used by the browser. Any variable declared with the var keyword inside a function is a local variable. So PeerConnection will only be available within the onUserMediaSuccess function. Local variables are faster and conserve memory, as they are deleted when the function ends.

  pc = new PeerConnection({iceServers: [{url: "stun:stun.l.google.com:19302"}, {url:"stun:stun.services.mozilla.com"}]});

When you see the new keyword, you know this is a constructor, something that creates a new object. The above line assigns to the global variable named pc a new PeerConnection object, which does much of the work in connecting the webcams of two users who want to communicate. The argument in brackets is a JSON object, which is contained in curly brackets. JSON stands for JavaScript Object Notation, a human-readable way of communicating complex data as attribute-value pairs. In this case iceServers is the attribute and what follows in the square brackets is its value, which itself comprises two separate attribute-value pairs, each contained in curly brackets, and each giving the attribute url a value. What is this data? It is the urls of two ICE (Internet Connectivity Establishment) servers, used to find out how to navigate a network to reach the device on which this WebRTC demo is running.

  pc.addStream(stream);

Adds the video stream from the local webcam to the peer connection.

  pc.onicecandidate = function (evt) {

Allocates to the onicecandidate property of our pc object a callback function, to be executed every time the browser finds an ICE (Interactive Connectivity Establishment) candidate. This is a possible network route for your conversation partner to reach your browser, even if it is hidden behind a firewall or a NAT (Network Address Translation) router which disguises your internet address. Here the callback function is anonymous (unnamed) and created within the same statement. It has one parameter, given the name evt (for event), to which the ice candidate data is passed. There will normally be several ice candidates for each participant in the WebRTC conversation.

    if (!evt.candidate) {

Determine if the candidate property of the peer connection object actually has a value we can store. If it is empty, it will return false. The exclamation mark (!) is the not operator, which returns true if what follows it is false. So the if block will be run if evt.candidate is false.

      pc.onicecandidate = null;

Removes this function from the onicecandidate property, so no more ice gathering is attempted.

      if (pc.localDescription) postLocalData();

The other piece of data we need to store in the database is the pc.localDescription, which we will examine later. If this has been collected, then we can call the postLocalData() function to store in the database all local data about this peer connection so that it can be retrieved by the peer or conversation partner. The brackets after the function name are empty because it has no arguments.

      }

End of the if block.

      };

End of the anonymous function block assigned to the pc.onicecandidate property.

  pc.onaddstream = function(evt) {

The onaddstream property of the PeerConnection object (pc) contains the code to run when the onaddstream event is fired (triggered). This happens when the remote peer uses setRemoteDescription to add our local SDP (Session Description Protocol). This is a description of our video stream, including format, resolution and size, for the remote peer to be better able to connect. The peer will send their video stream as an argument to this function, which we assign to the parameter called evt.

    document.getElementById('remoteVideo').src = URL.createObjectURL(evt.stream);
    };

Embeds the stream (evt.stream) from the remote webcam into the remoteVideo element, via its src attribute. This accepts the URL of the video to embed, so we use the URL.createObjectURL method to convert the stream to a URL. This is the technique that can be used with the latest versions of Chrome, Firefox and Opera without resorting to polyfilling (using extra code to fill the gaps in or the differences between the capabilities of browsers).

  if (caller)

If this is the caller: same as if (caller == true).

    pc.createOffer(onDescCreated, onCreateOfferError);

If is the caller, we invoke the createOffer method, passing it the names of two callback functions as arguments. The first, onDescCreated, is called if the method completes successfully and creates an SDP object. The second argument, onCreateOfferError, is called if the method fails.

  else {

If this is not the caller; that is, if it is the responder.

    callNo = location.search.substr(location.search.indexOf('=') + 1);

location.search gives us the parameters portion of the url in the address box of the browser; the question mark (?) and everything that follows it. In our case, this will be '?callNo=' followed by the call number. To extract this number, we use the indexOf method on the location.search string to get the position of the equals sign. We increment (add one to) this position and use this as an argument to the substr method, applying it again to the location.search object to return all characters after the equals sign. We store the resulting number in the callNo variable.

    getRemoteData();
    }

Call the getRemoteData() function to retrieve data about the caller who made the offer of a call, given this callNo.

  window.onbeforeunload = function() {

The onbeforeunload method of the browser window object is fired just before the web page is closed. We allocate to it an anonymous function.

    stream.stop();

Stops the local video stream, so the webcam is freed up for use by other applications.

    if (caller && callNo > 0) navigator.sendBeacon('deleteCall.php', callNo);

If this is the caller and they have received a callNo, then call the deleteCall.php program to delete this callNo record from the database, for good housekeeping. The sendBeacon method of the navigator object sends data (in this case, the callNo) to the specified program on the server.

    };

End of the anonymous function.

  }

A closing curly bracket marks the end of this function.


function onUserMediaError(err) {

This function is called if there is a problem in trying to access the webcam. The function is passed the error message as a parameter called msg, which we convert to a string and display in the console log.

  console.log('User media error: ' + err.name);
  }

The log method of the browser's console object adds a message to the log book of the browser. Reading the log will give you a better idea of the steps involved in the webrtc communication process. The err argument returned to this function has a property called name which describes the error. A plus sign (+) is used to add the two strings ('User media error: ' and the error message) together to form a meaningful message.


function onCreateOfferError(err) {
  logError('Error creating offer: ' + err.name);
  }

Called if there was an error when creating an offer to communicate.


function onDescCreated(desc) {

This callback function is called on successful completion of the createOffer and createAnswer methods of the peer connection (pc) object. The function is passed an RTCSessionDescription object which we assign to the parameter desc. This object has two attributes: the SDP itself and the type of SDP (either Offer or Answer).

  pc.setLocalDescription(desc, onLocalDescSuccess, onLocalDescError);

This sets the local sdp for this browser to the value of desc and then executes the callback function specified in the second parameter if successful, or the function in the third parameter if there was a problem.


function onLocalDescSuccess() {
  console.log('Local sdp created: ' + JSON.stringify(pc.localDescription));
  if (pc.iceGatheringState == 'complete') postLocalData();
  }

If pc.iceGatheringState has the value 'complete', then all ice candidates have been gathered and we can proceed to store the local ice candidates and sdp in the database. Notice that an if statement can be all on one line. The JSON.stringify method converts the pc.localDescription object to a human-readable string.


function onLocalDescError(err) {
  logError("Local description could not be created: " + err.name);
  }

Error callback for the setLocalDescription method.


function postLocalData() {

This function posts data about the local browser (sdp) and network (ice candidates) to the server, for storing in the database so it can be retrieved by the peer (other person) in the webrtc call.

  console.log('Storing local sdp');
  var xhr = new XMLHttpRequest();

Constructs a new XMLHttpRequest object, used to send a request to the server. This object constructor has no arguments, hence the empty brackets (). When a variable, xhr in this case, is assigned to an object constructor, it becomes an instance of the object, inheriting all properties and methods of the object class.

  xhr.open('POST', 'storeSdp.php');

Calls the open method of the xhr object, passing two string arguments separated by a comma. The open method sets up the request parameters, but does not actually send the request. Its first argument is the HTTP method to use. HTTP (HyperText Transfer Protocol) is the way data is communicated over the world wide web. The 'POST' method we use here is for submitting or updating data, rather than requesting it (for which we would use the 'GET' method). The second argument is the url to which to send the request. Here we specify storeSdpIce.php, which is the name of a program that stores the data and, for the caller, returns a random call number to identify the call to both peers. This program is written in a language called PHP (covered in the next section) and run on a server.

  xhr.onload = (caller ? receiveCallNo : waitForConnection);

The onload property specifies the name of the callback function to be executed when the request has completed successfully. The part of the line after the equals sign is a ternary operator. If caller is true, it evaluates to receiveCallNo, if false, to waitForConnection. Note that we do not put () after the function names as we are not calling the functions, just stating their names.

  xhr.send(callNo + '~' + pc.localDescription.sdp);
  }

Now that everything has been set up we can actually send the request, which is executed asynchronously, meaning it can run in parallel with other code, so doesn't hold up the rest of the program while it's running. We send all the local data as a single string comprising the callNo followed by '~' (tilde) as a separator character followed by the local sdp string.


function receiveCallNo() {

This function is called when the xhr request sent by the caller in the postLocalData() function above has completed successfully.

  if (callNo = this.response) {

Note that this is not a comparison but an assignment; it uses a single, not a double, equals sign. The this keyword is very useful. Here it refers to the object that sent us here, which is the XMLHttpRequest object that sent the request (xhr in the postLocalData function). The response property, as you might imagine, contains the response of (data returned by) the PHP program. The callNo variable is assigned the value of this.response. The if keyword applied to this assignment will return true if this.response is not empty, in which case there was no database error and the PHP program completed successfully. Although this line does not adhere to best practice as it could be confusing, it demonstrates how two operations can be accomplished in one statement.

    console.log("Received call number");
    var link = location.href + '?callNo=' + callNo;

Creates the url of a link for a potential recipient to click to respond to a call.

    document.getElementById('test').href = link;

Changes the href property of the 'test' link element, to allow the user to test the application on one device.

    document.getElementById('email').href = "mailto:?subject=Video chat&body=Please click the link below to connect with me.%0D" + link;

Changes the href property of the 'email' link element, which the user can click to send an email containing the link to the call. The mailto command in the href opens a new email message having the specified subject and body text, in which we include a newline character (%0D) and the call link itself.

    document.getElementById('test').onclick = getRemoteData;
    document.getElementById('email').onclick = getRemoteData;
    }

Associate the onclick event of both links with the getRemoteData function, so when either is clicked the application starts checking the database for the sdp of the respondent.

  else
    logError('Error connecting to database');
  }

If the response from the PHP program was empty, then there was a problem connecting to the database, so log this error and abort the call.


function getRemoteData() {
  console.log("Checking db for remote sdp");
  var xhr = new XMLHttpRequest();

This function is called from different places depending on whether this is the caller or the responder. For the caller, it is called by the receiveCallNo function. For the responder, it is called by the onUserMediaSuccess function.

  xhr.open('GET', 'fetchSdp.php?callNo=' + (caller ? '' : '-') + callNo);

Calls the fetchSdp.php program which retrieves from the database the sdp of the peer in the call. As this is a 'GET' request, used when retrieving rather than storing data, we append the parameters to the url as a query string (?callNo=...), rather than specifying it as an parameter of the send method as we did previously with the 'POST' request. Using the ternary operator, the callNo appended with a negative value if the caller sdp is required.

  xhr.onload = processRemoteData;
  xhr.send();
  }

Process the remote sdp when it is returned by the php script.


function processRemoteData() {

Callback function for XMLHttpRequest call to fetchSdpIce.php, extracting the sdp and ice candidates from the results.

  if (this.response) {

The response should be a string containing the peer's sdp. Only perform the following block if there is data to process.

    var SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;

Standardise the SessionDescription constructor (for creating a new session description object) across all browsers using a similar technique to that used for the PeerConnection in the onUserMediaSuccess function above.

    var sdp = new SessionDescription();

Create a new sdp object.

    sdp.type = (caller ? 'answer' : 'offer');

Make the type property 'answer' if this is the caller, or 'offer' if the responder, as this is the type of sdp being processed.

    sdp.sdp = this.response;

Assign the actual sdp string retrieved from the database.

    console.log('Processing remote sdp: ' + sdp);
    pc.setRemoteDescription(sdp, onRemoteDescSuccess, onRemoteDescError);

We use the setRemoteDescription method to set the sdp received from the peer.

    }
  else
    setTimeout(getRemoteData, 1000);
  }

If the response is empty, the peer has yet to store their local data, so use the JavaScript setTimeout function to check the database again in one second (1000ms).


function onRemoteDescSuccess() {
  console.log("Remote sdp successfully set");

Callback for the setRemoteDescription method above.

  if (caller) {
    waitForConnection();
    document.getElementsByTagName('div')[0].style.display = 'none';
    document.getElementById('remoteVideo').style.display = 'block';
    }

The caller's work has finished, so they just need to call the waitForConnection() function, hide the options (circular links) and display the remote video.

  else
    pc.createAnswer(onDescCreated, onCreateAnswerError);
  }

The responder should create an answer to the caller's offer. This is similar to the createOffer method used by the caller, sharing the onDescCreated callback function that creates a local sdp, this time for the responder.


function onRemoteDescError(err) {
  logError("Remote description could not be set: " + err.name);
  }

Log an error if the setRemoteDescription method failed


function onCreateAnswerError(err) {
  logError("Error creating answer: " + err.name);
  }

Error callback for the createAnswer method


function waitForConnection() {

This function checks to see if the connection is complete.

  if (pc.iceConnectionState == 'connected' || pc.iceConnectionState == 'completed') {

The iceConnectionState property of the peer connection object has a value of 'connected' or 'completed' when the webRTC connection is complete; the caller and responder are connected.

    logError('Connection complete');

Log that the connection is complete. Although it's not an error, we use the logError function as it cleans up the database, removing data no longer needed.

    caller = null;

Prevent the logError function from clearing the database when it's already been done: see below. Null is a special type of value used to represent an 'empty' variable.

    document.getElementById('remoteVideo').ondblclick = function() {

Allow the user to double click the remote video to display it full screen.

      this.requestFullscreen = this.mozRequestFullScreen || this.webkitRequestFullscreen;
      this.requestFullscreen();
      }
    }

Uses a cross-browser polyfill to request that the remote video (this) is displayed full screen.

  else
    setTimeout(waitForConnection, 1000);
  }

If the peers are not yet connected, call the function again in one second to test again.


function logError(msg) {

This function is called by many other functions if a video connection cannot be established or the user closes the browser window. It displays an error message in the log and runs a php program to remove any database entries added during the failed attempt.

  console.log(msg);
  if (caller && callNo > 0) navigator.sendBeacon('deleteCall.php', callNo);
  }

This is identical to the line that appeared for the window.onbeforeunload event earlier in the program.