Building the Browser RTC Trapezoid: A Local Perspective - Real-Time Communication with WebRTC (2014)

Real-Time Communication with WebRTC (2014)

Chapter 3. Building the Browser RTC Trapezoid: A Local Perspective

In the previous chapter, we started to delve into the details of the Media Capture and Streams API by covering the first three steps of what we called a 10-step web real-time communications recipe. In particular, we discussed a couple of examples showing how we can access and manage local media streams by using the getUserMedia() method. The time is now ripe to start taking a look at the communication part.

In this chapter we will analyze the WebRTC 1.0 API, whose main purpose is to allow media to be sent to and received from another browser.

As we already anticipated in previous chapters, a mechanism is needed to properly coordinate the real-time communication, as well as to let peers exchange control messages. Such a mechanism, universally known as signaling, has not been defined inside WebRTC and thus does not belong in the RTCPeerConnection API specification.

The choice to make such an API agnostic with respect to signaling was made at the outset. Signaling is not standardized in WebRTC because the interoperability between browsers is ensured by the web server, using downloaded JavaScript code. This means that WebRTC developers can implement the signaling channel by relying on their favorite messaging protocol (SIP, XMPP, Jingle, etc.), or they can design a proprietary signaling mechanism that might only provide the features needed by the application.

The one and only architectural requirement with respect to this part of a WebRTC application concerns the availability of a properly configured bidirectional communication channel between the web browser and the web server. XMLHttpRequest (XHR), WebSocket, and solutions like Google’s Channel API represent good candidates for this.

The signaling channel is needed to allow the exchange of three types of information between WebRTC peers:

Media session management

Setting up and tearing down the communication, as well as reporting potential error conditions

Nodes’ network configuration

Network addresses and ports available for the exchanging of real-time data, even in the presence of NATs

Nodes’ multimedia capabilities

Supported media, available encoders/decoders (codecs), supported resolutions and frame rates, etc.

No data can be transferred between WebRTC peers before all of the above information has been properly exchanged and negotiated.

In this chapter, we will disregard all of the above mentioned issues related to the setup (and use) of a signaling channel and just focus on the description of the RTCPeerConnection API. We will achieve this goal by somehow emulating peer-to-peer behavior on a single machine. This means that we will for the time being bypass the signaling channel setup phase and let the three steps mentioned above (session management, network configuration, and multimedia capabilities exchange) happen on a single machine. In Chapter 5 we will eventually add the last brick to the WebRTC building, by showing how the local scenario can become a distributed one thanks to the introduction of a real signaling channel between two WebRTC-enabled peers.

Coming back to the API, calling new RTCPeerConnection (configuration) creates an RTCPeerConnection object, which is an abstraction for a communication channel between two users/browsers and can be either input or output for a particular MediaStream, as illustrated in Figure 3-1. The configuration parameter contains information to find access to the STUN and TURN servers, necessary for the NAT traversal setup phase.

Peer Connections and Media Streams

Figure 3-1. Adding a MediaStream to a PeerConnection

Using PeerConnection Objects Locally: An Example

Let’s now start with the simple HTML code shown in Example 3-1.

Example 3-1. Local RTCPeerConnection usage example

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

"http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<title>Local PeerConnection() example</title>

</head>

<body>

<table border="1" width="100%">

<tr>

<th>

Local video

</th>

<th>

'Remote' video

</th>

</tr>

<tr>

<td>

<video id="localVideo" autoplay></video>

</td>

<td>

<video id="remoteVideo" autoplay></video>

</td>

</tr>

<tr>

<td class="center">

<div>

<button id="startButton">Start</button>

<button id="callButton">Call</button>

<button id="hangupButton">Hang Up</button>

</div>

</td>

<td>

<!-- void -->

</td>

</tr>

</table>

<script src="js/localPeerConnection.js"></script>

</body>

</html>

Example 3-1 acts as a container for two video streams, represented side by side in a table format. The stream on the left represents a local capture, whereas the one on the right mimics a remote party (which will actually be a further capture of the local audio and video devices). Media capture and rendering is triggered by events associated with three buttons, which function, respectively, to start the application, to place a call between the local and the (fake) remote party, and to hang up the call. The core of this application is, as usual, the JavaScript code contained in the filelocalPeerConnection.js, which is reported in the following:

// JavaScript variables holding stream and connection information

var localStream, localPeerConnection, remotePeerConnection;

// JavaScript variables associated with HTML5 video elements in the page

var localVideo = document.getElementById("localVideo");

var remoteVideo = document.getElementById("remoteVideo");

// JavaScript variables assciated with call management buttons in the page

var startButton = document.getElementById("startButton");

var callButton = document.getElementById("callButton");

var hangupButton = document.getElementById("hangupButton");

// Just allow the user to click on the Call button at start-up

startButton.disabled = false;

callButton.disabled = true;

hangupButton.disabled = true;

// Associate JavaScript handlers with click events on the buttons

startButton.onclick = start;

callButton.onclick = call;

hangupButton.onclick = hangup;

// Utility function for logging information to the JavaScript console

function log(text) {

console.log("At time: " + (performance.now() / 1000).toFixed(3) + " --> " \

+ text);

}

// Callback in case of success of the getUserMedia() call

