WebRTC- Video Channel

WebRTC (Web Real-Time Communication) allows text and media data to be transferred from one client to another client on a peer-to-peer basis. In other words, the server is bypassed during the transferring process. This lightens the workload on the server and minimizes latency.

To establish contact, two peer computers first need to exchange some information (in the form of JSON strings), over a server. This can be done by periodically polling your web server with AJAX calls, or through a third-party server using Firebase.

A STUN/TURN ice server is used to traverse Network Address Translation (NAT), allowing you to bypass the routers' restrictions and firewalls. You can use Google's STUN server for free. To create an account for free access to a TURN server, go to http://numb.viagenie.ca/.

An SDP offer is made by one peer. When an SDP answer is generated by the other contacted peer, a connection is established. If STUN is used, the media will travel directly. If TURN is used, the media will be proxied. TURN is effectively a packet mirror.

To utilize Firebase to initiate a WebRTC connection, first register a Firebase Account. Then, on Firebase console, navigate to Storage > Rules. Edit the rules as follows:

rules_version = '2';service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write; } } }

You can use the connection between two peers to exchange arbitrary binary data using the RTCDataChannel interface. This can be used for back-channel information, metadata exchange, game status packets, file transfers, or even as a primary channel for data transfer.

To summarize the steps required to initiate a WebRTC connection between two remote PCs:

  1. Register an account to use the TURN server at http://numb.viagenie.ca/ .
  2. Register a Firebase account to use its Storage service.
  3. Assign an ID to each peer.
  4. Initialize the Firebase service in your code.
  5. Let the offerrer generate its RTCPeerConnection(servers) object. With it, create an 'offer' and then generate its local description.
  6. Pass the 'offer' of the offerrer to the other peer(ie. the answerer) using the Firebase service.
  7. Let the answerer generate its RTCPeerConnection(servers) object. Set its remote description to be the 'offer' of the offerrer received via Firebase. Create an answer and then generate its local description.
  8. Pass the 'answer' of the answerer back to the offerrer.
  9. Set the remote description of the the offerrer to be the 'answer' of the answerer.

This streams live video through the webcams attached to two geographically separated PCs. To see this example in action, host the HTML file on a server, and connect to it on two tabs on a browser through HTTPS. (getUserMedia() requires an HTTPS connection)
Assign an id to each of the two peers. Open the first tab (ie. answerer) by clicking FULL. Then uncomment the last line of JavaScript and replace the ids. Next click RUN (ie. offerrer) to connect to the opened tab. You will see two live videos being streamed.

RESETRUNFULL
<!DOCTYPE html><html>
<head>
   <script src="https://www.gstatic.com/firebasejs/7.13.1/firebase-app.js" > </script>
   <script src="https://www.gstatic.com/firebasejs/7.13.1/firebase-database.js" ></script>
</head><body>
   <video id="me" autoplay></video>
   <video id="him" autoplay></video>
   <script>
      var id = "0";
      id = "1";                 // comment out on the second tab or PC, ie. the offerrer
      var comm=null;
      const config = {   // provided by Firebase
         apiKey: "AIzaSyAEa88lL0H2FMc69FIF3Zj_4ZvXuZ3WDLY",
         authDomain: "aladdin-25960.firebaseapp.com",
         databaseURL: "https://aladdin-25960.firebaseio.com",
         projectId: "aladdin-25960",
         storageBucket: "aladdin-25960.appspot.com",
         messagingSenderId: "425644961089",
         appId: "1:425644961089:web:bb2af70ba11202404505c0",
         measurementId: "G-J84LGPB6FX"};
      firebase.initializeApp(config);
      var fdb = firebase.database();
      fdb.ref().remove();
      var servers = {'iceServers': [{'urls': 'stun:stun.services.mozilla.com'}, 
                                    {'urls': 'stun:stun.l.google.com:19302'}, 
                                    {'urls': 'turn:numb.viagenie.ca', 'credential': 'Canons200sp',
                                                                      'username': '[email protected]'}]};
      fdb.ref(id + "v").on('child_added', async (data) => {
         let d = data.val();
         let m = JSON.parse(d.data);
         if (!comm) {
            comm = new RTCPeerConnection(servers);
            comm.pid = d.sender;
            setCallHandlers(comm, true, d.sender);
         }
         if (d.ice) {
            if (!comm.ice) comm.addIceCandidate(m);
         } else if (m.type == 'offer') {
            await comm.setRemoteDescription(m);
            const stream = await navigator.mediaDevices.getUserMedia(
                                      {video:{width:"50%",height:"50%",facingMode: 'user' }, audio: true });
            document.getElementById("me").srcObject = stream;
            stream.getTracks().forEach(track => comm.addTrack(track, stream));
            const answer = await comm.createAnswer();
            comm.setLocalDescription(answer);
            let child = fdb.ref(d.sender + "v").push();
            child.set({ sender: id, data: JSON.stringify(answer) });
            child.remove();
         } else { // on answer received
            comm.setRemoteDescription(m).catch(alert);
         }
      });
      async function streamComm(pid, withVideo) {
         if (pid == id) { alert("You can't call yourself!"); return; }
         callID = pid;
         comm = new RTCPeerConnection(servers);
         comm.pid = pid;
         setCallHandlers(comm, withVideo, pid);
         let localStream = await navigator.mediaDevices.getUserMedia(
                                    {video:{width:"50%",height: "50%", facingMode: 'user' }, audio: true });
         document.getElementById("me").srcObject = localStream;
         localStream.getTracks().forEach(track =>comm.addTrack(track, localStream));
         let offer = await comm.createOffer();
         await comm.setLocalDescription(offer);
         let child = fdb.ref(pid+ "v").push();
         child.set({ sender: id,
                     data: JSON.stringify(comm.localDescription) });
         child.remove();
      }
      function setCallHandlers(c, withVideo, pid) {
         c.onicecandidate = event => {
            if (!event.candidate) return;
            let child = fdb.ref(pid + "v").push();
            child.set({ sender: id, ice: true, 
                        data: JSON.stringify(event.candidate) });
            child.remove();
         };
         c.ontrack = event => {
            if (document.getElementById("him").srcObject != event.streams[0]){
               document.getElementById("him").srcObject = event.streams[0];
            }
         };
         c.onremovestream = event => { };
         c.oniceconnectionstatechange = event => {
            switch (event.iceConnectionState) {
               case "closed":
               case "failed":
               case "disconnected":
                  break;
            }
         };
         c.onicegatheringstatechange = event => { };
         c.onsignalingstatechange = event => {
            switch (event.signalingState) {
               case "closed":
                break;
            }
         };
      } 
      //streamComm("1",true);  // uncomment on the second tab or PC, ie. the offerrer
   </script>
</body></html>