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.
<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.
Here is a simple breakdown of how we're using PubNub in the demo:
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 } }
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.
p2pSetup: function () { console.log("Setting up P2P..."); this.shareStart = Date.now(); this.pubnub.subscribe({ user: this.id, // Indicates P2P communication callback: this.onP2PMessage }); }
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.
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); } }
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(); }
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:
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"); }
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; }
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); }