function successCallback(stream){

log("Received local stream");

// Associate the local video element with the retrieved stream

if (window.URL) {

localVideo.src = URL.createObjectURL(stream);

} else {

localVideo.src = stream;

}

localStream = stream;

// We can now enable the Call button

callButton.disabled = false;

}

// Function associated with clicking on the Start button

// This is the event triggering all other actions

function start() {

log("Requesting local stream");

// First of all, disable the Start button on the page

startButton.disabled = true;

// Get ready to deal with different browser vendors...

navigator.getUserMedia = navigator.getUserMedia ||

navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

// Now, call getUserMedia()

navigator.getUserMedia({audio:true, video:true}, successCallback,

function(error) {

log("navigator.getUserMedia error: ", error);

});

}

// Function associated with clicking on the Call button

// This is enabled upon successful completion of the Start button handler

function call() {

// First of all, disable the Call button on the page...

callButton.disabled = true;

// ...and enable the Hangup button

hangupButton.disabled = false;

log("Starting call");

// Note that getVideoTracks() and getAudioTracks() are not currently

// supported in Firefox...

// ...just use them with Chrome

if (navigator.webkitGetUserMedia) {

// Log info about video and audio device in use

if (localStream.getVideoTracks().length > 0) {

log('Using video device: ' + localStream.getVideoTracks()[0].label);

}

if (localStream.getAudioTracks().length > 0) {

log('Using audio device: ' + localStream.getAudioTracks()[0].label);

}

}

// Chrome

if (navigator.webkitGetUserMedia) {

RTCPeerConnection = webkitRTCPeerConnection;

// Firefox

} elseif(navigator.mozGetUserMedia){

RTCPeerConnection = mozRTCPeerConnection;

RTCSessionDescription = mozRTCSessionDescription;

RTCIceCandidate = mozRTCIceCandidate;

}

log("RTCPeerConnection object: " + RTCPeerConnection);

// This is an optional configuration string, associated with

// NAT traversal setup

var servers = null;

// Create the local PeerConnection object

localPeerConnection = new RTCPeerConnection(servers);

log("Created local peer connection object localPeerConnection");

// Add a handler associated with ICE protocol events

localPeerConnection.onicecandidate = gotLocalIceCandidate;

// Create the remote PeerConnection object

remotePeerConnection = new RTCPeerConnection(servers);

log("Created remote peer connection object remotePeerConnection");

// Add a handler associated with ICE protocol events...

remotePeerConnection.onicecandidate = gotRemoteIceCandidate;

// ...and a second handler to be activated as soon as the remote

// stream becomes available.

remotePeerConnection.onaddstream = gotRemoteStream;

// Add the local stream (as returned by getUserMedia())

// to the local PeerConnection.

localPeerConnection.addStream(localStream);

log("Added localStream to localPeerConnection");

// We're all set! Create an Offer to be 'sent' to the callee as soon

// as the local SDP is ready.

localPeerConnection.createOffer(gotLocalDescription, onSignalingError);

}

function onSignalingError(error){

console.log('Failed to create signaling message : ' + error.name);

}

// Handler to be called when the 'local' SDP becomes available

function gotLocalDescription(description){

// Add the local description to the local PeerConnection

localPeerConnection.setLocalDescription(description);

log("Offer from localPeerConnection: \n" + description.sdp);

// ...do the same with the 'pseudoremote' PeerConnection

// Note: this is the part that will have to be changed if you want

// the communicating peers to become remote

// (which calls for the setup of a proper signaling channel)

remotePeerConnection.setRemoteDescription(description);

// Create the Answer to the received Offer based on the 'local' description

remotePeerConnection.createAnswer(gotRemoteDescription, onSignalingError);

}

// Handler to be called when the remote SDP becomes available

function gotRemoteDescription(description){

// Set the remote description as the local description of the

// remote PeerConnection.

remotePeerConnection.setLocalDescription(description);

log("Answer from remotePeerConnection: \n" + description.sdp);

// Conversely, set the remote description as the remote description of the

// local PeerConnection

localPeerConnection.setRemoteDescription(description);

}

// Handler to be called when hanging up the call

function hangup() {

log("Ending call");

// Close PeerConnection(s)

localPeerConnection.close();

remotePeerConnection.close();

// Reset local variables

localPeerConnection = null;

remotePeerConnection = null;

// Disable Hangup button

hangupButton.disabled = true;

// Enable Call button to allow for new calls to be established

callButton.disabled = false;

}

// Handler to be called as soon as the remote stream becomes available

function gotRemoteStream(event){

// Associate the remote video element with the retrieved stream

if (window.URL) {

// Chrome

remoteVideo.src = window.URL.createObjectURL(event.stream);

} else {

// Firefox

remoteVideo.src = event.stream;

}

log("Received remote stream");

}

// Handler to be called whenever a new local ICE candidate becomes available

function gotLocalIceCandidate(event){

if (event.candidate) {

// Add candidate to the remote PeerConnection

remotePeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));

log("Local ICE candidate: \n" + event.candidate.candidate);

}

}

// Handler to be called whenever a new remote ICE candidate becomes available

function gotRemoteIceCandidate(event){

if (event.candidate) {

// Add candidate to the local PeerConnection

localPeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));

log("Remote ICE candidate: \n " + event.candidate.candidate);

}

}

