Modern JavaScript Applications
上QQ阅读APP看书,第一时间看更新

WebRTC applications using PeerJS

PeerJS is a client-side JavaScript library that provides an easy-to-use API to work with WebRTC. It only provides an API to exchange MediaStream and arbitrary data between peers. It doesn't provide an API to work with MediaStream.

PeerServer

PeerServer is an open source signaling server used by PeerJS to establish a peer-to-peer connection. PeerServer is written in Node.js. If you don't want to run your own PeerServer instance, then you can use PeerServer cloud, which hosts PeerServer for public use. PeerServer cloud allows you to establish a maximum of 50 concurrent connections for free.

A unique ID identifies every peer connected to PeerServer. PeerServer itself can generate the ID, or else the peers can provide their own ID. For a peer to establish a peer-to-peer connection with another peer, it just needs to know the other peer's ID.

You might want to run your own PeerServer instance when you want to add more functionality to PeerServer or you want to support more than 50 concurrent connections. For example, if you want to check whether the user is logged in to PeerServer, then you need to add this feature and host your own customized PeerServer.

In this chapter, we will use PeerServer cloud, but in the next chapter, we will create our own instance of PeerServer. Therefore, to continue further with this chapter, create an account on the PeerServer cloud and retrieve the API key. Every application gets an API key to access the PeerServer cloud. If you are hosting your own PeerServer, then you won't need an API key. The API key is used by PeerServer cloud to track the total connections established by an application. To create an account and retrieve an API key, visit http://peerjs.com/peerserver.

PeerJS API

Let's discuss the PeerJS API by creating a simple app that allows the users to exchange video and text messages with any user whose ID they have.

Create a peerjs-demo directory in your web server and place a file named index.html in it.

In the index.html file, we need to first enqueue the PeerJS library. Download PeerJS from http://peerjs.com/. At the time of writing, the latest version of PeerJS was 0.3.14. I would recommend that you stick to this version for the following examples. Place this starting code in the index.html file:

<!doctype html>
<html>
  <head>
    <title>PeerJS Demo</title>
  </head>
  <body>

    <!-- Place HTML code here -->

    <script src="peer.min.js"></script>
    <script>
      //place JavaScript code here
    </script>
  </body>
</html>

Here, I enqueued the minified version of PeerJS.

PeerJS API comprises of three main constructors, as follows:

  • Peer: An instance of Peer represents a peer in the network. A peer is connected to the signaling server and STUN, and optionally, to a TURN.
  • DataConnection: DataConnection (that is, the instance of DataConnection) represents a peer-to-peer connection, which is used to exchange the arbitrary data. Technically, it wraps RTCDataChannel.
  • MediaConnection: MediaConnection (that is, the instance of MediaConnection) represents a peer-to-peer connection that is used to exchange MediaStream. Technically, it wraps RTCPeerConnection.

If a peer wants to establish DataConnection or MediaConnection with another peer, then it simply needs to know the other peer's ID. PeerJS doesn't give the other peer an option to accept or reject DataConnection. Also, in the case of MediaConnection, PeerJS doesn't give the other peer an option to accept or reject MediaConnection, but MediaConnection will be inactive until it is activated programmatically by the other peer so that MediaStream can be transferred, otherwise MediaStream will not be transferred. So, we can write our own logic to let the other user accept or reject DataConnection or MediaConnecton, that is, as soon as DataConnection or MediaConnection is established, we can cancel it by asking the user for their opinion.

Note

At present, one MediaConnection can transfer only one MediaStream. In future releases of PeerJS, a single MediaConnection will support the transfer of multiple MediaStreams.

Now, we need to create a <video> tag where the video will be displayed, a button to connect to a peer, and also a text box to send message. Here is the HTML code to display all these:

<video id="remoteVideo"></video>
<br>
<button onclick="connect()">Connect</button>
<br>
<input type="text" id="message">
<button onclick="send_message()">Send Message</button>

Now as soon as the page loads, we need to connect to PeerServer and ICE servers so that other peers can talk to us, and also when a user clicks on the connect button, we can establish DataConnection and MediaConnection. The following is the code for this:

var peer = null;

