/*! peerjs.js build:0.2.7, development. Copyright(c) 2013 Michelle Bu */ (function(exports){ var binaryFeatures = {}; binaryFeatures.useBlobBuilder = (function(){ try { new Blob([]); return false; } catch (e) { return true; } })(); binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){ try { return (new Blob([new Uint8Array([])])).size === 0; } catch (e) { return true; } })(); exports.binaryFeatures = binaryFeatures; exports.BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder || window.BlobBuilder; function BufferBuilder(){ this._pieces = []; this._parts = []; } BufferBuilder.prototype.append = function(data) { if(typeof data === 'number') { this._pieces.push(data); } else { this._flush(); this._parts.push(data); } }; BufferBuilder.prototype._flush = function() { if (this._pieces.length > 0) { var buf = new Uint8Array(this._pieces); if(!binaryFeatures.useArrayBufferView) { buf = buf.buffer; } this._parts.push(buf); this._pieces = []; } }; BufferBuilder.prototype.getBuffer = function() { this._flush(); if(binaryFeatures.useBlobBuilder) { var builder = new BlobBuilder(); for(var i = 0, ii = this._parts.length; i < ii; i++) { builder.append(this._parts[i]); } return builder.getBlob(); } else { return new Blob(this._parts); } }; exports.BinaryPack = { unpack: function(data){ var unpacker = new Unpacker(data); return unpacker.unpack(); }, pack: function(data, utf8){ var packer = new Packer(utf8); var buffer = packer.pack(data); return buffer; } }; function Unpacker (data){ // Data is ArrayBuffer this.index = 0; this.dataBuffer = data; this.dataView = new Uint8Array(this.dataBuffer); this.length = this.dataBuffer.byteLength; } Unpacker.prototype.unpack = function(){ var type = this.unpack_uint8(); if (type < 0x80){ var positive_fixnum = type; return positive_fixnum; } else if ((type ^ 0xe0) < 0x20){ var negative_fixnum = (type ^ 0xe0) - 0x20; return negative_fixnum; } var size; if ((size = type ^ 0xa0) <= 0x0f){ return this.unpack_raw(size); } else if ((size = type ^ 0xb0) <= 0x0f){ return this.unpack_string(size); } else if ((size = type ^ 0x90) <= 0x0f){ return this.unpack_array(size); } else if ((size = type ^ 0x80) <= 0x0f){ return this.unpack_map(size); } switch(type){ case 0xc0: return null; case 0xc1: return undefined; case 0xc2: return false; case 0xc3: return true; case 0xca: return this.unpack_float(); case 0xcb: return this.unpack_double(); case 0xcc: return this.unpack_uint8(); case 0xcd: return this.unpack_uint16(); case 0xce: return this.unpack_uint32(); case 0xcf: return this.unpack_uint64(); case 0xd0: return this.unpack_int8(); case 0xd1: return this.unpack_int16(); case 0xd2: return this.unpack_int32(); case 0xd3: return this.unpack_int64(); case 0xd4: return undefined; case 0xd5: return undefined; case 0xd6: return undefined; case 0xd7: return undefined; case 0xd8: size = this.unpack_uint16(); return this.unpack_string(size); case 0xd9: size = this.unpack_uint32(); return this.unpack_string(size); case 0xda: size = this.unpack_uint16(); return this.unpack_raw(size); case 0xdb: size = this.unpack_uint32(); return this.unpack_raw(size); case 0xdc: size = this.unpack_uint16(); return this.unpack_array(size); case 0xdd: size = this.unpack_uint32(); return this.unpack_array(size); case 0xde: size = this.unpack_uint16(); return this.unpack_map(size); case 0xdf: size = this.unpack_uint32(); return this.unpack_map(size); } } Unpacker.prototype.unpack_uint8 = function(){ var byte = this.dataView[this.index] & 0xff; this.index++; return byte; }; Unpacker.prototype.unpack_uint16 = function(){ var bytes = this.read(2); var uint16 = ((bytes[0] & 0xff) * 256) + (bytes[1] & 0xff); this.index += 2; return uint16; } Unpacker.prototype.unpack_uint32 = function(){ var bytes = this.read(4); var uint32 = ((bytes[0] * 256 + bytes[1]) * 256 + bytes[2]) * 256 + bytes[3]; this.index += 4; return uint32; } Unpacker.prototype.unpack_uint64 = function(){ var bytes = this.read(8); var uint64 = ((((((bytes[0] * 256 + bytes[1]) * 256 + bytes[2]) * 256 + bytes[3]) * 256 + bytes[4]) * 256 + bytes[5]) * 256 + bytes[6]) * 256 + bytes[7]; this.index += 8; return uint64; } Unpacker.prototype.unpack_int8 = function(){ var uint8 = this.unpack_uint8(); return (uint8 < 0x80 ) ? uint8 : uint8 - (1 << 8); }; Unpacker.prototype.unpack_int16 = function(){ var uint16 = this.unpack_uint16(); return (uint16 < 0x8000 ) ? uint16 : uint16 - (1 << 16); } Unpacker.prototype.unpack_int32 = function(){ var uint32 = this.unpack_uint32(); return (uint32 < Math.pow(2, 31) ) ? uint32 : uint32 - Math.pow(2, 32); } Unpacker.prototype.unpack_int64 = function(){ var uint64 = this.unpack_uint64(); return (uint64 < Math.pow(2, 63) ) ? uint64 : uint64 - Math.pow(2, 64); } Unpacker.prototype.unpack_raw = function(size){ if ( this.length < this.index + size){ throw new Error('BinaryPackFailure: index is out of range' + ' ' + this.index + ' ' + size + ' ' + this.length); } var buf = this.dataBuffer.slice(this.index, this.index + size); this.index += size; //buf = util.bufferToString(buf); return buf; } Unpacker.prototype.unpack_string = function(size){ var bytes = this.read(size); var i = 0, str = '', c, code; while(i < size){ c = bytes[i]; if ( c < 128){ str += String.fromCharCode(c); i++; } else if ((c ^ 0xc0) < 32){ code = ((c ^ 0xc0) << 6) | (bytes[i+1] & 63); str += String.fromCharCode(code); i += 2; } else { code = ((c & 15) << 12) | ((bytes[i+1] & 63) << 6) | (bytes[i+2] & 63); str += String.fromCharCode(code); i += 3; } } this.index += size; return str; } Unpacker.prototype.unpack_array = function(size){ var objects = new Array(size); for(var i = 0; i < size ; i++){ objects[i] = this.unpack(); } return objects; } Unpacker.prototype.unpack_map = function(size){ var map = {}; for(var i = 0; i < size ; i++){ var key = this.unpack(); var value = this.unpack(); map[key] = value; } return map; } Unpacker.prototype.unpack_float = function(){ var uint32 = this.unpack_uint32(); var sign = uint32 >> 31; var exp = ((uint32 >> 23) & 0xff) - 127; var fraction = ( uint32 & 0x7fffff ) | 0x800000; return (sign == 0 ? 1 : -1) * fraction * Math.pow(2, exp - 23); } Unpacker.prototype.unpack_double = function(){ var h32 = this.unpack_uint32(); var l32 = this.unpack_uint32(); var sign = h32 >> 31; var exp = ((h32 >> 20) & 0x7ff) - 1023; var hfrac = ( h32 & 0xfffff ) | 0x100000; var frac = hfrac * Math.pow(2, exp - 20) + l32 * Math.pow(2, exp - 52); return (sign == 0 ? 1 : -1) * frac; } Unpacker.prototype.read = function(length){ var j = this.index; if (j + length <= this.length) { return this.dataView.subarray(j, j + length); } else { throw new Error('BinaryPackFailure: read index out of range'); } } function Packer(utf8){ this.utf8 = utf8; this.bufferBuilder = new BufferBuilder(); } Packer.prototype.pack = function(value){ var type = typeof(value); if (type == 'string'){ this.pack_string(value); } else if (type == 'number'){ if (Math.floor(value) === value){ this.pack_integer(value); } else{ this.pack_double(value); } } else if (type == 'boolean'){ if (value === true){ this.bufferBuilder.append(0xc3); } else if (value === false){ this.bufferBuilder.append(0xc2); } } else if (type == 'undefined'){ this.bufferBuilder.append(0xc0); } else if (type == 'object'){ if (value === null){ this.bufferBuilder.append(0xc0); } else { var constructor = value.constructor; if (constructor == Array){ this.pack_array(value); } else if (constructor == Blob || constructor == File) { this.pack_bin(value); } else if (constructor == ArrayBuffer) { if(binaryFeatures.useArrayBufferView) { this.pack_bin(new Uint8Array(value)); } else { this.pack_bin(value); } } else if ('BYTES_PER_ELEMENT' in value){ if(binaryFeatures.useArrayBufferView) { this.pack_bin(value); } else { this.pack_bin(value.buffer); } } else if (constructor == Object){ this.pack_object(value); } else if (constructor == Date){ this.pack_string(value.toString()); } else if (typeof value.toBinaryPack == 'function'){ this.bufferBuilder.append(value.toBinaryPack()); } else { throw new Error('Type "' + constructor.toString() + '" not yet supported'); } } } else { throw new Error('Type "' + type + '" not yet supported'); } return this.bufferBuilder.getBuffer(); } Packer.prototype.pack_bin = function(blob){ var length = blob.length || blob.byteLength || blob.size; if (length <= 0x0f){ this.pack_uint8(0xa0 + length); } else if (length <= 0xffff){ this.bufferBuilder.append(0xda) ; this.pack_uint16(length); } else if (length <= 0xffffffff){ this.bufferBuilder.append(0xdb); this.pack_uint32(length); } else{ throw new Error('Invalid length'); return; } this.bufferBuilder.append(blob); } Packer.prototype.pack_string = function(str){ var length; if (this.utf8) { var blob = new Blob([str]); length = blob.size; } else { length = str.length; } if (length <= 0x0f){ this.pack_uint8(0xb0 + length); } else if (length <= 0xffff){ this.bufferBuilder.append(0xd8) ; this.pack_uint16(length); } else if (length <= 0xffffffff){ this.bufferBuilder.append(0xd9); this.pack_uint32(length); } else{ throw new Error('Invalid length'); return; } this.bufferBuilder.append(str); } Packer.prototype.pack_array = function(ary){ var length = ary.length; if (length <= 0x0f){ this.pack_uint8(0x90 + length); } else if (length <= 0xffff){ this.bufferBuilder.append(0xdc) this.pack_uint16(length); } else if (length <= 0xffffffff){ this.bufferBuilder.append(0xdd); this.pack_uint32(length); } else{ throw new Error('Invalid length'); } for(var i = 0; i < length ; i++){ this.pack(ary[i]); } } Packer.prototype.pack_integer = function(num){ if ( -0x20 <= num && num <= 0x7f){ this.bufferBuilder.append(num & 0xff); } else if (0x00 <= num && num <= 0xff){ this.bufferBuilder.append(0xcc); this.pack_uint8(num); } else if (-0x80 <= num && num <= 0x7f){ this.bufferBuilder.append(0xd0); this.pack_int8(num); } else if ( 0x0000 <= num && num <= 0xffff){ this.bufferBuilder.append(0xcd); this.pack_uint16(num); } else if (-0x8000 <= num && num <= 0x7fff){ this.bufferBuilder.append(0xd1); this.pack_int16(num); } else if ( 0x00000000 <= num && num <= 0xffffffff){ this.bufferBuilder.append(0xce); this.pack_uint32(num); } else if (-0x80000000 <= num && num <= 0x7fffffff){ this.bufferBuilder.append(0xd2); this.pack_int32(num); } else if (-0x8000000000000000 <= num && num <= 0x7FFFFFFFFFFFFFFF){ this.bufferBuilder.append(0xd3); this.pack_int64(num); } else if (0x0000000000000000 <= num && num <= 0xFFFFFFFFFFFFFFFF){ this.bufferBuilder.append(0xcf); this.pack_uint64(num); } else{ throw new Error('Invalid integer'); } } Packer.prototype.pack_double = function(num){ var sign = 0; if (num < 0){ sign = 1; num = -num; } var exp = Math.floor(Math.log(num) / Math.LN2); var frac0 = num / Math.pow(2, exp) - 1; var frac1 = Math.floor(frac0 * Math.pow(2, 52)); var b32 = Math.pow(2, 32); var h32 = (sign << 31) | ((exp+1023) << 20) | (frac1 / b32) & 0x0fffff; var l32 = frac1 % b32; this.bufferBuilder.append(0xcb); this.pack_int32(h32); this.pack_int32(l32); } Packer.prototype.pack_object = function(obj){ var keys = Object.keys(obj); var length = keys.length; if (length <= 0x0f){ this.pack_uint8(0x80 + length); } else if (length <= 0xffff){ this.bufferBuilder.append(0xde); this.pack_uint16(length); } else if (length <= 0xffffffff){ this.bufferBuilder.append(0xdf); this.pack_uint32(length); } else{ throw new Error('Invalid length'); } for(var prop in obj){ if (obj.hasOwnProperty(prop)){ this.pack(prop); this.pack(obj[prop]); } } } Packer.prototype.pack_uint8 = function(num){ this.bufferBuilder.append(num); } Packer.prototype.pack_uint16 = function(num){ this.bufferBuilder.append(num >> 8); this.bufferBuilder.append(num & 0xff); } Packer.prototype.pack_uint32 = function(num){ var n = num & 0xffffffff; this.bufferBuilder.append((n & 0xff000000) >>> 24); this.bufferBuilder.append((n & 0x00ff0000) >>> 16); this.bufferBuilder.append((n & 0x0000ff00) >>> 8); this.bufferBuilder.append((n & 0x000000ff)); } Packer.prototype.pack_uint64 = function(num){ var high = num / Math.pow(2, 32); var low = num % Math.pow(2, 32); this.bufferBuilder.append((high & 0xff000000) >>> 24); this.bufferBuilder.append((high & 0x00ff0000) >>> 16); this.bufferBuilder.append((high & 0x0000ff00) >>> 8); this.bufferBuilder.append((high & 0x000000ff)); this.bufferBuilder.append((low & 0xff000000) >>> 24); this.bufferBuilder.append((low & 0x00ff0000) >>> 16); this.bufferBuilder.append((low & 0x0000ff00) >>> 8); this.bufferBuilder.append((low & 0x000000ff)); } Packer.prototype.pack_int8 = function(num){ this.bufferBuilder.append(num & 0xff); } Packer.prototype.pack_int16 = function(num){ this.bufferBuilder.append((num & 0xff00) >> 8); this.bufferBuilder.append(num & 0xff); } Packer.prototype.pack_int32 = function(num){ this.bufferBuilder.append((num >>> 24) & 0xff); this.bufferBuilder.append((num & 0x00ff0000) >>> 16); this.bufferBuilder.append((num & 0x0000ff00) >>> 8); this.bufferBuilder.append((num & 0x000000ff)); } Packer.prototype.pack_int64 = function(num){ var high = Math.floor(num / Math.pow(2, 32)); var low = num % Math.pow(2, 32); this.bufferBuilder.append((high & 0xff000000) >>> 24); this.bufferBuilder.append((high & 0x00ff0000) >>> 16); this.bufferBuilder.append((high & 0x0000ff00) >>> 8); this.bufferBuilder.append((high & 0x000000ff)); this.bufferBuilder.append((low & 0xff000000) >>> 24); this.bufferBuilder.append((low & 0x00ff0000) >>> 16); this.bufferBuilder.append((low & 0x0000ff00) >>> 8); this.bufferBuilder.append((low & 0x000000ff)); } /** * Light EventEmitter. Ported from Node.js/events.js * Eric Zhang */ /** * EventEmitter class * Creates an object with event registering and firing methods */ function EventEmitter() { // Initialise required storage variables this._events = {}; } var isArray = Array.isArray; EventEmitter.prototype.addListener = function(type, listener, scope, once) { if ('function' !== typeof listener) { throw new Error('addListener only takes instances of Function'); } // To avoid recursion in the case that type == "newListeners"! Before // adding it to the listeners, first emit "newListeners". this.emit('newListener', type, typeof listener.listener === 'function' ? listener.listener : listener); if (!this._events[type]) { // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; } else if (isArray(this._events[type])) { // If we've already got an array, just append. this._events[type].push(listener); } else { // Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener, scope) { if ('function' !== typeof listener) { throw new Error('.once only takes instances of Function'); } var self = this; function g() { self.removeListener(type, g); listener.apply(this, arguments); }; g.listener = listener; self.on(type, g); return this; }; EventEmitter.prototype.removeListener = function(type, listener, scope) { if ('function' !== typeof listener) { throw new Error('removeListener only takes instances of Function'); } // does not use listeners(), so no side effect of creating _events[type] if (!this._events[type]) return this; var list = this._events[type]; if (isArray(list)) { var position = -1; for (var i = 0, length = list.length; i < length; i++) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; list.splice(position, 1); if (list.length == 0) delete this._events[type]; } else if (list === listener || (list.listener && list.listener === listener)) { delete this._events[type]; } return this; }; EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.removeAllListeners = function(type) { if (arguments.length === 0) { this._events = {}; return this; } // does not use listeners(), so no side effect of creating _events[type] if (type && this._events && this._events[type]) this._events[type] = null; return this; }; EventEmitter.prototype.listeners = function(type) { if (!this._events[type]) this._events[type] = []; if (!isArray(this._events[type])) { this._events[type] = [this._events[type]]; } return this._events[type]; }; EventEmitter.prototype.emit = function(type) { var type = arguments[0]; var handler = this._events[type]; if (!handler) return false; if (typeof handler == 'function') { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3: handler.call(this, arguments[1], arguments[2]); break; // slower default: var l = arguments.length; var args = new Array(l - 1); for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; handler.apply(this, args); } return true; } else if (isArray(handler)) { var l = arguments.length; var args = new Array(l - 1); for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; var listeners = handler.slice(); for (var i = 0, l = listeners.length; i < l; i++) { listeners[i].apply(this, args); } return true; } else { return false; } }; var util = { chromeCompatible: true, firefoxCompatible: true, chromeVersion: 26, firefoxVersion: 22, debug: false, browserisms: '', inherits: function(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }, extend: function(dest, source) { for(var key in source) { if(source.hasOwnProperty(key)) { dest[key] = source[key]; } } return dest; }, pack: BinaryPack.pack, unpack: BinaryPack.unpack, log: function () { if (util.debug) { var err = false; var copy = Array.prototype.slice.call(arguments); copy.unshift('PeerJS: '); for (var i = 0, l = copy.length; i < l; i++){ if (copy[i] instanceof Error) { copy[i] = '(' + copy[i].name + ') ' + copy[i].message; err = true; } } err ? console.error.apply(console, copy) : console.log.apply(console, copy); } }, setZeroTimeout: (function(global) { var timeouts = []; var messageName = 'zero-timeout-message'; // Like setTimeout, but only takes a function argument. There's // no time argument (always zero) and no arguments (you have to // use a closure). function setZeroTimeoutPostMessage(fn) { timeouts.push(fn); global.postMessage(messageName, '*'); } function handleMessage(event) { if (event.source == global && event.data == messageName) { if (event.stopPropagation) { event.stopPropagation(); } if (timeouts.length) { timeouts.shift()(); } } } if (global.addEventListener) { global.addEventListener('message', handleMessage, true); } else if (global.attachEvent) { global.attachEvent('onmessage', handleMessage); } return setZeroTimeoutPostMessage; }(this)), blobToArrayBuffer: function(blob, cb){ var fr = new FileReader(); fr.onload = function(evt) { cb(evt.target.result); }; fr.readAsArrayBuffer(blob); }, blobToBinaryString: function(blob, cb){ var fr = new FileReader(); fr.onload = function(evt) { cb(evt.target.result); }; fr.readAsBinaryString(blob); }, binaryStringToArrayBuffer: function(binary) { var byteArray = new Uint8Array(binary.length); for (var i = 0; i < binary.length; i++) { byteArray[i] = binary.charCodeAt(i) & 0xff; } return byteArray.buffer; }, randomToken: function () { return Math.random().toString(36).substr(2); }, isBrowserCompatible: function() { var c, f; if (this.chromeCompatible) { if ((c = navigator.userAgent.split('Chrome/')) && c.length > 1) { // Get version #. var v = c[1].split('.')[0]; return parseInt(v) >= this.chromeVersion; } } if (this.firefoxCompatible) { if ((f = navigator.userAgent.split('Firefox/')) && f.length > 1) { // Get version #. var v = f[1].split('.')[0]; return parseInt(v) >= this.firefoxVersion; } } return false; }, isSecure: function() { return location.protocol === 'https:'; } }; /** * Reliable transfer for Chrome Canary DataChannel impl. * Author: @michellebu */ function Reliable(dc, debug) { if (!(this instanceof Reliable)) return new Reliable(dc); this._dc = dc; util.debug = debug; // Messages sent/received so far. // id: { ack: n, chunks: [...] } this._outgoing = {}; // id: { ack: ['ack', id, n], chunks: [...] } this._incoming = {}; this._received = {}; // Window size. this._window = 1000; // MTU. this._mtu = 500; // Interval for setInterval. In ms. this._interval = 0; // Messages sent. this._count = 0; // Outgoing message queue. this._queue = []; this._setupDC(); }; // Send a message reliably. Reliable.prototype.send = function(msg) { // Determine if chunking is necessary. var bl = util.pack(msg); if (bl.size < this._mtu) { this._handleSend(['no', bl]); return; } this._outgoing[this._count] = { ack: 0, chunks: this._chunk(bl) }; if (util.debug) { this._outgoing[this._count].timer = new Date(); } // Send prelim window. this._sendWindowedChunks(this._count); this._count += 1; }; // Set up interval for processing queue. Reliable.prototype._setupInterval = function() { // TODO: fail gracefully. var self = this; this._timeout = setInterval(function() { // FIXME: String stuff makes things terribly async. var msg = self._queue.shift(); if (msg._multiple) { for (var i = 0, ii = msg.length; i < ii; i += 1) { self._intervalSend(msg[i]); } } else { self._intervalSend(msg); } }, this._interval); }; Reliable.prototype._intervalSend = function(msg) { var self = this; msg = util.pack(msg); util.blobToBinaryString(msg, function(str) { self._dc.send(str); }); if (self._queue.length === 0) { clearTimeout(self._timeout); self._timeout = null; //self._processAcks(); } }; // Go through ACKs to send missing pieces. Reliable.prototype._processAcks = function() { for (var id in this._outgoing) { if (this._outgoing.hasOwnProperty(id)) { this._sendWindowedChunks(id); } } }; // Handle sending a message. // FIXME: Don't wait for interval time for all messages... Reliable.prototype._handleSend = function(msg) { var push = true; for (var i = 0, ii = this._queue.length; i < ii; i += 1) { var item = this._queue[i]; if (item === msg) { push = false; } else if (item._multiple && item.indexOf(msg) !== -1) { push = false; } } if (push) { this._queue.push(msg); if (!this._timeout) { this._setupInterval(); } } }; // Set up DataChannel handlers. Reliable.prototype._setupDC = function() { // Handle various message types. var self = this; this._dc.onmessage = function(e) { var msg = e.data; var datatype = msg.constructor; // FIXME: msg is String until binary is supported. // Once that happens, this will have to be smarter. if (datatype === String) { var ab = util.binaryStringToArrayBuffer(msg); msg = util.unpack(ab); self._handleMessage(msg); } }; }; // Handles an incoming message. Reliable.prototype._handleMessage = function(msg) { var id = msg[1]; var idata = this._incoming[id]; var odata = this._outgoing[id]; var data; switch (msg[0]) { // No chunking was done. case 'no': var message = id; if (!!message) { this.onmessage(util.unpack(message)); } break; // Reached the end of the message. case 'end': data = idata; // In case end comes first. this._received[id] = msg[2]; if (!data) { break; } this._ack(id); break; case 'ack': data = odata; if (!!data) { var ack = msg[2]; // Take the larger ACK, for out of order messages. data.ack = Math.max(ack, data.ack); // Clean up when all chunks are ACKed. if (data.ack >= data.chunks.length) { util.log('Time: ', new Date() - data.timer); delete this._outgoing[id]; } else { this._processAcks(); } } // If !data, just ignore. break; // Received a chunk of data. case 'chunk': // Create a new entry if none exists. data = idata; if (!data) { var end = this._received[id]; if (end === true) { break; } data = { ack: ['ack', id, 0], chunks: [] }; this._incoming[id] = data; } var n = msg[2]; var chunk = msg[3]; data.chunks[n] = new Uint8Array(chunk); // If we get the chunk we're looking for, ACK for next missing. // Otherwise, ACK the same N again. if (n === data.ack[2]) { this._calculateNextAck(id); } this._ack(id); break; default: // Shouldn't happen, but would make sense for message to just go // through as is. this._handleSend(msg); break; } }; // Chunks BL into smaller messages. Reliable.prototype._chunk = function(bl) { var chunks = []; var size = bl.size; var start = 0; while (start < size) { var end = Math.min(size, start + this._mtu); var b = bl.slice(start, end); var chunk = { payload: b } chunks.push(chunk); start = end; } util.log('Created', chunks.length, 'chunks.'); return chunks; }; // Sends ACK N, expecting Nth blob chunk for message ID. Reliable.prototype._ack = function(id) { var ack = this._incoming[id].ack; // if ack is the end value, then call _complete. if (this._received[id] === ack[2]) { this._complete(id); this._received[id] = true; } this._handleSend(ack); }; // Calculates the next ACK number, given chunks. Reliable.prototype._calculateNextAck = function(id) { var data = this._incoming[id]; var chunks = data.chunks; for (var i = 0, ii = chunks.length; i < ii; i += 1) { // This chunk is missing!!! Better ACK for it. if (chunks[i] === undefined) { data.ack[2] = i; return; } } data.ack[2] = chunks.length; }; // Sends the next window of chunks. Reliable.prototype._sendWindowedChunks = function(id) { util.log('sendWindowedChunks for: ', id); var data = this._outgoing[id]; var ch = data.chunks; var chunks = []; var limit = Math.min(data.ack + this._window, ch.length); for (var i = data.ack; i < limit; i += 1) { if (!ch[i].sent || i === data.ack) { ch[i].sent = true; chunks.push(['chunk', id, i, ch[i].payload]); } } if (data.ack + this._window >= ch.length) { chunks.push(['end', id, ch.length]) } chunks._multiple = true; this._handleSend(chunks); }; // Puts together a message from chunks. Reliable.prototype._complete = function(id) { util.log('Completed called for', id); var self = this; var chunks = this._incoming[id].chunks; var bl = new Blob(chunks); util.blobToArrayBuffer(bl, function(ab) { self.onmessage(util.unpack(ab)); }); delete this._incoming[id]; }; // Ups bandwidth limit on SDP. Meant to be called during offer/answer. Reliable.higherBandwidthSDP = function(sdp) { // AS stands for Application-Specific Maximum. // Bandwidth number is in kilobits / sec. // See RFC for more info: http://www.ietf.org/rfc/rfc2327.txt var parts = sdp.split('b=AS:30'); var replace = 'b=AS:102400'; // 100 Mbps return parts[0] + replace + parts[1]; }; // Overwritten, typically. Reliable.prototype.onmessage = function(msg) {}; exports.Reliable = Reliable; if (window.mozRTCPeerConnection) { util.browserisms = 'Firefox'; } else if (window.webkitRTCPeerConnection) { util.browserisms = 'Webkit'; } else { util.browserisms = 'Unknown'; } exports.RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; exports.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.RTCPeerConnection; /** * A peer who can initiate connections with other peers. */ function Peer(id, options) { if (id && id.constructor == Object) { options = id; id = undefined; } if (!(this instanceof Peer)) return new Peer(id, options); EventEmitter.call(this); options = util.extend({ debug: false, host: '0.peerjs.com', port: 9000, key: 'peerjs', config: { 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] } }, options); this._options = options; util.debug = options.debug; // First check if browser can use PeerConnection/DataChannels. // TODO: when media is supported, lower browser version limit and move DC // check to where`connect` is called. var self = this; if (!util.isBrowserCompatible()) { util.setZeroTimeout(function() { self._abort('browser-incompatible', 'The current browser does not support WebRTC DataChannels'); }); return; } // Detect relative URL host. if (options.host === '/') { options.host = window.location.hostname; } // Ensure alphanumeric_- if (id && !/^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id)) { util.setZeroTimeout(function() { self._abort('invalid-id', 'ID "' + id + '" is invalid'); }); return; } if (options.key && !/^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(options.key)) { util.setZeroTimeout(function() { self._abort('invalid-key', 'API KEY "' + options.key + '" is invalid'); }); return; } this._secure = util.isSecure(); // Errors for now because no support for SSL on cloud server. if (this._secure && options.host === '0.peerjs.com') { util.setZeroTimeout(function() { self._abort('ssl-unavailable', 'The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS.'); }); return; } // States. this.destroyed = false; this.disconnected = false; // Connections for this peer. this.connections = {}; // Connection managers. this.managers = {}; // Queued connections to make. this._queued = []; // Init immediately if ID is given, otherwise ask server for ID if (id) { this.id = id; this._init(); } else { this.id = null; this._retrieveId(); } }; util.inherits(Peer, EventEmitter); Peer.prototype._retrieveId = function(cb) { var self = this; try { var http = new XMLHttpRequest(); var protocol = this._secure ? 'https://' : 'http://'; var url = protocol + this._options.host + ':' + this._options.port + '/' + this._options.key + '/id'; var queryString = '?ts=' + new Date().getTime() + '' + Math.random(); url += queryString; // If there's no ID we need to wait for one before trying to init socket. http.open('get', url, true); http.onreadystatechange = function() { if (http.readyState === 4) { if (http.status !== 200) { throw 'Retrieve id response not 200'; return; } self.id = http.responseText; self._init(); } }; http.send(null); } catch(e) { this._abort('server-error', 'Could not get an ID from the server'); } }; Peer.prototype._init = function() { var self = this; this._socket = new Socket(this._options.host, this._options.port, this._options.key, this.id); this._socket.on('message', function(data) { self._handleServerJSONMessage(data); }); this._socket.on('error', function(error) { util.log(error); self._abort('socket-error', error); }); this._socket.on('close', function() { var msg = 'Underlying socket has closed'; util.log('error', msg); self._abort('socket-closed', msg); }); this._socket.start(); } Peer.prototype._handleServerJSONMessage = function(message) { var peer = message.src; var manager = this.managers[peer]; var payload = message.payload; switch (message.type) { case 'OPEN': this._processQueue(); this.emit('open', this.id); break; case 'ERROR': this._abort('server-error', payload.msg); break; case 'ID-TAKEN': this._abort('unavailable-id', 'ID `'+this.id+'` is taken'); break; case 'OFFER': var options = { sdp: payload.sdp, labels: payload.labels, config: this._options.config }; var manager = this.managers[peer]; if (!manager) { manager = new ConnectionManager(this.id, peer, this._socket, options); this._attachManagerListeners(manager); this.managers[peer] = manager; this.connections[peer] = manager.connections; } manager.update(options.labels); manager.handleSDP(payload.sdp, message.type); break; case 'EXPIRE': if (manager) { manager.close(); manager.emit('error', new Error('Could not connect to peer ' + manager.peer)); } break; case 'ANSWER': if (manager) { manager.handleSDP(payload.sdp, message.type); } break; case 'CANDIDATE': if (manager) { manager.handleCandidate(payload); } break; case 'LEAVE': if (manager) { manager.handleLeave(); } break; case 'INVALID-KEY': this._abort('invalid-key', 'API KEY "' + this._key + '" is invalid'); break; default: util.log('Unrecognized message type:', message.type); break; } }; /** Process queued calls to connect. */ Peer.prototype._processQueue = function() { while (this._queued.length > 0) { var manager = this._queued.pop(); manager.initialize(this.id, this._socket); } }; /** Listeners for manager. */ Peer.prototype._attachManagerListeners = function(manager) { var self = this; // Handle receiving a connection. manager.on('connection', function(connection) { self.emit('connection', connection); }); // Handle a connection closing. manager.on('close', function() { if (!!self.managers[manager.peer]) { delete self.managers[manager.peer]; delete self.connections[manager.peer]; } }); manager.on('error', function(err) { self.emit('error', err); }); }; /** Destroys the Peer and emits an error message. */ Peer.prototype._abort = function(type, message) { util.log('Aborting. Error:', message); var err = new Error(message); err.type = type; this.destroy(); this.emit('error', err); }; Peer.prototype._cleanup = function() { var self = this; if (!!this.managers) { var peers = Object.keys(this.managers); for (var i = 0, ii = peers.length; i < ii; i++) { this.managers[peers[i]].close(); } } util.setZeroTimeout(function(){ self.disconnect(); }); this.emit('close'); }; /** Exposed connect function for users. Will try to connect later if user * is waiting for an ID. */ Peer.prototype.connect = function(peer, options) { if (this.disconnected) { var err = new Error('This Peer has been disconnected from the server and can no longer make connections.'); err.type = 'server-disconnected'; this.emit('error', err); return; } options = util.extend({ config: this._options.config }, options); var manager = this.managers[peer]; // Firefox currently does not support multiplexing once an offer is made. if (util.browserisms === 'Firefox' && !!manager && manager.firefoxSingular) { var err = new Error('Firefox currently does not support multiplexing after a DataChannel has already been established'); err.type = 'firefoxism'; this.emit('error', err); return; } if (!manager) { manager = new ConnectionManager(this.id, peer, this._socket, options); this._attachManagerListeners(manager); this.managers[peer] = manager; this.connections[peer] = manager.connections; } var connection = manager.connect(options); if (!this.id) { this._queued.push(manager); } return connection; }; /** * Return the peer id or null, if there's no id at the moment. * Reasons for no id could be 'connect in progress' or 'disconnected' */ Peer.prototype.getId = function() { return this.id; }; /** * Destroys the Peer: closes all active connections as well as the connection * to the server. * Warning: The peer can no longer create or accept connections after being * destroyed. */ Peer.prototype.destroy = function() { if (!this.destroyed) { this._cleanup(); this.destroyed = true; } }; /** * Disconnects the Peer's connection to the PeerServer. Does not close any * active connections. * Warning: The peer can no longer create or accept connections after being * disconnected. It also cannot reconnect to the server. */ Peer.prototype.disconnect = function() { if (!this.disconnected) { if (!!this._socket) { this._socket.close(); } this.id = null; this.disconnected = true; } }; /** The current browser. */ Peer.browser = util.browserisms; /** * Provides a clean method for checking if there's an active connection to the * peer server. */ Peer.prototype.isConnected = function() { return !this.disconnected; }; /** * Returns true if this peer is destroyed and can no longer be used. */ Peer.prototype.isDestroyed = function() { return this.destroyed; }; exports.Peer = Peer; /** * Wraps a DataChannel between two Peers. */ function DataConnection(peer, dc, options) { if (!(this instanceof DataConnection)) return new DataConnection(peer, dc, options); EventEmitter.call(this); options = util.extend({ serialization: 'binary' }, options); // Connection is not open yet. this.open = false; this.label = options.label; this.metadata = options.metadata; this.serialization = options.serialization; this.peer = peer; this.reliable = options.reliable; this._dc = dc; if (!!this._dc) { this._configureDataChannel(); } }; util.inherits(DataConnection, EventEmitter); DataConnection.prototype._configureDataChannel = function() { var self = this; if (util.browserisms !== 'Webkit') { // Webkit doesn't support binary yet this._dc.binaryType = 'arraybuffer'; } this._dc.onopen = function() { util.log('Data channel connection success'); self.open = true; self.emit('open'); }; // Use the Reliable shim for non Firefox browsers if (this.reliable && util.browserisms !== 'Firefox') { this._reliable = new Reliable(this._dc, util.debug); } if (this._reliable) { this._reliable.onmessage = function(msg) { self.emit('data', msg); }; } else { this._dc.onmessage = function(e) { self._handleDataMessage(e); }; } this._dc.onclose = function(e) { util.log('DataChannel closed.'); self.close(); }; }; DataConnection.prototype._cleanup = function() { if (!!this._dc && this._dc.readyState !== 'closed') { this._dc.close(); this._dc = null; } this.open = false; this.emit('close'); }; // Handles a DataChannel message. DataConnection.prototype._handleDataMessage = function(e) { var self = this; var data = e.data; var datatype = data.constructor; if (this.serialization === 'binary' || this.serialization === 'binary-utf8') { if (datatype === Blob) { // Datatype should never be blob util.blobToArrayBuffer(data, function(ab) { data = util.unpack(ab); self.emit('data', data); }); return; } else if (datatype === ArrayBuffer) { data = util.unpack(data); } else if (datatype === String) { // String fallback for binary data for browsers that don't support binary yet var ab = util.binaryStringToArrayBuffer(data); data = util.unpack(ab); } } else if (this.serialization === 'json') { data = JSON.parse(data); } this.emit('data', data); }; DataConnection.prototype.addDC = function(dc) { this._dc = dc; this._configureDataChannel(); }; /** * Exposed functionality for users. */ /** Allows user to close connection. */ DataConnection.prototype.close = function() { if (!this.open) { return; } this._cleanup(); }; /** Allows user to send data. */ DataConnection.prototype.send = function(data) { if (!this.open) { this.emit('error', new Error('Connection no longer open.')); } if (this._reliable) { // Note: reliable sending will make it so that you cannot customize // serialization. this._reliable.send(data); return; } var self = this; if (this.serialization === 'none') { this._dc.send(data); } else if (this.serialization === 'json') { this._dc.send(JSON.stringify(data)); } else { var utf8 = (this.serialization === 'binary-utf8'); var blob = util.pack(data, utf8); // DataChannel currently only supports strings. if (util.browserisms === 'Webkit') { util.blobToBinaryString(blob, function(str){ self._dc.send(str); }); } else { this._dc.send(blob); } } }; /** * Returns true if the DataConnection is open and able to send messages. */ DataConnection.prototype.isOpen = function() { return this.open; }; /** * Gets the metadata associated with this DataConnection. */ DataConnection.prototype.getMetadata = function() { return this.metadata; }; /** * Gets the label associated with this DataConnection. */ DataConnection.prototype.getLabel = function() { return this.label; }; /** * Gets the brokering ID of the peer that you are connected with. * Note that this ID may be out of date if the peer has disconnected from the * server, so it's not recommended that you use this ID to identify this * connection. */ DataConnection.prototype.getPeer = function() { return this.peer; }; /** * Manages DataConnections between its peer and one other peer. * Internally, manages PeerConnection. */ function ConnectionManager(id, peer, socket, options) { if (!(this instanceof ConnectionManager)) return new ConnectionManager(id, peer, socket, options); EventEmitter.call(this); options = util.extend({ config: { 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] } }, options); this._options = options; // PeerConnection is not yet dead. this.open = true; this.id = id; this.peer = peer; this.pc = null; // Mapping labels to metadata and serialization. // label => { metadata: ..., serialization: ..., reliable: ...} this.labels = {}; // A default label in the event that none are passed in. this._default = 0; // DataConnections on this PC. this.connections = {}; this._queued = []; this._socket = socket; if (!!this.id) { this.initialize(); } }; util.inherits(ConnectionManager, EventEmitter); ConnectionManager.prototype.initialize = function(id, socket) { if (!!id) { this.id = id; } if (!!socket) { this._socket = socket; } // Set up PeerConnection. this._startPeerConnection(); // Process queued DCs. this._processQueue(); // Listen for ICE candidates. this._setupIce(); // Listen for negotiation needed. // Chrome only ** this._setupNegotiationHandler(); // Listen for data channel. this._setupDataChannel(); this.initialize = function() { }; }; /** Start a PC. */ ConnectionManager.prototype._startPeerConnection = function() { util.log('Creating RTCPeerConnection'); this.pc = new RTCPeerConnection(this._options.config, { optional: [ { RtpDataChannels: true } ]}); }; /** Add DataChannels to all queued DataConnections. */ ConnectionManager.prototype._processQueue = function() { var conn = this._queued.pop(); if (!!conn) { var reliable = util.browserisms === 'Firefox' ? conn.reliable : false; conn.addDC(this.pc.createDataChannel(conn.label, { reliable: reliable })); } }; /** Set up ICE candidate handlers. */ ConnectionManager.prototype._setupIce = function() { util.log('Listening for ICE candidates.'); var self = this; this.pc.onicecandidate = function(evt) { if (evt.candidate) { util.log('Received ICE candidates.'); self._socket.send({ type: 'CANDIDATE', payload: { candidate: evt.candidate }, dst: self.peer }); } }; this.pc.oniceconnectionstatechange = function() { if (!!self.pc && self.pc.iceConnectionState === 'disconnected') { util.log('iceConnectionState is disconnected, closing connections to ' + this.peer); self.close(); } }; // Fallback for older Chrome impls. this.pc.onicechange = function() { if (!!self.pc && self.pc.iceConnectionState === 'disconnected') { util.log('iceConnectionState is disconnected, closing connections to ' + this.peer); self.close(); } }; }; /** Set up onnegotiationneeded. */ ConnectionManager.prototype._setupNegotiationHandler = function() { var self = this; util.log('Listening for `negotiationneeded`'); this.pc.onnegotiationneeded = function() { util.log('`negotiationneeded` triggered'); self._makeOffer(); }; }; /** Set up Data Channel listener. */ ConnectionManager.prototype._setupDataChannel = function() { var self = this; util.log('Listening for data channel'); this.pc.ondatachannel = function(evt) { util.log('Received data channel'); var dc = evt.channel; var label = dc.label; // This should not be empty. var options = self.labels[label] || {}; var connection = new DataConnection(self.peer, dc, options); self._attachConnectionListeners(connection); self.connections[label] = connection; self.emit('connection', connection); }; }; /** Send an offer. */ ConnectionManager.prototype._makeOffer = function() { var self = this; this.pc.createOffer(function(offer) { util.log('Created offer.'); // Firefox currently does not support multiplexing once an offer is made. self.firefoxSingular = true; if (util.browserisms === 'Webkit') { offer.sdp = Reliable.higherBandwidthSDP(offer.sdp); } self.pc.setLocalDescription(offer, function() { util.log('Set localDescription to offer'); self._socket.send({ type: 'OFFER', payload: { sdp: offer, config: self._options.config, labels: self.labels }, dst: self.peer }); // We can now reset labels because all info has been communicated. self.labels = {}; }, function(err) { self.emit('error', err); util.log('Failed to setLocalDescription, ', err); }); }); }; /** Create an answer for PC. */ ConnectionManager.prototype._makeAnswer = function() { var self = this; this.pc.createAnswer(function(answer) { util.log('Created answer.'); if (util.browserisms === 'Webkit') { answer.sdp = Reliable.higherBandwidthSDP(answer.sdp); } self.pc.setLocalDescription(answer, function() { util.log('Set localDescription to answer.'); self._socket.send({ type: 'ANSWER', payload: { sdp: answer }, dst: self.peer }); }, function(err) { self.emit('error', err); util.log('Failed to setLocalDescription, ', err); }); }, function(err) { self.emit('error', err); util.log('Failed to create answer, ', err); }); }; /** Clean up PC, close related DCs. */ ConnectionManager.prototype._cleanup = function() { util.log('Cleanup ConnectionManager for ' + this.peer); if (!!this.pc && (this.pc.readyState !== 'closed' || this.pc.signalingState !== 'closed')) { this.pc.close(); this.pc = null; } var self = this; this._socket.send({ type: 'LEAVE', dst: self.peer }); this.open = false; this.emit('close'); }; /** Attach connection listeners. */ ConnectionManager.prototype._attachConnectionListeners = function(connection) { var self = this; connection.on('close', function() { if (!!self.connections[connection.label]) { delete self.connections[connection.label]; } if (!Object.keys(self.connections).length) { self._cleanup(); } }); connection.on('open', function() { self._lock = false; self._processQueue(); }); }; /** Handle an SDP. */ ConnectionManager.prototype.handleSDP = function(sdp, type) { sdp = new RTCSessionDescription(sdp); var self = this; this.pc.setRemoteDescription(sdp, function() { util.log('Set remoteDescription: ' + type); if (type === 'OFFER') { self._makeAnswer(); } }, function(err) { self.emit('error', err); util.log('Failed to setRemoteDescription, ', err); }); }; /** Handle a candidate. */ ConnectionManager.prototype.handleCandidate = function(message) { var candidate = new RTCIceCandidate(message.candidate); this.pc.addIceCandidate(candidate); util.log('Added ICE candidate.'); }; /** Handle peer leaving. */ ConnectionManager.prototype.handleLeave = function() { util.log('Peer ' + this.peer + ' disconnected.'); this.close(); }; /** Closes manager and all related connections. */ ConnectionManager.prototype.close = function() { if (!this.open) { this.emit('error', new Error('Connections to ' + this.peer + 'are already closed.')); return; } var labels = Object.keys(this.connections); for (var i = 0, ii = labels.length; i < ii; i += 1) { var label = labels[i]; var connection = this.connections[label]; connection.close(); } this.connections = null; this._cleanup(); }; /** Create and returns a DataConnection with the peer with the given label. */ ConnectionManager.prototype.connect = function(options) { if (!this.open) { return; } options = util.extend({ label: 'peerjs', reliable: (util.browserisms === 'Firefox') }, options); // Check if label is taken...if so, generate a new label randomly. while (!!this.connections[options.label]) { options.label = 'peerjs' + this._default; this._default += 1; } this.labels[options.label] = options; var dc; if (!!this.pc && !this._lock) { var reliable = util.browserisms === 'Firefox' ? options.reliable : false; dc = this.pc.createDataChannel(options.label, { reliable: reliable }); if (util.browserisms === 'Firefox') { this._makeOffer(); } } var connection = new DataConnection(this.peer, dc, options); this._attachConnectionListeners(connection); this.connections[options.label] = connection; if (!this.pc || this._lock) { this._queued.push(connection); } this._lock = true return connection; }; /** Updates label:[serialization, reliable, metadata] pairs from offer. */ ConnectionManager.prototype.update = function(updates) { var labels = Object.keys(updates); for (var i = 0, ii = labels.length; i < ii; i += 1) { var label = labels[i]; this.labels[label] = updates[label]; } }; /** * An abstraction on top of WebSockets and XHR streaming to provide fastest * possible connection for peers. */ function Socket(host, port, key, id) { if (!(this instanceof Socket)) return new Socket(host, port, key, id); EventEmitter.call(this); this._id = id; var token = util.randomToken(); this.disconnected = false; var secure = util.isSecure(); var protocol = secure ? 'https://' : 'http://'; var wsProtocol = secure ? 'wss://' : 'ws://'; this._httpUrl = protocol + host + ':' + port + '/' + key + '/' + id + '/' + token; this._wsUrl = wsProtocol + host + ':' + port + '/peerjs?key='+key+'&id='+id+'&token='+token; }; util.inherits(Socket, EventEmitter); /** Check in with ID or get one from server. */ Socket.prototype.start = function() { this._startXhrStream(); this._startWebSocket(); }; /** Start up websocket communications. */ Socket.prototype._startWebSocket = function() { var self = this; if (!!this._socket) { return; } this._socket = new WebSocket(this._wsUrl); this._socket.onmessage = function(event) { var data; try { data = JSON.parse(event.data); } catch(e) { util.log('Invalid server message', event.data); return; } self.emit('message', data); }; // Take care of the queue of connections if necessary and make sure Peer knows // socket is open. this._socket.onopen = function() { if (!!self._timeout) { clearTimeout(self._timeout); setTimeout(function(){ self._http.abort(); self._http = null; }, 5000); } util.log('Socket open'); }; }; /** Start XHR streaming. */ Socket.prototype._startXhrStream = function(n) { try { var self = this; this._http = new XMLHttpRequest(); this._http._index = 1; this._http._streamIndex = n || 0; this._http.open('post', this._httpUrl + '/id?i=' + this._http._streamIndex, true); this._http.onreadystatechange = function() { if (this.readyState == 2 && !!this.old) { this.old.abort(); delete this.old; } if (this.readyState > 2 && this.status == 200 && !!this.responseText) { self._handleStream(this); } }; this._http.send(null); this._setHTTPTimeout(); } catch(e) { util.log('XMLHttpRequest not available; defaulting to WebSockets'); } }; /** Handles onreadystatechange response as a stream. */ Socket.prototype._handleStream = function(http) { // 3 and 4 are loading/done state. All others are not relevant. var messages = http.responseText.split('\n'); // Check to see if anything needs to be processed on buffer. if (!!http._buffer) { while (http._buffer.length > 0) { var index = http._buffer.shift(); var bufferedMessage = messages[index]; try { bufferedMessage = JSON.parse(bufferedMessage); } catch(e) { http._buffer.shift(index); break; } this.emit('message', bufferedMessage); } } var message = messages[http._index]; if (!!message) { http._index += 1; // Buffering--this message is incomplete and we'll get to it next time. // This checks if the httpResponse ended in a `\n`, in which case the last // element of messages should be the empty string. if (http._index === messages.length) { if (!http._buffer) { http._buffer = []; } http._buffer.push(http._index - 1); } else { try { message = JSON.parse(message); } catch(e) { util.log('Invalid server message', message); return; } this.emit('message', message); } } }; Socket.prototype._setHTTPTimeout = function() { var self = this; this._timeout = setTimeout(function() { var old = self._http; if (!self._wsOpen()) { self._startXhrStream(old._streamIndex + 1); self._http.old = old; } else { old.abort(); } }, 25000); }; Socket.prototype._wsOpen = function() { return !!this._socket && this._socket.readyState == 1; }; /** Exposed send for DC & Peer. */ Socket.prototype.send = function(data) { if (this.disconnected) { return; } if (!data.type) { this.emit('error', 'Invalid message'); return; } var message = JSON.stringify(data); if (this._wsOpen()) { this._socket.send(message); } else { var http = new XMLHttpRequest(); var url = this._httpUrl + '/' + data.type.toLowerCase(); http.open('post', url, true); http.setRequestHeader('Content-Type', 'application/json'); http.send(message); } }; Socket.prototype.close = function() { if (!this.disconnected && this._wsOpen()) { this._socket.close(); this.disconnected = true; } }; })(this);