├── .npmignore ├── .gitignore ├── lib ├── exports.js ├── util.js └── reliable.js ├── package.json ├── Gruntfile.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /lib/exports.js: -------------------------------------------------------------------------------- 1 | window.Reliable = require('./reliable'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reliable", 3 | "version": "0.1.0", 4 | "description": "Reliable DataChannels.", 5 | "main": "./lib/reliable.js", 6 | "scripts": { 7 | "prepublish": "./node_modules/.bin/grunt" 8 | }, 9 | "devDependencies": { 10 | "grunt": "^0.4.5", 11 | "grunt-browserify": "^3.0.1", 12 | "grunt-cli": "^0.1.13", 13 | "grunt-contrib-concat": "^0.5.0", 14 | "grunt-contrib-uglify": "^0.5.1" 15 | }, 16 | "dependencies": { 17 | "js-binarypack": "0.0.9" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | 5 | browserify: { 6 | dev: { 7 | src: ['lib/exports.js'], 8 | dest: 'dist/reliable.js' 9 | } 10 | }, 11 | 12 | uglify: { 13 | prod: { 14 | options: { mangle: true, compress: true }, 15 | src: 'dist/reliable.js', 16 | dest: 'dist/reliable.min.js' 17 | } 18 | }, 19 | 20 | concat: { 21 | dev: { 22 | options: { 23 | banner: '/*! <%= pkg.name %> build:<%= pkg.version %>, development. '+ 24 | 'Copyright(c) 2013 Michelle Bu */' 25 | }, 26 | src: 'dist/reliable.js', 27 | dest: 'dist/reliable.js', 28 | }, 29 | prod: { 30 | options: { 31 | banner: '/*! <%= pkg.name %> build:<%= pkg.version %>, production. '+ 32 | 'Copyright(c) 2013 Michelle Bu */' 33 | }, 34 | src: 'dist/reliable.min.js', 35 | dest: 'dist/reliable.min.js', 36 | } 37 | } 38 | }); 39 | 40 | grunt.loadNpmTasks('grunt-browserify'); 41 | grunt.loadNpmTasks('grunt-contrib-uglify'); 42 | grunt.loadNpmTasks('grunt-contrib-concat'); 43 | 44 | grunt.registerTask('default', ['browserify', 'uglify', 'concat']); 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reliable transfer over DataChannels 2 | 3 | 4 | ## Reliable 5 | 6 | `new Reliable(dc)`: A reliable utility class for DataChannel. Takes in a `DataChannel` object. 7 | * `.send(msg)`: Takes any message and sends it reliably. 8 | * `.onmessage(msg)`: Called when data is received. 9 | 10 | `Reliable.higherBandwidthSDP(sdp)`: This need to be applied to all offer/answer SDPs for Reliable to function properly. Returns the new SDP with added bandwidth. See usage below. 11 | 12 | ```js 13 | // Assuming 2 PeerConnections pc1, pc2. 14 | pc1.createOffer(function(offer) { 15 | offer.sdp = Reliable.higherBandwidthSDP(offer.sdp); 16 | pc1.setLocalDescription(offer, ...); 17 | }); 18 | 19 | ... 20 | 21 | // Same process for answer. 22 | pc2.createAnswer(function(answer) { 23 | answer.sdp = Reliable.higherBandwidthSDP(answer.sdp); 24 | pc2.setLocalDescription(answer, ...); 25 | }); 26 | ``` 27 | 28 | ## Internal message format 29 | 30 | ### ACK 31 | 32 | This is an ACK for a chunk of the message. 33 | 34 | ```js 35 | [ 36 | /* type */ 'ack', 37 | /* id */ message_id, 38 | /* ACK */ n // The next chunk # expected. 39 | ] 40 | ``` 41 | 42 | ### Chunk 43 | 44 | This is a chunk of the message. 45 | 46 | ```js 47 | [ 48 | /* type */ 'chunk', 49 | /* id */ message_id, 50 | /* n */ n, // The chunk #. 51 | /* chunk */ chunk // The actual binary chunk. 52 | ] 53 | ``` 54 | 55 | 56 | ### END 57 | 58 | This is the end of a message. 59 | 60 | ```js 61 | [ 62 | /* type */ 'end', 63 | /* id */ message_id, 64 | /* n */ n // The last index. 65 | ] 66 | ``` 67 | 68 | 69 | ### Unchunked message 70 | 71 | This is a message that was able to be sent without being chunked. 72 | 73 | ```js 74 | [ 75 | /* type */ 'no', 76 | /* msg */ payload 77 | ] 78 | ``` 79 | 80 | ## Future plans 81 | 82 | Use stream API. 83 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var BinaryPack = require('js-binarypack'); 2 | 3 | var util = { 4 | debug: false, 5 | 6 | inherits: function(ctor, superCtor) { 7 | ctor.super_ = superCtor; 8 | ctor.prototype = Object.create(superCtor.prototype, { 9 | constructor: { 10 | value: ctor, 11 | enumerable: false, 12 | writable: true, 13 | configurable: true 14 | } 15 | }); 16 | }, 17 | extend: function(dest, source) { 18 | for(var key in source) { 19 | if(source.hasOwnProperty(key)) { 20 | dest[key] = source[key]; 21 | } 22 | } 23 | return dest; 24 | }, 25 | pack: BinaryPack.pack, 26 | unpack: BinaryPack.unpack, 27 | 28 | log: function () { 29 | if (util.debug) { 30 | var copy = []; 31 | for (var i = 0; i < arguments.length; i++) { 32 | copy[i] = arguments[i]; 33 | } 34 | copy.unshift('Reliable: '); 35 | console.log.apply(console, copy); 36 | } 37 | }, 38 | 39 | setZeroTimeout: (function(global) { 40 | var timeouts = []; 41 | var messageName = 'zero-timeout-message'; 42 | 43 | // Like setTimeout, but only takes a function argument. There's 44 | // no time argument (always zero) and no arguments (you have to 45 | // use a closure). 46 | function setZeroTimeoutPostMessage(fn) { 47 | timeouts.push(fn); 48 | global.postMessage(messageName, '*'); 49 | } 50 | 51 | function handleMessage(event) { 52 | if (event.source == global && event.data == messageName) { 53 | if (event.stopPropagation) { 54 | event.stopPropagation(); 55 | } 56 | if (timeouts.length) { 57 | timeouts.shift()(); 58 | } 59 | } 60 | } 61 | if (global.addEventListener) { 62 | global.addEventListener('message', handleMessage, true); 63 | } else if (global.attachEvent) { 64 | global.attachEvent('onmessage', handleMessage); 65 | } 66 | return setZeroTimeoutPostMessage; 67 | }(this)), 68 | 69 | blobToArrayBuffer: function(blob, cb){ 70 | var fr = new FileReader(); 71 | fr.onload = function(evt) { 72 | cb(evt.target.result); 73 | }; 74 | fr.readAsArrayBuffer(blob); 75 | }, 76 | blobToBinaryString: function(blob, cb){ 77 | var fr = new FileReader(); 78 | fr.onload = function(evt) { 79 | cb(evt.target.result); 80 | }; 81 | fr.readAsBinaryString(blob); 82 | }, 83 | binaryStringToArrayBuffer: function(binary) { 84 | var byteArray = new Uint8Array(binary.length); 85 | for (var i = 0; i < binary.length; i++) { 86 | byteArray[i] = binary.charCodeAt(i) & 0xff; 87 | } 88 | return byteArray.buffer; 89 | }, 90 | randomToken: function () { 91 | return Math.random().toString(36).substr(2); 92 | } 93 | }; 94 | 95 | module.exports = util; 96 | -------------------------------------------------------------------------------- /lib/reliable.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'); 2 | 3 | /** 4 | * Reliable transfer for Chrome Canary DataChannel impl. 5 | * Author: @michellebu 6 | */ 7 | function Reliable(dc, debug) { 8 | if (!(this instanceof Reliable)) return new Reliable(dc); 9 | this._dc = dc; 10 | 11 | util.debug = debug; 12 | 13 | // Messages sent/received so far. 14 | // id: { ack: n, chunks: [...] } 15 | this._outgoing = {}; 16 | // id: { ack: ['ack', id, n], chunks: [...] } 17 | this._incoming = {}; 18 | this._received = {}; 19 | 20 | // Window size. 21 | this._window = 1000; 22 | // MTU. 23 | this._mtu = 500; 24 | // Interval for setInterval. In ms. 25 | this._interval = 0; 26 | 27 | // Messages sent. 28 | this._count = 0; 29 | 30 | // Outgoing message queue. 31 | this._queue = []; 32 | 33 | this._setupDC(); 34 | }; 35 | 36 | // Send a message reliably. 37 | Reliable.prototype.send = function(msg) { 38 | // Determine if chunking is necessary. 39 | var bl = util.pack(msg); 40 | if (bl.size < this._mtu) { 41 | this._handleSend(['no', bl]); 42 | return; 43 | } 44 | 45 | this._outgoing[this._count] = { 46 | ack: 0, 47 | chunks: this._chunk(bl) 48 | }; 49 | 50 | if (util.debug) { 51 | this._outgoing[this._count].timer = new Date(); 52 | } 53 | 54 | // Send prelim window. 55 | this._sendWindowedChunks(this._count); 56 | this._count += 1; 57 | }; 58 | 59 | // Set up interval for processing queue. 60 | Reliable.prototype._setupInterval = function() { 61 | // TODO: fail gracefully. 62 | 63 | var self = this; 64 | this._timeout = setInterval(function() { 65 | // FIXME: String stuff makes things terribly async. 66 | var msg = self._queue.shift(); 67 | if (msg._multiple) { 68 | for (var i = 0, ii = msg.length; i < ii; i += 1) { 69 | self._intervalSend(msg[i]); 70 | } 71 | } else { 72 | self._intervalSend(msg); 73 | } 74 | }, this._interval); 75 | }; 76 | 77 | Reliable.prototype._intervalSend = function(msg) { 78 | var self = this; 79 | msg = util.pack(msg); 80 | util.blobToBinaryString(msg, function(str) { 81 | self._dc.send(str); 82 | }); 83 | if (self._queue.length === 0) { 84 | clearTimeout(self._timeout); 85 | self._timeout = null; 86 | //self._processAcks(); 87 | } 88 | }; 89 | 90 | // Go through ACKs to send missing pieces. 91 | Reliable.prototype._processAcks = function() { 92 | for (var id in this._outgoing) { 93 | if (this._outgoing.hasOwnProperty(id)) { 94 | this._sendWindowedChunks(id); 95 | } 96 | } 97 | }; 98 | 99 | // Handle sending a message. 100 | // FIXME: Don't wait for interval time for all messages... 101 | Reliable.prototype._handleSend = function(msg) { 102 | var push = true; 103 | for (var i = 0, ii = this._queue.length; i < ii; i += 1) { 104 | var item = this._queue[i]; 105 | if (item === msg) { 106 | push = false; 107 | } else if (item._multiple && item.indexOf(msg) !== -1) { 108 | push = false; 109 | } 110 | } 111 | if (push) { 112 | this._queue.push(msg); 113 | if (!this._timeout) { 114 | this._setupInterval(); 115 | } 116 | } 117 | }; 118 | 119 | // Set up DataChannel handlers. 120 | Reliable.prototype._setupDC = function() { 121 | // Handle various message types. 122 | var self = this; 123 | this._dc.onmessage = function(e) { 124 | var msg = e.data; 125 | var datatype = msg.constructor; 126 | // FIXME: msg is String until binary is supported. 127 | // Once that happens, this will have to be smarter. 128 | if (datatype === String) { 129 | var ab = util.binaryStringToArrayBuffer(msg); 130 | msg = util.unpack(ab); 131 | self._handleMessage(msg); 132 | } 133 | }; 134 | }; 135 | 136 | // Handles an incoming message. 137 | Reliable.prototype._handleMessage = function(msg) { 138 | var id = msg[1]; 139 | var idata = this._incoming[id]; 140 | var odata = this._outgoing[id]; 141 | var data; 142 | switch (msg[0]) { 143 | // No chunking was done. 144 | case 'no': 145 | var message = id; 146 | if (!!message) { 147 | this.onmessage(util.unpack(message)); 148 | } 149 | break; 150 | // Reached the end of the message. 151 | case 'end': 152 | data = idata; 153 | 154 | // In case end comes first. 155 | this._received[id] = msg[2]; 156 | 157 | if (!data) { 158 | break; 159 | } 160 | 161 | this._ack(id); 162 | break; 163 | case 'ack': 164 | data = odata; 165 | if (!!data) { 166 | var ack = msg[2]; 167 | // Take the larger ACK, for out of order messages. 168 | data.ack = Math.max(ack, data.ack); 169 | 170 | // Clean up when all chunks are ACKed. 171 | if (data.ack >= data.chunks.length) { 172 | util.log('Time: ', new Date() - data.timer); 173 | delete this._outgoing[id]; 174 | } else { 175 | this._processAcks(); 176 | } 177 | } 178 | // If !data, just ignore. 179 | break; 180 | // Received a chunk of data. 181 | case 'chunk': 182 | // Create a new entry if none exists. 183 | data = idata; 184 | if (!data) { 185 | var end = this._received[id]; 186 | if (end === true) { 187 | break; 188 | } 189 | data = { 190 | ack: ['ack', id, 0], 191 | chunks: [] 192 | }; 193 | this._incoming[id] = data; 194 | } 195 | 196 | var n = msg[2]; 197 | var chunk = msg[3]; 198 | data.chunks[n] = new Uint8Array(chunk); 199 | 200 | // If we get the chunk we're looking for, ACK for next missing. 201 | // Otherwise, ACK the same N again. 202 | if (n === data.ack[2]) { 203 | this._calculateNextAck(id); 204 | } 205 | this._ack(id); 206 | break; 207 | default: 208 | // Shouldn't happen, but would make sense for message to just go 209 | // through as is. 210 | this._handleSend(msg); 211 | break; 212 | } 213 | }; 214 | 215 | // Chunks BL into smaller messages. 216 | Reliable.prototype._chunk = function(bl) { 217 | var chunks = []; 218 | var size = bl.size; 219 | var start = 0; 220 | while (start < size) { 221 | var end = Math.min(size, start + this._mtu); 222 | var b = bl.slice(start, end); 223 | var chunk = { 224 | payload: b 225 | } 226 | chunks.push(chunk); 227 | start = end; 228 | } 229 | util.log('Created', chunks.length, 'chunks.'); 230 | return chunks; 231 | }; 232 | 233 | // Sends ACK N, expecting Nth blob chunk for message ID. 234 | Reliable.prototype._ack = function(id) { 235 | var ack = this._incoming[id].ack; 236 | 237 | // if ack is the end value, then call _complete. 238 | if (this._received[id] === ack[2]) { 239 | this._complete(id); 240 | this._received[id] = true; 241 | } 242 | 243 | this._handleSend(ack); 244 | }; 245 | 246 | // Calculates the next ACK number, given chunks. 247 | Reliable.prototype._calculateNextAck = function(id) { 248 | var data = this._incoming[id]; 249 | var chunks = data.chunks; 250 | for (var i = 0, ii = chunks.length; i < ii; i += 1) { 251 | // This chunk is missing!!! Better ACK for it. 252 | if (chunks[i] === undefined) { 253 | data.ack[2] = i; 254 | return; 255 | } 256 | } 257 | data.ack[2] = chunks.length; 258 | }; 259 | 260 | // Sends the next window of chunks. 261 | Reliable.prototype._sendWindowedChunks = function(id) { 262 | util.log('sendWindowedChunks for: ', id); 263 | var data = this._outgoing[id]; 264 | var ch = data.chunks; 265 | var chunks = []; 266 | var limit = Math.min(data.ack + this._window, ch.length); 267 | for (var i = data.ack; i < limit; i += 1) { 268 | if (!ch[i].sent || i === data.ack) { 269 | ch[i].sent = true; 270 | chunks.push(['chunk', id, i, ch[i].payload]); 271 | } 272 | } 273 | if (data.ack + this._window >= ch.length) { 274 | chunks.push(['end', id, ch.length]) 275 | } 276 | chunks._multiple = true; 277 | this._handleSend(chunks); 278 | }; 279 | 280 | // Puts together a message from chunks. 281 | Reliable.prototype._complete = function(id) { 282 | util.log('Completed called for', id); 283 | var self = this; 284 | var chunks = this._incoming[id].chunks; 285 | var bl = new Blob(chunks); 286 | util.blobToArrayBuffer(bl, function(ab) { 287 | self.onmessage(util.unpack(ab)); 288 | }); 289 | delete this._incoming[id]; 290 | }; 291 | 292 | // Ups bandwidth limit on SDP. Meant to be called during offer/answer. 293 | Reliable.higherBandwidthSDP = function(sdp) { 294 | // AS stands for Application-Specific Maximum. 295 | // Bandwidth number is in kilobits / sec. 296 | // See RFC for more info: http://www.ietf.org/rfc/rfc2327.txt 297 | 298 | // Chrome 31+ doesn't want us munging the SDP, so we'll let them have their 299 | // way. 300 | var version = navigator.appVersion.match(/Chrome\/(.*?) /); 301 | if (version) { 302 | version = parseInt(version[1].split('.').shift()); 303 | if (version < 31) { 304 | var parts = sdp.split('b=AS:30'); 305 | var replace = 'b=AS:102400'; // 100 Mbps 306 | if (parts.length > 1) { 307 | return parts[0] + replace + parts[1]; 308 | } 309 | } 310 | } 311 | 312 | return sdp; 313 | }; 314 | 315 | // Overwritten, typically. 316 | Reliable.prototype.onmessage = function(msg) {}; 317 | 318 | module.exports = Reliable; 319 | --------------------------------------------------------------------------------