window.addEventListener("load", function(){
  var id = prompt("Please enter an unique name");
  
  peer = new Peer(id, {key: "io3esxy6y43zyqfr"}); 

  peer.on("open", function(id){
    alert("Connected to PeerServer successfully with ID: " + id);
  });

  peer.on("error", function(err){
    alert("An error occured. Error type: " + err.type);
  })

  peer.on("disconnected", function(){
    alert("Disconnected from signaling server. You ID is taken away. Peer-to-peer connections is still intact");
  })

  peer.on("close", function(){
    alert("Connection to signaling server and peer-to-peer connections have been killed. You ID is taken away. You have been destroyed");
  })

  peer.on("connection", function(dataConnection){
    setTimeout(function(){
      if(confirm(dataConnection.peer + " wants to send data to you. Do you want to accept?"))
      {
        acceptDataConnection(dataConnection);
      }
      else
      {
        dataConnection.close();
      }
    }, 100)
  })

  peer.on("call", function(mediaConnection){
    setTimeout(function(){
      if(confirm("Got a call from " + mediaConnection.peer + ". Do you want to pick the call?"))
      {
        acceptMediaConnection(mediaConnection);
      }
      else
      {
        mediaConnection.close();
      }
    }, 100);
  })
});

Here is how the code works:

  • First we displayed a prompt box to take the ID as an input so that every peer can decide their own ID.
  • Then we created an instance of Peer with ID and PeerServer cloud key. Here we didn't provide signaling and ICE server's URLs, therefore, PeerJS will use PeerServer cloud as the signaling server and Google's public STUN server. It will not use any TURN server. As soon as a Peer instance is created, the instance connects to the signaling server and registers the given ID.
  • Then we attached five event handlers to the peer object.
  • The open event is triggered when the connection to PeerServer was successful.
  • The error event is triggered for errors on the peer object.
  • The disconnected event is triggered when the connection with the signaling server is disconnected. The connection with the signaling server may get disconnected due to network problem or if you manually call the peer.disconnect() method. Once you are disconnected, your ID can be taken by someone else. You can try to reconnect with the same ID using the peer.reconnect() method. You can check whether peer is connected to the signaling server using the peer.disconnect Boolean property.
  • The close event is triggered when peer is destroyed, that is, it cannot be used anymore, all MediaConnections and DataConnections are killed, connection with the signaling server is killed, the ID is taken away, and so on. You may want to manually destroy peer when you don't need it anymore. You can destroy a peer using the peer.destroy() method.
  • The connection event is triggered when some other peer establishes DataConnection with you. As I said earlier, DataConnection is established without further permission, but you can close it as soon as it's established if you want. Here we let the user decide if they want to continue or close DataConnection established by another peer. The event handler attached to the event receives an instance of DataConnection via the parameter that represents the currently established DataConnection.
  • The call event is triggered when some other peer establishes MediaConnection with you. Here, we also let the user decide if they want to continue or close MediaConnection established by another peer. The event handler attached to the event receives an instance of MediaConnection via the parameter that represents the currently established MediaConnection.
  • Here, in the call and connection event handlers, we asynchronously displayed the confirm popup boxes to prevent blocking the execution of the event handler that causes issues in some browsers, that is, blocking it fails to establish DataConnection and MediaConnection.

Now, let's implement the acceptDataConnection() and acceptMediaConnection() functions so that we can display the text messages and remote MediaStream when other peer establishes DataConnection or MediaConnection with us. Here's the code:

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

var myDataConnection = null;
var myMediaConnection = null;

function acceptDataConnection(dataConnection)
{
  myDataConnection = dataConnection;

  dataConnection.on("data", function(data){
    alert("Message from " + dataConnection.peer + ".\n" + data)
  })

  dataConnection.on("close", function(data){
    alert("DataConnecion closed");
  })

  dataConnection.on("error", function(err){
    alert("Error occured on DataConnection. Error: " + err);
  })
}

