RTC PubNub Guide

Welcome to the RTC PubNub how-to guide. This page is dedicated to providing a bit of insight into how we built the PubShare demo. As you can see, the actual demo app is embedded in this page so you can conveniently interact with it as you need while looking over the guide.

The PubShare demo was created using PubNub and the WebRTC DataChannel via PubNub's new beta WebRTC API. As you may know already, PubNub allows developers to quickly enable their applications with real-time communication capabilities with minimal effort. But certain scenarios require lower latency and more data throughput than is economically feasible with vanilla PubNub. So we've continued the ease-of-use that comes with PubNub and made our WebRTC API simple to use, especially if you're already familiar with the PubNub JavaScript API.

Note: Google sign-in is disabled for the iframes.

Setup

Download on GitHub...

...include in your page

        <script src="http://cdn.pubnub.com/pubnub-3.5.1.min.js"></script>
        <script src="./webrtc-beta-pubnub.js"></script>

The official PubNub JavaScript API is available most easily from our CDN. But the beta WebRTC API is only available on GitHub at the moment due to its ongoing development and the rapidly changing nature of WebRTC itself.

In the adjacent code sample you can see how to include PubNub and the beta API. Note that the normal PubNub include comes before you include the API you downloaded from GitHub. This is to avoid any dependency issues.

And just in case you were wondering, we used a couple of other tools for mainly UI work: jQuery and underscore.js.

Using the API

Here is a simple breakdown of how we're using PubNub in the demo:

  1. Basic call to subscribe
  2. Presence to see who is joining/leaving the sharing room
  3. Callback for signals indicating someone wants to share a file
  4. Sending/receiving file data via WebRTC API

Normal PubNub subscribe for handling people coming and going, and share signaling:

1
pubnub.subscribe({
    channel: protocol.CHANNEL,
    callback: this.handleSignal.bind(this),
    presence: this.handlePresence.bind(this)
});
handlePresence: function (msg) {
    var conn = this.connections[msg.uuid];
    if (conn) {
        // Pass the message to specific contact/connection
        conn.handlePresence(msg);
    }
    else {
        // Create a new connection and update the UI list
    }
}
2
3
handleSignal: function (msg) {
    if (msg.action === protocol.ANSWER) {
        console.log("THE OTHER PERSON IS READY");
        this.p2pSetup();
    }
    else if (msg.action === protocol.OFFER) {
        // Someone's ready to send a file.
        // Let user opt-in to receive file data
        // Update UI to indicate there is a file available
    }
    else if (msg.action === protocol.ERR_REJECT) {
        alert("Unable to communicate with " + this.email);
    }
    else if (msg.action === protocol.CANCEL) {
        alert(this.email + " cancelled the share.");
    }
}

Below you can see how we initiate the peer-to-peer connection for sending the actual file data. Notice that the only difference from the normal PubNub subscribe is that a user is specified instead of a normal channel.

4
p2pSetup: function () {
    console.log("Setting up P2P...");
    this.shareStart = Date.now();
    this.pubnub.subscribe({
        user: this.id,  // Indicates P2P communication
        callback: this.onP2PMessage
    });
}

Handling File Data

This is the onchange event handler for the file input:

this.filePicked = function (e) {
    var file = self.fileInput.files[0];
    if (file) {
        var mbSize = file.size / (1024 * 1024);
        if (mbSize > MAX_FSIZE) {
            alert("Your file is too big, sorry.");
            // Reset file input
        }
        var reader = new FileReader();
        reader.onloadend = function (e) {
            if (reader.readyState == FileReader.DONE) {
                self.fileManager.stageLocalFile(file.name, file.type, reader.result);
                self.offerShare();
            }
        };
        reader.readAsArrayBuffer(file);
    }
}

In order to help simplify the code, all of the logic for manipulating file data is done inside the FileManager. Each connection with people listed on the screen has its own FileManager which deals with setting up a local file for sending over the wire, or piecing together file data sent from a remote partner.

The reason we need all of this code to break up the file into chunks and control how much data is sent at a time is because the underlying RTCDataChannel has a limit on the size of individual messages. The DataChannel is also not entirely reliable yet, so some file chunks might get lost when trying to send them, in which case we have to resend them. That's why we're following a request/response model for file chunks: the receiver knows how many chunks are needed to build the file, then requests groups of chunks at a time from the file owner.

Once the receiver actually has all of the file chunks, their FileManager just sticks them into a Blob and uses an objectURL to download the file from the browser.

Please browse through the code as well if you need more information about how file data is handled and how the request/response system works.

This function just splits the buffer created from the File into an array of equally sized chunks:

stageLocalFile: function (fName, fType, buffer) {
    this.fileName = fName;
    this.fileType = fType;
    this.buffer = buffer;
    var nChunks = Math.ceil(buffer.byteLength / this.chunkSize);
    this.fileChunks = new Array(nChunks);
    var start;
    for (var i = 0; i < nChunks; i++) {
        start = i * this.chunkSize;
        this.fileChunks[i] = buffer.slice(start, start + this.chunkSize);
    }
}

Automatically download the file once all chunks are received:

downloadFile: function () {
    var blob = new Blob(this.fileChunks, { type: this.fileType });
    var link = document.querySelector("#download");
    link.href = window.URL.createObjectURL(blob);
    link.download = this.fileName;
    link.click();
}

Extras - Google Contacts

As you probably noticed, we included the ability to use the demo by signing in to your Google account and share files with any of your Google contacts who have also signed into the demo.

Here are the steps we followed to setup Google login via OAUTH2 in order to use the Contacts API:

  1. Enable API access by creating a client ID in the Google APIs Console
  2. Direct the user to the Google OAUTH2 request page
  3. Parse the auth token when the user is redirected back to your page
  4. Pull down contacts using Google's API

This will open the OAUTH2 page in the current tab:

2
obtainGoogleToken: function () {
        var params = {
            response_type: "token",
            client_id: "YOUR_CLIENT_ID",
            redirect_uri: window.location.origin + window.location.pathname,
            scope: CONTACT_API_URL
        };
        var query = [];
        for (var p in params) {
            query.push(p + "=" + encodeURIComponent(params[p]));
        }
        query = query.join("&");
        window.open("https://accounts.google.com/o/oauth2/auth?" + query, "_self");
    }

Check to see if the page is opening with a Google auth token in the URL:

var params = {}, queryString = location.hash.substring(1),
        regex = /([^&=]+)=([^&]*)/g, m;
    while (m = regex.exec(queryString)) {
        params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
    }

    if (params.access_token) {
        window.location.hash = "";
        client.getContacts(params.access_token);
        return;
    }
3

Call the Contacts API and process the results:

getContacts: function (token) {
        this.token = token;
        var self = this;
        var req = {
            url: CONTACT_API_URL + "/contacts/default/full",
            data: {
                access_token: this.token,
                v: 3.0,
                alt: "json",
                "max-results": 10000
            }
        };
        var handleRes = function (res) {
            var userEmail = res.feed.author[0].email["$t"].toLowerCase(),
                contacts = res.feed.entry;
            contacts.forEach(function (e) {
                if (!e["gd$email"]) {
                    return;
                }
                var contactEmail = e["gd$email"][0].address.toLowerCase();
                if (userEmail === contactEmail) {
                    return;
                }
                self.contactEmails[contactEmail] = true;
                // Create a Connection for this contact and
                // add them to the UI list
            });
        }
        $.ajax(req).done(handleRes);
    }
4