Video call in browser on PeerJS. Fast start

I greet all readers of Habr. This year, I had the opportunity to write a video communication module for one training portal for phoning via video communication directly on the teacher’s and student’s website. It was not necessary to solve such an early task. After a short search, I discovered that there are 2 ways: Flash and WebRTC . WebRTC in its pure form turned out to be complicated, and in general it is natural, since the task of video communication is not simple. But then I came across PeerJS , which is a wrapper for WebRTC. In this article I will tell you how to quickly organize your browser dialer.



In order to repeat the example, access to your test page via the https protocol will be required (since the page will request access to the camera and microphone, and without a secure protocol the browser will simply give an error)



Starting layout will look like this:



<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Peer</title> <script src="https://unpkg.com/peerjs@1.0.0/dist/peerjs.min.js"></script> </head> <body> <p><h3> ID: </h3><span id=myid ></span></p> <input id=otherPeerId type=text placeholder="otherPeerId" > <button onclick="callToNode(document.getElementById('otherPeerId').value)"></button> <br> <video id=myVideo muted="muted" width="400px" height="auto" ></video> <div id=callinfo ></div> <video id=remVideo width="400px" height="auto" ></video> </body>
      
      





In the head section, we connect PeerJS remotely. It is also possible to download the script and connect it locally.



input id = otherPeerId - designed to enter the feast of the one we will call (you can take it as an index, or as a phone number).



Two video tags are designed to display your own video and for the video of the interlocutor, respectively.



Now a little about WebRTC technology and how the call is made. WebRTC makes a call from client to client directly, without server involvement, so in the first step 2 browsers must find each other. To do this, a classic WebRTC requires a signal server, that is, a server that tells one browser the parameters of another browser, and in WebRTC you have to organize such a server yourself. However, PeerJS developers provide their own signal server. All you need to do is to pass the peerID to the potential interlocutor, that is, the unique index received in the PeerJS system. In a working draft, I organized it like this:



  1. After loading the page, a Peer object is created
  2. Its peerID is written to mysql database
  3. When the Call button is pressed, the peerID of the interlocutor is pulled out of the database and used to establish a connection


In the current test case, we will enter the peerID of the interlocutor in the otherPeerId text field



So, let's start writing code.



1. Create the main peer object



 var peer = new Peer();
      
      





2. At the opening of the feast, we will receive the coveted peerID, which must be transferred to the partner so that he can contact us



 peer.on('open', function(peerID) { document.getElementById('myid').innerHTML=peerID; });
      
      