function acceptMediaConnection(mediaConnection)
{
  myMediaConnection = mediaConnection;

  mediaConnection.on("stream", function(remoteStream){

    document.getElementById("remoteVideo").setAttribute("src", URL.createObjectURL(remoteStream));
    document.getElementById("remoteVideo").play();
  })

  mediaConnection.on("close", function(data){
    alert("MediaConnecion closed");
  })

  mediaConnection.on("error", function(err){
    alert("Error occured on MediaConnection. Error: " + err);
  })

  navigator.getUserMedia({video: true, audio: true}, function(mediaStream) {
    mediaConnection.answer(mediaStream);
  }, function(e){ alert("Error with MediaStream: " + e); });
}

This is how the preceding code works:

  • In the acceptDataConnection() function, we attached three event handlers to DataConnection. The data event is triggered when the other peer sends us data. The close event is triggered when DataConnection is closed. Finally, the error event is triggered when an error occurs on DataConnection. We can manually close DataConnection using the dataConnection.close() method.
  • In the acceptMediaConnection() function, we attached three event handlers and transferred our MediaStream to the other peer. The stream event is triggered when other peer sends us MediaStream. The close event is triggered when MediaConnection is closed. Finally, we activated MediaConnection using the mediaConnection.answer() method by passing our MediaStream. After MediaConnection is activated, the stream event will be triggered.

We finished writing the code to handle MediaConnection or DataConnection established by another peer with us. Now we need to write a code to create MediaConnection and DataConnection that a user clicks on the connect button. Here is the code:

function connect()
{
  var id = prompt("Please enter other peer ID");
  establishDataConnection(id);
  establishMediaConnection(id);
}

function establishDataConnection(id)

{
  var dataConnection = peer.connect(id, {reliable: true, ordered: true});

  myDataConnection = dataConnection;

  dataConnection.on("open", function(){
    alert("DataConnecion Established");
  });

  dataConnection.on("data", function(data){
    alert("Message from " + dataConnection.peer + ".\n" + data)
  })

  dataConnection.on("close", function(data){
    alert("DataConnecion closed");
  })

  dataConnection.on("error", function(err){
    alert("Error occured on DataConnection. Error: " + err);
  })
}

function establishMediaConnection(id)
{
  var mediaConnection = null;

  navigator.getUserMedia({video: true, audio: true}, function(mediaStream) {
    mediaConnection = peer.call(id, mediaStream);

    myMediaConnection = mediaConnection;

    mediaConnection.on("stream", function(remoteStream){
      document.getElementById("remoteVideo").setAttribute("src", URL.createObjectURL(remoteStream));
      document.getElementById("remoteVideo").play();
    })

    mediaConnection.on("error", function(err){
      alert("Error occured on MediaConnection. Error: " + err);
    })

    mediaConnection.on("close", function(data){
      alert("MediaConnecion closed");
    })
  }, function(e){ alert("Error with MediaStream: " + e); });
}

Here is how the code works:

  • First we asked the user to input another user's ID.
  • Then we established DataConnection. To establish a DataConnection with another user, we need to invoke the connect() method of the Peer instance with other peer's ID. We also made DataConnection reliable and ordered. Then, we attached the event handlers. We also saw how data, close, and error events work. The open event is triggered when DataConnection is established.
  • After establishing the DataConnection, we established MediaConnection. To establish MediaConnection, we need to call the call() method of the Peer instance. We need to pass MediaStream to the call() method. Finally, we attached the event handlers. The stream event will be triggered when the other user calls the answer() method of the MediaConnection instance, that is, when the MediaConnection is activated.

Now the last thing we need to do is write the code to send the message when a user clicks on the send message button. Here is the code for this:

function send_message()
{
  var text = document.getElementById("message").value;

  myDataConnection.send(text);
}

To send data via MediaConnection, we need to call the send() method of the MediaConnection instance. Here, we are sending a string, but you can pass any type of data including blobs and objects.

Now, to test the application, open the index.html page URL in two different browsers, devices, or tabs. I am assuming that you have opened the URL in two different devices. In each device, provide a different ID to identify the user. Then click on the connect button in any one device and enter the other peer's ID. Now accept the request on the other device. Once this is done, both the devices will be able to display each other's webcam video and microphone audio. You can also send messages between them.

Note

You can find the official documentation of PeerJS API at http://peerjs.com/docs/#api.