In order to easily understand the contents of this code, let’s follow the evolution of our application step by step. We will show screen captures taken with both Chrome and Firefox, so you can appreciate the differences related to both the look and feel of the application and the developers’ tools made available by the two browsers.

Starting the Application

Here is what happens when the user clicks on the Start button in Chrome (Figure 3-2) and in Firefox (Figure 3-3).

The example page loaded in Chrome

Figure 3-2. The example page loaded in Chrome

The example page loaded in Firefox

Figure 3-3. The example page loaded in Firefox

As you can see from both figures, the browser is asking for the user’s consent to access local audio and video devices. As we know from the previous chapter, this is due to the execution of the getUserMedia() call, as indicated by the JavaScript snippet that follows:

// Function associated with clicking on the Start button

// This is the event triggering all other actions

function start() {

log("Requesting local stream");

// First of all, disable the Start button on the page

startButton.disabled = true;

// Get ready to deal with different browser vendors...

navigator.getUserMedia = navigator.getUserMedia ||

navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

// Now, call getUserMedia()

navigator.getUserMedia({audio:true, video:true}, successCallback,

function(error) {

log("navigator.getUserMedia error: ", error);

});

}

As soon as the user provides consent, the successCallback() function is triggered. Such a function simply attaches the local stream (containing both audio and video tracks) to the localVideo element in the HTML5 page:

...

// Associate the local video element with the retrieved stream

if (window.URL) {

localVideo.src = URL.createObjectURL(stream);

} else {

localVideo.src = stream;

}

localStream = stream;

...

The effect of the execution of the callback is shown in Figure 3-4 (Chrome) and Figure 3-5 (Firefox).

The example page after user’s consent

Figure 3-4. The example page after user grants consent, in Chrome

The example page after user’s consent

Figure 3-5. The example page after user grants consent, in Firefox

Placing a Call

Once consent has been granted, the Start button gets disabled and the Call button becomes in turn enabled. If the user clicks on it, the Call() function is triggered. Such a function first does some basic housekeeping like disabling the Call button and enabling the Hangup button. Then, in the case of Chrome and Opera (this feature is not currently implemented in Firefox), it logs some information about the available media tracks to the console:

// Function associated with clicking on the Call button

// This is enabled upon successful completion of the Start button handler

