diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee new file mode 100644 index 000000000..cba73f015 --- /dev/null +++ b/src/packages/collaboration/lib/collaboration.coffee @@ -0,0 +1,15 @@ +Peer = require './peer' + +createConnection = -> + peer = new Peer('some-id1', {host: 'ec2-54-218-51-127.us-west-2.compute.amazonaws.com', port: 8080}) + peer.on 'connection', (connection) -> + console.log 'connection' + connection.on 'data', (data) -> + console.log('Got data:', data) + +module.exports = + activate: -> + createConnection() + peer = new Peer('some-id2', {host: 'ec2-54-218-51-127.us-west-2.compute.amazonaws.com', port: 8080}) + c2 = peer.connect('some-id1') + c2.on 'open', -> c2.send('test?') diff --git a/src/packages/collaboration/lib/peer.js b/src/packages/collaboration/lib/peer.js new file mode 100644 index 000000000..fc63d2717 --- /dev/null +++ b/src/packages/collaboration/lib/peer.js @@ -0,0 +1,2136 @@ +/*! peerjs.js build:0.2.7, development. Copyright(c) 2013 Michelle Bu */ +var binaryFeatures = {}; +binaryFeatures.useBlobBuilder = false; + +binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){ + try { + return (new Blob([new Uint8Array([])])).size === 0; + } catch (e) { + return true; + } +})(); + +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); + } +}; +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() { + return true; + } +}; +/** + * 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) {}; + +if (window.mozRTCPeerConnection) { + util.browserisms = 'Firefox'; +} else if (window.webkitRTCPeerConnection) { + util.browserisms = 'Webkit'; +} else { + util.browserisms = 'Unknown'; +} + +RTCSessionDescription = window.RTCSessionDescription; +RTCPeerConnection = window.webkitRTCPeerConnection; +/** + * 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; + } + + // 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 url = 'http://' + 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; +}; + +/** + * 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; + + this._httpUrl = 'http://' + host + ':' + port + '/' + key + '/' + id + '/' + token; + this._wsUrl = 'ws://' + 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; + } +}; + +module.exports = Peer; diff --git a/src/packages/collaboration/package.cson b/src/packages/collaboration/package.cson new file mode 100644 index 000000000..7f0fee642 --- /dev/null +++ b/src/packages/collaboration/package.cson @@ -0,0 +1 @@ +'main': './lib/collaboration'