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.
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).
Figure 3-2. The example page loaded in Chrome
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).
Figure 3-4. The example page after user grants consent, in Chrome
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).
Figure 3-6. Chrome 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).
Figure 3-8. Chrome 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.
Figure 3-10. Chrome 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.
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.
Figure 3-13. Starting the data channel application in Chrome
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.
Figure 3-15. The data channel application in Chrome, after startup
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.
Figure 3-17. Getting ready to stream a message across the data channel in Chrome
Figure 3-18. Getting ready to stream a message across the data channel in Firefox
Figure 3-19. Receiving a message from the data channel in Chrome
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.
Figure 3-21. Closing channels and resetting the application in Chrome
Figure 3-22. Closing channels and resetting the application in Firefox