function call() {

// First of all, disable the Call button on the page...

callButton.disabled = true;

// ...and enable the Hangup button

hangupButton.disabled = false;

log("Starting call");

// Note that getVideoTracks() and getAudioTracks() are not currently

// supported in Firefox...

// ...just use them with Chrome

if (navigator.webkitGetUserMedia) {

// Log info about video and audio device in use

if (localStream.getVideoTracks().length > 0) {

log('Using video device: ' + localStream.getVideoTracks()[0].label);

}

if (localStream.getAudioTracks().length > 0) {

log('Using audio device: ' + localStream.getAudioTracks()[0].label);

}

}

...

NOTE

The getVideoTracks() and getAudioTracks() methods, defined by the MediaStream constructor in the Media Capture and Streams API, return a sequence of MediaStreamTrack objects representing, respectively, the video tracks and the audio tracks in the stream.

Once done with the preceding operations, we finally get into the core of the code, namely the part where we encounter the RTCPeerConnection object for the very first time:

...

// Chrome

if (navigator.webkitGetUserMedia) {

RTCPeerConnection = webkitRTCPeerConnection;

// Firefox

} elseif(navigator.mozGetUserMedia){

RTCPeerConnection = mozRTCPeerConnection;

RTCSessionDescription = mozRTCSessionDescription;

RTCIceCandidate = mozRTCIceCandidate;

}

log("RTCPeerConnection object: " + RTCPeerConnection);

...

The above snippet contains some JavaScript code that has a solitary goal of detecting the type of browser in use, in order to give the right name to the right object. You will notice from the code that the standard RTCPeerConnection object is currently prefixed both in Chrome (webkitRTCPeerConnection) and in Firefox (mozRTCPeerConnection). The latter browser, by the way, also has a nonstandard way of naming the related RTCSessionDescription and RTCIceCandidate objects associated, respectively, with the description of the session to be negotiated and the representation of ICE protocol candidate addresses (see Chapter 4).

Once the (right) RTCPeerConnection object has been identified, we can eventually instantiate it:

...

// This is an optional configuration string, associated with

// NAT traversal setup

var servers = null;

// Create the local PeerConnection object

localPeerConnection = new RTCPeerConnection(servers);

log("Created local peer connection object localPeerConnection");

// Add a handler associated with ICE protocol events

localPeerConnection.onicecandidate = gotLocalIceCandidate;

...

The above snippet shows that an RTCPeerConnection object is instantiated through a constructor taking an optional servers parameter as input. Such a parameter can be used to properly deal with NAT traversal issues, as will be explained in Chapter 4.

RTCPEERCONNECTION

Calling new RTCPeerConnection(configuration) creates an RTCPeerConnection object. The configuration has the information to find and access the STUN and TURN servers (there may be multiple servers of each type, with any TURN server also acting as a STUN server). Optionally, it also takes a MediaConstraints object Media Constraints.

When the RTCPeerConnection constructor is invoked, it also creates an ICE Agent responsible for the ICE state machine, controlled directly by the browser. The ICE Agent will proceed with gathering the candidate addresses when the IceTransports constraint is not set to “none.”

An RTCPeerConnection object has two associated stream sets. A local streams set, representing streams that are currently sent, and a remote streams set, representing streams that are currently received through this RTCPeerConnection object. The stream sets are initialized to empty sets when the RTCPeerConnection object is created.

The interesting thing to notice here is that the configuration of the newly created PeerConnection is done asynchronously, through the definition of proper callback methods.

NOTE

The onicecandidate handler is triggered whenever a new candidate is made available to the local peer by the ICE protocol machine inside the browser.

// Handler to be called whenever a new local ICE candidate becomes available

function gotLocalIceCandidate(event){

if (event.candidate) {

// Add candidate to the remote PeerConnection

remotePeerConnection.addIceCandidate(new RTCIceCandidate(event.candidate));

log("Local ICE candidate: \n" + event.candidate.candidate);

}

}

NOTE

The addIceCandidate() method provides a remote candidate to the ICE Agent. In addition to being added to the remote description, connectivity checks will be sent to the new candidates as long as the IceTransports constraint is not set to “none.”

The snippet takes for granted that the remote peer is actually run locally, which avoids the need for sending information about the gathered local address to the other party across a properly configured signaling channel. Here is why this application won’t work at all if you try and run it on two remote machines. In subsequent chapters we will discuss how we can create such a signaling channel and use it to transfer ICE-related (as well as session-related) information to the remote party. For the moment, we simply add the gathered local network reachability information to the (locally available) remote peer connection. Clearly, the same reasoning applies when switching roles between caller and callee, i.e., the remote candidates will simply be added to the local peer connection as soon as they become available:

...

// Create the remote PeerConnection object

remotePeerConnection = new RTCPeerConnection(servers);

log("Created remote peer connection object remotePeerConnection");

// Add a handler associated with ICE protocol events...

remotePeerConnection.onicecandidate = gotRemoteIceCandidate;

// ...and a second handler to be activated as soon as the remote

// stream becomes available

remotePeerConnection.onaddstream = gotRemoteStream;

...

NOTE

The onaddstream and onremovestream handlers are called any time a MediaStream is respectively added or removed by the remote peer. Both will be fired only as a result of the execution of the setRemoteDescription() method.

The preceding snippet is related to the onaddstream handler, whose implementation looks after attaching the remote stream (as soon as it becomes available) to the remoteVideo element of the HTML5 page, as reported in the following:

// Handler to be called as soon as the remote stream becomes available

function gotRemoteStream(event){

// Associate the remote video element with the retrieved stream

if (window.URL) {

// Chrome

remoteVideo.src = window.URL.createObjectURL(event.stream);

} else {

// Firefox

remoteVideo.src = event.stream;

}

log("Received remote stream");

}

Coming back to the Call() function, the only remaining actions concern adding the local stream to the local PeerConnection and eventually invoking the createOffer() method on it:

...

// Add the local stream (as returned by getUserMedia()

// to the local PeerConnection

localPeerConnection.addStream(localStream);

log("Added localStream to localPeerConnection");

// We're all set! Create an Offer to be 'sent' to the callee as soon as

// the local SDP is ready

localPeerConnection.createOffer(gotLocalDescription,onSignalingError);

}

function onSignalingError(error) {

console.log('Failed to create signaling message : ' + error.name);

}

NOTE

The addStream() and removeStream() methods add a stream to and remove a stream from an RTCPeerConnection object, respectively.

The createOffer() method plays a fundamental role, since it asks the browser to properly examine the internal state of the PeerConnection and generate an appropriate RTCSessionDescription object, thus initiating the Offer/Answer-state machine.

NOTE

The createOffer() method generates an SDP blob containing an RFC3264 offer with the supported configurations for the session: the descriptions of the local MediaStreams attached, the codec/RTP/RTCP options supported by the browser, and any candidates that have been gathered by the ICE Agent. The constraints parameter may be supplied to provide additional control over the offer generated.

The createOffer() method takes as input a callback (gotLocalDescription) to be called as soon as the session description is made available to the application. Also in this case, once the session description is available, the local peer should send it to the callee by using the signaling channel. For the moment, we will skip this phase and once more make the assumption that the remote party is actually a locally reachable one, which translates to the following actions:

// Handler to be called when the 'local' SDP becomes available

function gotLocalDescription(description){

// Add the local description to the local PeerConnection

localPeerConnection.setLocalDescription(description);

log("Offer from localPeerConnection: \n" + description.sdp);

// ...do the same with the 'pseudoremote' PeerConnection

// Note: this is the part that will have to be changed if

// you want the communicating peers to become remote

// (which calls for the setup of a proper signaling channel)

remotePeerConnection.setRemoteDescription(description);

// Create the Answer to the received Offer based on the 'local' description

remotePeerConnection.createAnswer(gotRemoteDescription,onSignalingError);

}

As stated in the commented snippet above, we herein directly set the retrieved session description as both the local description for the local peer and the remote description for the remote peer.

NOTE

The setLocalDescription() and setRemoteDescription() methods instruct the RTCPeerConnection to apply the supplied RTCSessionDescription as the local description and as the remote offer or answer, respectively.