3. In order to receive a call, we hang up the handler for the call event



 var peercall; peer.on('call', function(call) { // Answer the call, providing our mediaStream peercall=call; document.getElementById('callinfo').innerHTML="  <button onclick='callanswer()' ></button><button onclick='callcancel()' ></button>"; });
      
      





With an incoming call, we get a call object, which we save in the peercall global variable. Also in the information block a notification about an incoming call and 2 buttons will be displayed: Accept and Reject



4. We write the function for the Accept button



 function callanswer() { navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) { var video = document.getElementById('myVideo'); peercall.answer(mediaStream); //         //peercall.on ('close', onCallClose); //  -  video.srcObject = mediaStream; //      (  ) document.getElementById('callinfo').innerHTML=" ... <button onclick='callclose()' > </button>"; //,   ,     video.onloadedmetadata = function(e) {// ,    video.play(); }; setTimeout(function() { //        document.getElementById('remVideo').srcObject = peercall.remoteStream; document.getElementById('remVideo').onloadedmetadata= function(e) { //       document.getElementById('remVideo').play(); }; },1500); }).catch(function(err) { console.log(err.name + ": " + err.message); }); }
      
      





navigator.mediaDevices.getUserMedia - requests access to the camera and microphone. In the data of the object that is passed to this method {audio: true, video: true}, you can accordingly request access only to the camera or only to the microphone. Further comments added directly to the code.



setTimeout was added empirically: the partner’s video did not start playing, but it worked with a timeout.



5. The function of dialing by the Call button



 function callToNode(peerId) { // navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) { var video = document.getElementById('myVideo'); peercall = peer.call(peerId,mediaStream); //,  peerId-    mediaStream peercall.on('stream', function (stream) { // ,   setTimeout(function() { document.getElementById('remVideo').srcObject = peercall.remoteStream; document.getElementById('remVideo').onloadedmetadata= function(e) { document.getElementById('remVideo').play(); }; },1500); }); // peercall.on('close', onCallClose); video.srcObject = mediaStream; video.onloadedmetadata = function(e) { video.play(); }; }).catch(function(err) { console.log(err.name + ": " + err.message); }); }
      
      





As in the previous paragraph, we request your media stream. After we call the call function of the peer object, which will return the call object to us, save it in peercall. We process the stream event to find out what they answered and put the incoming stream in the corresponding video object



That's all, but ...



If both callers are behind the NATth call will not go through. (Why? Read here habr.com/en/company/yandex/blog/419951 )

In order to overcome this obstacle, it is necessary to specify the TURN server when creating the peer object (The question of where to get it was not the easiest. We had to raise our own: VPS on Ubuntu 16.04. Installation by
 apt install coturn
      
      



)



Then the creation of the feast will look something like this:



 var callOptions={'iceServers': [ {url: 'stun:95.xxx.xx.x9:3479', username: "user", credential: "xxxxxxxxxx"}, { url: "turn:95.xxx.xx.x9:3478", username: "user", credential: "xxxxxxxx"}] }; peer= new Peer({config: callOptions});
      
      





Finally, the entire code:



 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Peer</title> <script src="https://unpkg.com/peerjs@1.0.0/dist/peerjs.min.js"></script> </head> <body> <p><h3> ID: </h3><span id=myid ></span></p> <input id=otherPeerId type=text placeholder="otherPeerId" > <button onclick="callToNode(document.getElementById('otherPeerId').value)"></button> <br> <video id=myVideo muted="muted" width="400px" height="auto" ></video> <div id=callinfo ></div> <video id=remVideo width="400px" height="auto" ></video> <script> var callOptions={'iceServers': [ {url: 'stun:95.xxx.xx.x9:3479', username: "user", credential: "xxxxxxxxxx"}, { url: "turn:95.xxx.xx.x9:3478", username: "user", credential: "xxxxxxxx"}] }; peer= new Peer({config: callOptions}); peer.on('open', function(peerID) { document.getElementById('myid').innerHTML=peerID; }); var peercall; peer.on('call', function(call) { // Answer the call, providing our mediaStream peercall=call; document.getElementById('callinfo').innerHTML="  <button onclick='callanswer()' ></button><button onclick='callcancel()' ></button>"; }); function callanswer() { navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) { var video = document.getElementById('myVideo'); peercall.answer(mediaStream); //         //peercall.on ('close', onCallClose); //  -  video.srcObject = mediaStream; //      (  ) document.getElementById('callinfo').innerHTML=" ... <button onclick='callclose()' > </button>"; //,   ,     video.onloadedmetadata = function(e) {// ,    video.play(); }; setTimeout(function() { //        document.getElementById('remVideo').srcObject = peercall.remoteStream; document.getElementById('remVideo').onloadedmetadata= function(e) { //       document.getElementById('remVideo').play(); }; },1500); }).catch(function(err) { console.log(err.name + ": " + err.message); }); } function callToNode(peerId) { // navigator.mediaDevices.getUserMedia ({ audio: true, video: true }).then(function(mediaStream) { var video = document.getElementById('myVideo'); peercall = peer.call(peerId,mediaStream); peercall.on('stream', function (stream) { // ,   setTimeout(function() { document.getElementById('remVideo').srcObject = peercall.remoteStream; document.getElementById('remVideo').onloadedmetadata= function(e) { document.getElementById('remVideo').play(); }; },1500); }); // peercall.on('close', onCallClose); video.srcObject = mediaStream; video.onloadedmetadata = function(e) { video.play(); }; }).catch(function(err) { console.log(err.name + ": " + err.message); }); } </script> </body>
      
      





This solution was successfully tested under Windows7 and Ubuntu 18.04 in Chrome, Opera, Firefox. Chrome also works on Android and MacOS, but it doesn’t work on iPhone and iPad.



All Articles