WebRTC- Data 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 sends a string to another PC every 3 seconds.
To test out this demo, 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. When you press F12 and go to the console of the opened tab, you will see the incoming message.

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>
   <script>
      var pc = {};      // mapping IDs of peers to their respective RTCPeerConenction objects
      var id = "0";
      id = "1";                 // comment out on the second tab or PC, ie. the offerrer
      const config = {
         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).on('child_added', async (data) => {
        let d = data.val();
        let m = JSON.parse(d.data);
        if (!pc[d.sender]) {                     // step 2. answerer
           pc[d.sender] = new RTCPeerConnection(servers);
           pc[d.sender].onicecandidate = (event) => {
              if (!event.candidate) return;
              let child = fdb.ref(d.sender).push();
              child.set({ sender: id, ice: true,
                          data: JSON.stringify(event.candidate) });
              child.remove();
           };
           pc[d.sender].ondatachannel = (event) => {
              pc[d.sender].channel = event.channel;
              addHandlers(d.sender);
           };
        }
        if (d.ice) {
           if (!pc[d.sender].ice && d.sender != id) pc[d.sender].addIceCandidate(m);
        } else if (m.type == 'offer') {          // step 2. answerer
           await pc[d.sender].setRemoteDescription(m)
           const answer = await pc[d.sender].createAnswer();
           pc[d.sender].setLocalDescription(answer);
           let child = fdb.ref(d.sender).push();
           child.set({ sender: id, data: JSON.stringify(answer) });
           child.remove();
           console.log("offering done");
        } else {                                 // step 3. offerrer
          pc[d.sender].setRemoteDescription(m)
                      .catch(console.error);
        }
     });
     async function streamComm(pid, withVideo){  //step 1. offerrer
        if (pid == id) { alert("You can't call yourself!"); return; }
        callID = pid;
        pc[pid] = new RTCPeerConnection(servers);
        pc[pid].channel = pc[pid].createDataChannel("chat");
        pc[pid].pid = pid;
        offer = await pc[pid].createOffer();
        await pc[pid].setLocalDescription(offer);
        pc[pid].ondatachannel = (event) => {
           pc[pid].channel = event.channel;
           pc[pid].connected = true;
           addHandlers(pid);
        };
        pc[pid].remoteSet=false;
        let child = fdb.ref(pid).push();
        child.set({ sender: id,
                    data: JSON.stringify(pc[pid].localDescription) });
        child.remove();
        setInterval(()=>{
           console.log("pinging",pc[pid].channel);
           pc[pid].channel.send("HELLO PEER!");  // step 4. offerrer sends message
        },3000);
     }
     function addHandlers(cid) {
        c = pc[cid];
        c.onicecandidate = (event) => {
            if (!event.candidate) return;
            let child = fdb.ref(cid).push();
            child.set({ sender: id, ice: true,
                        data: JSON.stringify(event.candidate) });
            child.remove();
        };
        c.channel.onopen = (event) => { };
        c.channel.onclose = (event) => { };
        c.channel.onmessage = (event) => {
           console.log(event.data);           // step 5. answerer receives message
        };
     }
     //streamComm("1",true);  // uncomment on the second tab or PC, ie. the offerrer
   </script>
</body></html>