Then, we ask the remote peer to answer the offered session by calling the createAnswer() method on the remote peer conection. Such a method takes as input parameter a callback (gotRemoteDescription) to be called as soon as the remote browser makes its own session description available to the remote peer. Such a handler actually mirrors the behavior of the companion callback on the caller’s side:

// Handler to be called when the remote SDP becomes available

function gotRemoteDescription(description){

// Set the remote description as the local description of the

// remote PeerConnection

remotePeerConnection.setLocalDescription(description);

log("Answer from remotePeerConnection: \n" + description.sdp);

// Conversely, set the remote description as the remote description

// of the local PeerConnection

localPeerConnection.setRemoteDescription(description);

}

NOTE

The createAnswer() method generates an SDP answer with the supported configuration for the session that is compatible with the parameters in the remote configuration.

The entire call flow described above can actually be tracked down on the browser’s console, as shown in Figure 3-6 (Chrome) and Figure 3-7 (Firefox).

Chrome’s console tracking down a call between two local peers

Figure 3-6. Chrome console tracking down a call between two local peers

Firefox console tracking down a call between two local peers

Figure 3-7. Firefox console tracking down a call between two local peers

The two snapshots show the sequence of events that have been logged by the application, as well as session description information in an SDP-compliant format. This last part of the log will become clearer when we briefly introduce the Session Description Protocol in Chapter 4.

When all of the above steps have completed, we finally see the two streams inside our browser’s window, as shown in Figure 3-8 (Chrome) and Figure 3-9 (Firefox).

Chrome showing local and remote media after a successful call

Figure 3-8. Chrome showing local and remote media after a successful call

Firefox showing local and remote media after a successful call

Figure 3-9. Firefox showing local and remote media after a successful call

Hanging Up

Once done with a call, the user can tear it down by clicking on the Hangup button. This triggers the execution of the associated handler:

// Handler to be called when hanging up the call

function hangup() {

log("Ending call");

// Close PeerConnection(s)

localPeerConnection.close();

remotePeerConnection.close();

// Reset local variables

localPeerConnection = null;

remotePeerConnection = null;

// Disable Hangup button

hangupButton.disabled = true;

// Enable Call button to allow for new calls to be established

callButton.disabled = false;

}

As we can see from a quick look at the code, the hangup() handler simply closes the instantiated peer connections and releases resources. It then disables the Hangup button and enables the Call button, thus rolling the settings back to the point we reached right after starting the application for the very first time (i.e., after the getUserMedia() call). We’re now in a state from which a new call can be placed and the game can be started all over again. This situation is depicted in Figure 3-10 (Chrome) and Figure 3-11 (Firefox).

NOTE

The close() method destroys the RTCPeerConnection ICE Agent, abruptly ending any active ICE processing and any active streams, and releasing any relevant resources.

Chrome after tearing down a call

Figure 3-10. Chrome after tearing down a call

Firefox after tearing down a call

Figure 3-11. Firefox after tearing down a call

Notice that the two frames in both windows are different, which illustrates the fact that, even though no peer connection is available anymore, we now have a live local stream and a frozen remote stream. This is also reported in the console log.

Adding a DataChannel to a Local PeerConnection

The Peer-to-Peer Data API lets a web application send and receive generic application data in a peer-to-peer fashion. The API for sending and receiving data draws inspiration from WebSocket.

In this section we will show how to add a DataChannel to a PeerConnection. Once again, we will stick to the local perspective and ignore signaling issues. Let’s get started with the HTML5 page in Example 3-2.

Example 3-2. Local DataChannel usage example

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"

"http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<title>DataChannel simple example</title>

</head>

<body>

<textarea rows="5" cols="50" id="dataChannelSend" disabled placeholder="

1: Press Start; 2: Enter text; 3: Press Send."></textarea>

<textarea rows="5" cols="50" id="dataChannelReceive" disabled></textarea>

<div id="buttons">

<button id="startButton">Start</button>

<button id="sendButton">Send</button>

<button id="closeButton">Stop</button>

</div>

<script src="js/dataChannel.js"></script>

</body>

</html>

The page (whose look and feel in Chrome is illustrated in Figure 3-12) simply contains two side-by-side text areas associated, respectively, with data to be sent from the sender’s data channel, and data received by the other party on the receiver’s data channel. Three buttons are used to orchestrate the application: (1) a Start button to be pressed upon startup; (2) a Send button to be used whenever new data has to be streamed across the data channel; and (3) a Close button useful for resetting the application and bringing it back to its original state.

The Data Channel example page loaded in Chrome

Figure 3-12. The DataChannel example page loaded in Chrome

As usual, the core behavior of this application is implemented in the embedded JavaScript file dataChannel.js, which is laid out in the following:

//JavaScript variables associated with send and receive channels

var sendChannel, receiveChannel;

//JavaScript variables associated with demo buttons

var startButton = document.getElementById("startButton");

var sendButton = document.getElementById("sendButton");

var closeButton = document.getElementById("closeButton");

//On startup, just the Start button must be enabled

startButton.disabled = false;

sendButton.disabled = true;

closeButton.disabled = true;

//Associate handlers with buttons

startButton.onclick = createConnection;

sendButton.onclick = sendData;

closeButton.onclick = closeDataChannels;

//Utility function for logging information to the JavaScript console

function log(text) {

console.log("At time: " + (performance.now() / 1000).toFixed(3) +

" --> " + text);

}

function createConnection() {

// Chrome

if (navigator.webkitGetUserMedia) {

RTCPeerConnection = webkitRTCPeerConnection;

// Firefox

} elseif(navigator.mozGetUserMedia){

RTCPeerConnection = mozRTCPeerConnection;

RTCSessionDescription = mozRTCSessionDescription;

RTCIceCandidate = mozRTCIceCandidate;

}

log("RTCPeerConnection object: " + RTCPeerConnection);

// This is an optional configuration string

// associated with NAT traversal setup

var servers = null;

// JavaScript variable associated with proper

// configuration of an RTCPeerConnection object:

// use DTLS/SRTP

var pc_constraints = {

'optional': [

{'DtlsSrtpKeyAgreement': true}

]};

// Create the local PeerConnection object...

// ...with data channels

localPeerConnection = new RTCPeerConnection(servers,pc_constraints);

log("Created local peer connection object, with Data Channel");

try {

// Note: SCTP-based reliable DataChannels supported

// in Chrome 29+ !

// use {reliable: false} if you have an older version of Chrome

sendChannel = localPeerConnection.createDataChannel( \

"sendDataChannel",{reliable: true});

log('Created reliable send data channel');

} catch (e) {

alert('Failed to create data channel!');

log('createDataChannel() failed with following message: ' \

+ e.message);

}

// Associate handlers with peer connection ICE events

localPeerConnection.onicecandidate = gotLocalCandidate;

// Associate handlers with data channel events

sendChannel.onopen = handleSendChannelStateChange;

sendChannel.onclose = handleSendChannelStateChange;

// Mimic a remote peer connection

window.remotePeerConnection = new RTCPeerConnection(servers, \

pc_constraints);

log('Created remote peer connection object, with DataChannel');

// Associate handlers with peer connection ICE events...

remotePeerConnection.onicecandidate = gotRemoteIceCandidate;

// ...and data channel creation event

remotePeerConnection.ondatachannel = gotReceiveChannel;

// We're all set! Let's start negotiating a session...

localPeerConnection.createOffer(gotLocalDescription,onSignalingError);

// Disable Start button and enable Close button

startButton.disabled = true;

closeButton.disabled = false;

}

function onSignalingError(error) {

console.log('Failed to create signaling message : ' + error.name);

}

// Handler for sending data to the remote peer

function sendData() {

var data = document.getElementById("dataChannelSend").value;

sendChannel.send(data);

log('Sent data: ' + data);

}

// Close button handler

function closeDataChannels() {

// Close channels...

log('Closing data channels');

sendChannel.close();

log('Closed data channel with label: ' + sendChannel.label);

receiveChannel.close();

log('Closed data channel with label: ' + receiveChannel.label);

// Close peer connections

localPeerConnection.close();

remotePeerConnection.close();

// Reset local variables

localPeerConnection = null;

remotePeerConnection = null;

log('Closed peer connections');

// Rollback to the initial setup of the HTML5 page

startButton.disabled = false;

sendButton.disabled = true;

closeButton.disabled = true;

dataChannelSend.value = "";

dataChannelReceive.value = "";

dataChannelSend.disabled = true;

dataChannelSend.placeholder = "1: Press Start; 2: Enter text; \

3: Press Send.";

}

// Handler to be called as soon as the local SDP is made available to

// the application

function gotLocalDescription(desc) {

// Set local SDP as the right (local/remote) description for both local

// and remote parties

localPeerConnection.setLocalDescription(desc);

log('localPeerConnection\'s SDP: \n' + desc.sdp);

remotePeerConnection.setRemoteDescription(desc);

// Create answer from the remote party, based on the local SDP

remotePeerConnection.createAnswer(gotRemoteDescription,onSignalingError);

}

// Handler to be called as soon as the remote SDP is made available to

// the application

function gotRemoteDescription(desc) {

// Set remote SDP as the right (remote/local) description for both local

// and remote parties

remotePeerConnection.setLocalDescription(desc);

log('Answer from remotePeerConnection\'s SDP: \n' + desc.sdp);

localPeerConnection.setRemoteDescription(desc);

}

// Handler to be called whenever a new local ICE candidate becomes available

function gotLocalCandidate(event) {

log('local ice callback');

if (event.candidate) {

remotePeerConnection.addIceCandidate(event.candidate);

log('Local ICE candidate: \n' + event.candidate.candidate);

}

}

// Handler to be called whenever a new remote ICE candidate becomes available

function gotRemoteIceCandidate(event) {

log('remote ice callback');

if (event.candidate) {

localPeerConnection.addIceCandidate(event.candidate);

log('Remote ICE candidate: \n ' + event.candidate.candidate);

}

}

// Handler associated with the management of remote peer connection's

// data channel events

function gotReceiveChannel(event) {

log('Receive Channel Callback: event --> ' + event);

// Retrieve channel information

receiveChannel = event.channel;

// Set handlers for the following events:

// (i) open; (ii) message; (iii) close

receiveChannel.onopen = handleReceiveChannelStateChange;

receiveChannel.onmessage = handleMessage;

receiveChannel.onclose = handleReceiveChannelStateChange;

}

// Message event handler

function handleMessage(event) {

log('Received message: ' + event.data);

// Show message in the HTML5 page

document.getElementById("dataChannelReceive").value = event.data;

// Clean 'Send' text area in the HTML page

document.getElementById("dataChannelSend").value = '';

}

// Handler for either 'open' or 'close' events on sender's data channel

function handleSendChannelStateChange() {

var readyState = sendChannel.readyState;

log('Send channel state is: ' + readyState);

if (readyState == "open") {

// Enable 'Send' text area and set focus on it

dataChannelSend.disabled = false;

dataChannelSend.focus();

dataChannelSend.placeholder = "";

// Enable both Send and Close buttons

sendButton.disabled = false;

closeButton.disabled = false;

} else { // event MUST be 'close', if we are here...

// Disable 'Send' text area

dataChannelSend.disabled = true;

// Disable both Send and Close buttons

sendButton.disabled = true;

closeButton.disabled = true;

}

}

// Handler for either 'open' or 'close' events on receiver's data channel

function handleReceiveChannelStateChange() {

var readyState = receiveChannel.readyState;

log('Receive channel state is: ' + readyState);

}

As we did with the previous example, we will analyze the behavior of the application by following its lifecycle step by step. We will skip all those parts that have already been explained. This allows us to focus just on the new functionality introduced in the code.

Starting Up the Application

When the user clicks on the Start button in the page, a number of events happen behind the scenes. Namely, the createConnection() handler is activated. Such a handler creates both the local and the (fake) remote peer connections, in much the same way as we saw with the previous example. The difference here is that this time, the peer connection is also equipped with a data channel for the streaming of generic data:

...

// JavaScript variable associated with proper

// configuration of an RTCPeerConnection object:

// use DTLS/SRTP

var pc_constraints = {

'optional': [

{'DtlsSrtpKeyAgreement': true}

]};

// Create the local PeerConnection object...

// ...with data channels

localPeerConnection = new RTCPeerConnection(servers,pc_constraints);

log("Created local peer connection object, with DataChannel");

try {

// Note: SCTP-based reliable data channels supported

// in Chrome 29+ !

// use {reliable: false} if you have an older version of Chrome

sendChannel = localPeerConnection.createDataChannel( \

"sendDataChannel", {reliable: true});

log('Created reliable send data channel');

} catch (e) {

alert('Failed to create data channel!');

log('createDataChannel() failed with following message: ' \

+ e.message);

}

...

The preceding snippet shows how to add a DataChannel to an existing PeerConnection by calling the createDataChannel() method. Note that this is a browser-specific feature, not a standardized constraint.

TIP

The WebRTC API does not define the use of constraints with the DataChannel API. It instead defines the usage of the so-called RTCDataChannelInit dictionary (Table 3-1).

The data channel itself is actually added to the newly instantiated peer connection by calling the createDataChannel("sendDataChannel", {reliable: true}); method on it. The code shows that such a data channel can be either unreliable or reliable. Reliability is guaranteed by the proper use of the SCTP protocol and is a feature that has been initially made available just in Firefox. Is has only recently been implemented in Chrome (since version 29 of the browser).

CREATEDATACHANNEL

The createDataChannel() method creates a new RTCDataChannel object with the given label. The RTCDataChannelInit dictionary (Table 3-1) can be used to configure properties of the underlying channel, such as data reliability.

The RTCDataChannel interface represents a bidirectional data channel between two peers. Each data channel has an associated underlying data transport that is used to transport data to the other peer. The properties of the underlying data transport are configured by the peer as the channel is created (Table 3-1). The properties of a channel cannot change after the channel has been created. The actual wire protocol between the peers is SCTP (see DataChannel).

An RTCDataChannel can be configured to operate in different reliability modes. A reliable channel ensures that data is delivered to the other peer through retransmissions. An unreliable channel is configured to either limit the number of retransmissions (maxRetransmits) or set a time during which retransmissions are allowed (maxRetransmitTime). These properties cannot be used simultaneously and an attempt to do so will result in an error. Not setting any of these properties results in the creation of a reliable channel.

Table 3-1. RTCDataChannelInit dictionary members

Member

Type

Description

id

unsigned short

Overrides the default selection of id for this channel.

maxRetransmits

unsigned short

Limits the number of times a channel will retransmit data if not successfully delivered.

maxRetransmitTime

unsigned short

Limits the time during which the channel will retransmit data if not successfully delivered.

negotiated

boolean

The default value of false tells the user agent to announce the channel in-band and instruct the other peer to dispatch a corresponding RTCDataChannel object.

ordered

boolean

If set to false, data are allowed to be delivered out of order. The default value of true guarantees that data will be delivered in order.

protocol

DOMString

Subprotocol name used for this channel.

Local data channel events (onopen and onclose) are dealt with through proper handlers, as illustrated in the following:

...

// Associate handlers with send data channel events

sendChannel.onopen = handleSendChannelStateChange;

sendChannel.onclose = handleSendChannelStateChange;

...

As to the remote data channel (ondatachannel), it also evolves through events and related callbacks:

...

remotePeerConnection.ondatachannel = gotReceiveChannel;

...

This callback is actually activated as soon as the pseudosignaling phase successfully completes. Such a phase is triggered by the call localPeerConnection.createOffer(gotLocalDescription,onSignalingError), which initiates the aforementioned call flow involving the gathering of ICE protocol candidates, as well as the exchanging of session descriptions.

The annotations on the JavaScript console log in Figures 3-13 and 3-14 show the first phases of the bootstrapping procedure, as it takes place in Chrome and in Firefox, respectively. We can see from the logs that the Offer/Answer phase starts right after the creation of the local and remote peer connections.

Starting the data channel application in Chrome

Figure 3-13. Starting the data channel application in Chrome

Starting the data channel application in Firefox

Figure 3-14. Starting the data channel application in Firefox

The answer, in particular, is prepared as soon as the local SDP is made available to the application, inside the gotLocalDescription() handler:

function gotLocalDescription(desc) {

// Set local SDP as the right (local/remote) description for both local

// and remote parties

localPeerConnection.setLocalDescription(desc);

log('localPeerConnection\'s SDP: \n' + desc.sdp);

remotePeerConnection.setRemoteDescription(desc);

// Create answer from the remote party, based on the local SDP

remotePeerConnection.createAnswer(gotRemoteDescription,onSignalingError);

}

Data channel state changes are dealt with, respectively, through the handleSendChannelStateChange() and handleReceiveChannelStateChange() event handlers. Upon reception of the open event, the former function prepares the HTML5 page for editing inside the sender’s text area, at the same time enabling both the Send and the Close buttons:

...

if (readyState == "open") {

// Enable 'Send' text area and set focus on it

dataChannelSend.disabled = false;

dataChannelSend.focus();

dataChannelSend.placeholder = "";

// Enable both Send and Close buttons

sendButton.disabled = false;

closeButton.disabled = false;

...

On the receiver’s side, the state change handler just logs information to the JavaScript console:

function handleReceiveChannelStateChange() {

var readyState = receiveChannel.readyState;

log('Receive channel state is: ' + readyState);

}

The snapshots in Figure 3-15 (Chrome) and Figure 3-16 (Firefox) show the application’s state at the end of the bootstrapping procedure.

The data channel application in Chrome

Figure 3-15. The data channel application in Chrome, after startup

The data channel application in Firefox

Figure 3-16. The data channel application in Firefox, after startup

Streaming Text Across the Data Channel

Once the data channel is ready, we can finally use it to transfer information between the sender and the receiver. Indeed, the user can edit a message inside the sender’s text area and then click on the Send button in order to stream such information across the already instantiated data channel, by using the sendData() handler:

function sendData() {

var data = document.getElementById("dataChannelSend").value;

sendChannel.send(data);

log('Sent data: ' + data);

}

NOTE

The send() method attempts to send data on the channel’s underlying data transport.

As soon as new data arrives at the receiver, the handleMessage() handler is called in turn. Such a handler first prints the received message inside the receiver’s text area and then resets the sender’s editing box:

function handleMessage(event) {

log('Received message: ' + event.data);

// Show message in the HTML5 page

document.getElementById("dataChannelReceive").value = event.data;

// Clean 'Send' text area in the HTML page

document.getElementById("dataChannelSend").value = '';

}

Figures 3-17 and 3-18 show the application’s state right before a message is transferred across the data channel in Chrome and in Firefox, respectively.

Similarly, Figure 3-19 (Chrome) and Figure 3-20 (Firefox) report message reception and associated actions in the HTML page.

Getting ready to stream a message across the data channel in Chrome

Figure 3-17. Getting ready to stream a message across the data channel in Chrome

Getting ready to stream a message across the data channel in Firefox

Figure 3-18. Getting ready to stream a message across the data channel in Firefox

Receiving a message from the data channel in Chrome

Figure 3-19. Receiving a message from the data channel in Chrome

Receiving a message from the data channel in Firefox

Figure 3-20. Receiving a message from the data channel in Firefox

Closing the Application

Once done with data transfers, the user can click on the Close button in order to:

§ Close the data channels:

function closeDataChannels() {

// Close channels...

log('Closing data channels');

sendChannel.close();

log('Closed data channel with label: ' + sendChannel.label);

receiveChannel.close();

log('Closed data channel with label: ' + receiveChannel.label);

...

NOTE

The close() method attempts to close the channel.

§ Close the peer connections:

...

// Close peer connections

localPeerConnection.close();

remotePeerConnection.close();

// Reset local variables

localPeerConnection = null;

remotePeerConnection = null;

log('Closed peer connections');

...

§ Reset the application:

...

// Rollback to the initial setup of the HTML5 page

startButton.disabled = false;

sendButton.disabled = true;

closeButton.disabled = true;

dataChannelSend.value = "";

dataChannelReceive.value = "";

dataChannelSend.disabled = true;

dataChannelSend.placeholder = "1: Press Start; 2: Enter text;

3: Press Send.";

}

By looking at both the HTML page and JavaScript console in Figure 3-21 (Chrome) and Figure 3-22 (Firefox), the reader can appreciate the effect of the execution of this code.

Closing channels and resetting the application in Chrome

Figure 3-21. Closing channels and resetting the application in Chrome

Closing channels and resetting the application in Firefox

Figure 3-22. Closing channels and resetting the application in Firefox