Browse Source

Merge pull request #78 from peers/av-rewrite

PeerJS refactor and A/V
Michelle Bu 11 years ago
parent
commit
38b21e1e3f

+ 58 - 36
README.md

@@ -1,57 +1,79 @@
-# PeerJS: Peer-to-peer data in the browser #
+# PeerJS: peer-to-peer in the browser #
 
-PeerJS provides a complete, configurable, and easy-to-use peer-to-peer data API built on top of WebRTC.   
-Each peer simply provides a identifier with which other peers using the same API key can connect.
+PeerJS provides a complete, configurable, and easy-to-use peer-to-peer API built on top of WebRTC, supporting both data channels and media streams.
 
-##[http://peerjs.com](http://peerjs.com)
+### [http://peerjs.com](http://peerjs.com)
+
+## Setup
 
 
 **Include the library**
 
-    <script src="http://cdn.peerjs.com/0/peer.js"></script>
+```html
+<script src="http://cdn.peerjs.com/0.3/peer.js"></script>
+```
 
-**Peer**
+**Create a Peer**  
+Get a [free API key](http://peerjs.com/peerserver). Your id only needs to be unique to the namespace of your API key.
+```javascript
+var peer = new Peer('pick-an-id', {key: 'myapikey'}); 
+// You can pick your own id or omit the id if you want to get a random one from the server.
+```
 
-```html
-<script>
-  var peer = new Peer('someid', {key: 'apikey'});
-  peer.on('connection', function(conn) {
-    conn.on('data', function(data){
-      // Will print 'hi!'
-      console.log(data);
-    });
+## Data connections
+**Connect**
+```javascript
+var conn = peer.connect('another-peers-id');
+conn.on('open', function(){
+  conn.send('hi!');
+});
+```
+**Receive**
+```javascript
+peer.on('connection', function(conn) {
+  conn.on('data', function(data){
+    // Will print 'hi!'
+    console.log(data);
   });
-</script>
+});
 ```
 
-**Connecting peer**
+## Media calls
+**Call**
+```javascript
+var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+getUserMedia({video: true, audio: true}, function(stream) {
+  var call = peer.call('another-peers-id', stream);
+  call.on('stream', function(remoteStream) {
+    // Show stream in some <video> element.
+  });
+}, function(err) {
+  console.log('Failed to get local stream' ,err);
+});
 
-```html
-<script>
-  var peer = new Peer('anotherid', {key: 'apikey'});
-  var conn = peer.connect('someid');
-  conn.on('open', function(){
-    conn.send('hi!');
-  }); 
-</script>
 ```
+**Answer**
+```javascript
+var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+peer.on('call', function(call) {
+  getUserMedia({video: true, audio: true}, function(stream) {
+    call.answer(stream); // Answer the call with an A/V stream.
+    call.on('stream', function(remoteStream) {
+      // Show stream in some <video> element.
+    });
+  }, function(err) {
+    console.log('Failed to get local stream' ,err);
+  });
+});
+```
+## Links
 
-### [Getting started](http://peerjs.com/start)
-
-### [API reference](https://github.com/peers/peerjs/blob/master/docs/api.md)
+### [Documentation / API Reference](http://peerjs.com/docs)
 
-### [Browser compatibility status](http://peerjs.com/status)
+### [WebRTC Browser compatibility status](http://peerjs.com/status)
 
 ### [PeerServer](https://github.com/peers/peerjs-server)
 
 ### [Discuss PeerJS on our Google Group](https://groups.google.com/forum/?fromgroups#!forum/peerjs)
 
 ### [Changelog](https://github.com/peers/peerjs/blob/master/changelog.md)
-
-
-## In the future
-
-* Tests
-* Video/audio support (in progress)
-
-

+ 3 - 2
bin/build.js

@@ -41,12 +41,13 @@ var base = [
     '../deps/js-binarypack/lib/bufferbuilder.js'
   , '../deps/js-binarypack/lib/binarypack.js'
   , '../deps/EventEmitter/EventEmitter.js'
-  , 'util.js'
   , '../deps/reliable/lib/reliable.js'
   , 'adapter.js' 
+  , 'util.js'
   , 'peer.js'
   , 'dataconnection.js'
-  , 'connectionmanager.js'
+  , 'mediaconnection.js'
+  , 'negotiator.js'
   , 'socket.js'
 
 ];

+ 15 - 0
changelog.md

@@ -1,5 +1,20 @@
 # PeerJS Changelog
 
+## Version 0.3.0 beta (20 Sept 2013)
+
+### Highlights
+* Support for WebRTC video and audio streams in both Firefox and Chrome.
+* Add `util.supports.[FEATURE]` flags, which represent the WebRTC features
+  supported by your browser.
+* **Breaking:** Deprecate current `Peer#connections` format. Connections will no longer be
+  keyed by label and will instead be in a list.
+
+### Other changes
+* **Breaking:** Deprecate `Peer.browser` in favor of `util.browser`.
+* Additional logging levels (warnings, errors, all).
+* Additional logging functionality (`logFunction`).
+* SSL option now in config rather than automatic.
+
 ## Version 0.2.8 (1 July 2013)
 * Fix bug, no error on Firefox 24 due to missing error callback.
 * TLS secure PeerServers now supported.

File diff suppressed because it is too large
+ 601 - 393
dist/peer.js


File diff suppressed because it is too large
+ 0 - 0
dist/peer.min.js


+ 464 - 0
docs/api.json

@@ -0,0 +1,464 @@
+[
+  {
+    "name": "Peer",
+    "type": "constructor",
+    "snippet": "var peer = new Peer([id], [options]);",
+    "description": "A peer can connect to other peers and listen for connections.",
+    "children": [
+      {
+        "name": "id",
+        "optional": true,
+        "type": "string",
+        "description": "Other peers can connect to this peer using the provided ID. If no ID is given, one will be generated by the brokering server.<span class='warn'>It's not recommended that you use this ID to identify peers, as it's meant to be used for brokering connections only. You're recommended to set the <a href='#peerconnect-options'><code>metadata</code></a> option to send other identifying information.</span>"
+      },
+      {
+        "name": "options",
+        "optional": true,
+        "type": "object",
+        "children": [
+          {
+            "name": "key",
+            "type": "string",
+            "description": "API key for the cloud PeerServer. This is not used for servers other than <code>0.peerjs.com</code>."
+          },
+          {
+            "name": "host",
+            "type": "string",
+            "description": "Server host. Defaults to <code>0.peerjs.com</code>. Also accepts <code>'/'</code> to signify relative hostname."
+          },
+          {
+            "name": "port",
+            "type": "number",
+            "description": "Server port. Defaults to <code>80</code>."
+          },
+          {
+            "name": "secure",
+            "type": "boolean",
+            "tags": ["beta (0.3.0)"],
+            "description": "<code>true</code> if you're using SSL.<span class='tip'>Note that our cloud-hosted server and assets may not support SSL.</span>"
+          },
+          {
+            "name": "config",
+            "type": "object",
+            "description": "Configuration hash passed to RTCPeerConnection. This hash contains any custom ICE/TURN server configuration. Defaults to <code>{ 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] }</code>"
+          },
+          {
+            "name": "debug",
+            "type": "number",
+            "tags": ["beta (0.3.0)"],
+            "description": "Prints log messages depending on the debug level passed in. Defaults to <code>0</code>.",
+            "children": [
+              {
+                "name": 0,
+                "description": "Prints no logs."
+              },
+              {
+                "name": 1,
+                "description": "Prints only errors."
+              },
+              {
+                "name": 2,
+                "description": "Prints errors and warnings."
+              },
+              {
+                "name": 3,
+                "description": "Prints all logs."
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+
+  {
+    "name": "peer.connect",
+    "type": "method",
+    "snippet": "var <a href='#dataconnection'>dataConnection</a> = peer.connect(id, [options]);",
+    "description": "Connects to the remote peer specified by <code>id</code> and returns a data connection. Be sure to listen on the <a href='#peeron-error'><code>error</code></a> event in case the connection fails.",
+    "children": [
+      {
+        "name": "id",
+        "type": "string",
+        "description": "The brokering ID of the remote peer (their <a href='#peerid'><code>peer.id</code></a>)."
+      },
+      {
+        "name": "options",
+        "optional": true,
+        "type": "object",
+        "children": [
+          {
+            "name": "label",
+            "type": "string",
+            "description": "A unique label by which you want to identify this data connection. If left unspecified, a label will be generated at random. Can be accessed with <a href='#dataconnection-label'><code>dataConnection.label</code></a>."
+          },
+          {
+            "name": "metadata",
+            "description": "Metadata associated with the connection, passed in by whoever initiated the connection. Can be accessed with <a href='#dataconnection-metadata'><code>dataConnection.metadata</code></a>. Can be any serializable type."
+          },
+          {
+            "name": "serialization",
+            "type": "string",
+            "description": "Can be <code>binary</code> (default), <code>binary-utf8</code>, <code>json</code>, or <code>none</code>. Can be accessed with <a href='#dataconnection-serialization'><code>dataConnection.serialization</code></a>.<span class='tip'><code>binary-utf8</code> will take a performance hit because of the way UTF8 strings are packed into binary format.</span>"
+          },
+          {
+            "name": "reliable",
+            "type": "boolean",
+            "description": "Whether the underlying data channels should be reliable (e.g. for large file transfers) or not (e.g. for gaming or streaming). Defaults to <code>true</code>."
+          }
+        ]
+      }
+    ]
+  },
+
+  {
+    "name": "peer.call",
+    "tags": ["beta (0.3.0)"],
+    "type": "method",
+    "snippet": "var <a href='#mediaconnection'>mediaConnection</a> = peer.call(id, stream);",
+    "description": "Calls the remote peer specified by <code>id</code> and returns a media connection. Be sure to listen on the <a href='#peeron-error'><code>error</code></a> event in case the connection fails.",
+    "children": [
+      {
+        "name": "id",
+        "type": "string",
+        "description": "The brokering ID of the remote peer (their <a href='#peerid'><code>peer.id</code></a>)."
+      },
+      {
+        "name": "stream",
+        "type": "MediaStream",
+        "description": "Something else"
+      }
+    ]
+  },
+
+  {
+    "name": "peer.on",
+    "type": "method",
+    "snippet": "peer.on(event, callback);",
+    "description": "Set listeners for peer events.",
+    "children": [
+      {
+        "name": "'open'",
+        "type": "event",
+        "snippet": "peer.on('open', function(id) { ... });",
+        "description": "Emitted when a connection to the PeerServer is established. You may use the peer before this is emitted, but messages to the server will be queued. <code>id</code> is the brokering ID of the peer (which was either provided in the constructor or assigned by the server).<span class='tip'>You should not wait for this event before connecting to other peers if connection speed is important.</span>"
+      },
+      {
+        "name": "'connection'",
+        "type": "event",
+        "snippet": "peer.on('connection', function(<a href='#dataconnection'>dataConnection</a>) { ... });",
+        "description": "Emitted when a new data connection is established from a remote peer."
+      },
+      {
+        "name": "'call'",
+        "type": "event",
+        "tags": ["beta (0.3.0)"],
+        "snippet": "peer.on('call', function(<a href='#mediaconnection'>mediaConnection</a>) { ... });",
+        "description": "Emitted when a remote peer attempts to call you. The emitted <code>mediaConnection</code> is not yet active; you must first answer the call (<a href='#mediaconnection-answer'><code>mediaConnection.answer([stream]);</code></a>). Then, you can listen for the <a href='#mediaconnection-on'><code>stream</code></a> event."
+      },
+      {
+        "name": "'close'",
+        "type": "event",
+        "snippet": "peer.on('close', function() { ... });",
+        "description": "Emitted when the peer is <a href='#peerdestroy'>destroyed</a>.<span class='tip'>To be extra certain that peers clean up correctly, we recommend calling <code>peer.destroy()</code> on a peer when it is no longer needed.</span>"
+      },
+      {
+        "name": "'error'",
+        "type": "event",
+        "snippet": "peer.on('error', function(err) { ... });",
+        "description": "Errors on the peer are <strong>almost always fatal</strong> and will destroy the peer. Errors from the underlying socket and PeerConnections are forwarded here.<br><br>These come in the following <code>err.type</code> flavors:",
+        "children": [
+          {
+            "name": "'browser-incompatible'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "The client's browser does not support some or all WebRTC features that you are trying to use."
+          },
+          {
+            "name": "'invalid-id'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "The ID passed into the Peer constructor contains illegal characters."
+          },
+          {
+            "name": "'invalid-key'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only)."
+          },
+          {
+            "name": "'unavailable-id'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "The ID passed into the Peer constructor is already taken."
+          },
+          {
+            "name": "'ssl-unavailable'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer."
+          },
+          {
+            "name": "'server-disconnected'",
+            "type": "Error",
+            "description": "You've already disconnected this peer and can no longer make any new connections on it."
+          },
+          {
+            "name": "'server-error'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "Unable to reach the server."
+          },
+          {
+            "name": "'socket-error'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "An error from the underlying socket."
+          },
+          {
+            "name": "'socket-closed'",
+            "type": "Error",
+            "tags": ["fatal"],
+            "description": "The underlying socket closed unexpectedly."
+          }
+        ]
+      }
+    ]
+  },
+
+  {
+    "name": "peer.disconnect",
+    "type": "method",
+    "snippet": "peer.disconnect();",
+    "description": "Close the connection to the server, leaving all existing data and media connections intact. <a href='#peerdisconnected'><code>peer.disconnected</code></a> will be set to <code>true</code>.<span class='warn'>This cannot be undone; the respective peer object will no longer be able to create or receive any connections and its ID will be forfeited on the (cloud) server.</span>"
+  },
+
+  {
+    "name": "peer.destroy",
+    "type": "method",
+    "snippet": "peer.destroy();",
+    "description": "Close the connection to the server and terminate all existing connections. <a href='#peerdestroyed'><code>peer.destroyed</code></a> will be set to <code>true</code>.<span class='warn'>This cannot be undone; the respective peer object will no longer be able to create or receive any connections, its ID will be forfeited on the (cloud) server, and all of its data and media connections will be closed.</span>"
+  },
+
+  {
+    "name": "peer.id",
+    "type": "string",
+    "description": "The brokering ID of this peer. If no ID was specified in <a href='#peer'>the constructor</a>, this will be <code>undefined</code> until the <a href='#peeron-open'><code>open</code></a> event is emitted."
+  },
+
+  {
+    "name": "peer.connections",
+    "type": "object",
+    "description": "A hash of all connections associated with this peer, keyed by the remote peer's ID.<span class='tip'>We recommend keeping track of connections yourself rather than relying on this hash.</span>"
+  },
+
+  {
+    "name": "peer.disconnected",
+    "type": "boolean",
+    "description": "<code>false</code> if there is an active connection to the PeerServer."
+  },
+
+  {
+    "name": "peer.destroyed",
+    "type": "boolean",
+    "description": "<code>true</code> if this peer and all of its connections can no longer be used."
+  },
+
+  {
+    "name": "DataConnection",
+    "type": "class",
+    "description": "Wraps WebRTC's DataChannel. To get one, use <a href='#peerconnect'><code>peer.connect</code></a> or listen for the <a href='#peeron-connect'><code>connect</code></a> event.<span class='tip'>Because Chrome currently does not support reliable messaging, PeerJS uses the <a href='https://github.com/michellebu/reliable'>Reliable shim</a> when necessary. A caveat is that with the shim you will not be able to customize <code>serialization</code> when the shim is used.</span>",
+    "children": [
+      {
+        "name": ".send",
+        "type": "method",
+        "snippet": "dataConnection.send(data);",
+        "description": "<code>data</code> is serialized by BinaryPack by default and sent to the remote peer.",
+        "children": {
+          "name": "data",
+          "description": "You can send any type of data, including objects, strings, and blobs."
+        }
+      },
+      {
+        "name": ".close",
+        "type": "method",
+        "snippet": "dataConnection.close();",
+        "description": "Closes the data connection gracefully, cleaning up underlying DataChannels and PeerConnections."
+      },
+      {
+        "name": ".on",
+        "type": "method",
+        "snippet": "dataConnection.on(event, callback);",
+        "description": "Set listeners for data connection events.",
+        "children": [
+          {
+            "name": "'data'",
+            "type": "event",
+            "snippet": "dataConnection.on('data', function(data) { ... });",
+            "description": "Emitted when data is received from the remote peer."
+          },
+          {
+            "name": "'open'",
+            "type": "event",
+            "snippet": "dataConnection.on('data', function() { ... });",
+            "description": "Emitted when the connection is established and ready-to-use."
+          },
+          {
+            "name": "'close'",
+            "type": "event",
+            "snippet": "dataConnection.on('close', function() { ... });",
+            "description": "Emitted when either you or the remote peer closes the data connection."
+          },
+          {
+            "name": "'error'",
+            "type": "event",
+            "snippet": "dataConnection.on('error', function(err) { ... });"
+          }
+        ]
+      },
+      {
+        "name": ".label",
+        "type": "string",
+        "description": "The optional label passed in or assigned by PeerJS when the connection was initiated."
+      },
+      {
+        "name": ".metadata",
+        "description": "Any type of metadata associated with the connection, passed in by whoever initiated the connection."
+      },
+      {
+        "name": ".serialization",
+        "type": "string",
+        "description": "The serialization format of the data sent over the connection. Can be <code>binary</code> (default), <code>binary-utf8</code>, <code>json</code>, or <code>none</code>."
+      },
+      {
+        "name": ".open",
+        "type": "boolean",
+        "description": "This is true if the connection is open and ready for read/write."
+      },
+      {
+        "name": ".peer",
+        "type": "string",
+        "description": "The ID of the peer on the other end of this connection."
+      },
+      {
+        "name": ".type",
+        "type": "string",
+        "description": "For data connections, this is always <code>'data'</code>."
+      }
+    ]
+  },
+
+  {
+    "name": "MediaConnection",
+    "type": "class",
+    "tags": ["beta (0.3.0)"],
+    "description": "Wraps WebRTC's media streams. To get one, use <a href='#peercall'><code>peer.call</code></a> or listen for the <a href='#peeron-call'><code>call</code></a> event.",
+    "children": [
+      {
+        "name": ".answer",
+        "type": "method",
+        "snippet": "mediaConnection.answer([stream]);",
+        "description": "When recieving a <a href='#peeron-call'><code>call</code></a> event on a peer, you can call <code>.answer</code> on the media connection provided by the callback to accept the call and optionally send your own media stream.",
+        "children": {
+          "name": "stream",
+          "optional": true,
+          "type": "MediaStream",
+          "description": "A WebRTC media stream from <a href='https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getUserMedia'><code>getUserMedia</code></a>."
+        }
+      },
+      {
+        "name": ".close",
+        "type": "method",
+        "snippet": "mediaConnection.close();",
+        "description": "Closes the media connection."
+      },
+      {
+        "name": ".on",
+        "type": "method",
+        "snippet": "mediaConnection.on(event, callback);",
+        "description": "Set listeners for media connection events.",
+        "children": [
+          {
+            "name": "'stream'",
+            "type": "event",
+            "snippet": "mediaConnection.on('stream', function(stream) { ... });",
+            "description": "Emitted when a remote peer adds a <code>stream</code>."
+          },
+          {
+            "name": "'close'",
+            "type": "event",
+            "snippet": "mediaConnection.on('close', function() { ... });",
+            "description": "Emitted when either you or the remote peer closes the media connection."
+          },
+          {
+            "name": "'error'",
+            "type": "event",
+            "snippet": "mediaConnection.on('error', function(err) { ... });"
+          }
+        ]
+      },
+      {
+        "name": ".open",
+        "type": "boolean",
+        "description": "Whether the media connection is active (e.g. your call has been answered). You can check this if you want to set a maximum wait time for a one-sided call."
+      },
+      {
+        "name": ".metadata",
+        "description": "Any type of metadata associated with the connection, passed in by whoever initiated the connection."
+      },
+      {
+        "name": ".peer",
+        "type": "string",
+        "description": "The ID of the peer on the other end of this connection."
+      },
+      {
+        "name": ".type",
+        "type": "string",
+        "description": "For media connections, this is always <code>'media'</code>."
+      }
+    ]
+  },
+
+  {
+    "name": "util",
+    "type": "object",
+    "tags": ["utility"],
+    "description": "Provides a variety of helpful utilities.<span class='warn'>Only the utilities documented here are guaranteed to be present on <code>util</code>. Undocumented utilities can be removed without warning. We don't consider these to be 'breaking changes.'</span>",
+    "children": [
+      {
+        "name": ".browser",
+        "type": "string",
+        "snippet": "if (util.browser === 'Firefox') { /* OK to peer with Firefox peers. */ }",
+        "description": "The current browser. This property can be useful in determining whether or not two peers can connect. For example, as of now data connections are not yet interoperable between major browsers. <code>util.browser</code> can currently have the values 'Firefox', 'Chrome', 'Unsupported', or 'Supported' (unknown WebRTC-compatible browser)."
+      },
+      {
+        "name": ".supports",
+        "type": "object",
+        "snippet": "if (util.supports.data) { /* OK to start a data connection. */ }",
+        "description": "A hash of WebRTC features mapped to booleans that correspond to whether the feature is supported by the current browser.<span class='warn'>Only the properties documented here are guaranteed to be present on <code>util.supports</code>.</span>",
+        "children": [
+          {
+            "name": ".audioVideo",
+            "type": "boolean",
+            "description": "True if the current browser supports media streams and PeerConnection."
+          },
+          {
+            "name": ".data",
+            "type": "boolean",
+            "description": "True if the current browser supports DataChannel and PeerConnection."
+          },
+          {
+            "name": ".binary",
+            "type": "boolean",
+            "description": "True if the current browser supports binary DataChannels."
+          },
+          {
+            "name": ".reliable",
+            "type": "boolean",
+            "description": "True if the current browser supports reliable DataChannels."
+          }
+        ]
+      }
+    ]
+  }
+]

+ 2 - 236
docs/api.md

@@ -1,238 +1,4 @@
 # PeerJS API Reference
 
-**Due to browsers' incomplete support of the WebRTC DataChannel specification, many features of PeerJS have caveats.
-[View the status page for full details](http://peerjs.com/status).**
-
-- [Class: peerjs.Peer](#class-peerjspeer)
-  - [new Peer([id], [options])](#new-peerid-options)
-  - [Peer.browser](#peerbrowser)
-  - [peer.id](#peerid)
-  - [peer.connections](#peerconnections)
-  - [peer.connect(id, [options])](#peerconnectid-options)
-  - [peer.destroy()](#peerdestroy)
-  - [peer.disconnect()](#peerdisconnect)
-  - [peer.disconnected](#peerdisconnected)
-  - [peer.destroyed](#peerdestroyed)
-  - [Event: 'connection'](#event-connection)
-  - [Event: 'open'](#event-open)
-  - [Event: 'error'](#event-error)
-  - [Event: 'close'](#event-close)
-- [Class: peerjs.DataConnection](#class-peerjsdataconnection)
-  - [EXPERIMENTAL reliable and large file transfer:](#experimental-reliable-and-large-file-transfer)
-  - [connection.peer](#connectionpeer)
-  - [connection.open](#connectionopen)
-  - [connection.metadata](#connectionmetadata)
-  - [connection.label](#connectionlabel)
-  - [connection.serialization](#connectionserialization)
-  - [connection.send(data)](#connectionsenddata)
-  - [connection.close()](#connectionclose)
-  - [Event: 'data'](#event-data)
-  - [Event: 'open'](#event-open-1)
-  - [Event: 'error'](#event-error-1)
-  - [Event: 'close'](#event-close-1)
-
-## Class: peerjs.Peer
-
-This class is a Peer, which can connect to other peers and listen for connections. It is an `EventEmitter`.
-
-### new Peer([id], [options])
-
-* `id` String. The id by which this peer will be identified when other peers try to connect to it. If no id is given, one will be generated by the server. **Note that this ID is mainly used for brokering and that it is not recommended that you use it to identify peers. Also note that you can set a `metadata` option to send other identifying information.**
-* `options` Object
-  * `key` String. API key for cloud PeerServer. Is not used for servers other than `cloud.peerjs.com`
-  * `host` String. Server host. Default `cloud.peerjs.com`. Also accepts `'/'` for relative hostname.
-  * `port` Number. Server port. Default `80`
-  * `config` Object. Configuration hash passed to `RTCPeerConnection`. This hash contains the ICE servers. Default `{ 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] }`
-  * `debug` Boolean. Prints verbose log messages. Default `false` 
-
-Construct a new Peer object.
-
-The Peer object is used to connect to other Peer clients and also to receive connections from other clients.
-
-The first argument is the id that other peers will use to connect to this peer, thus it must be unique for the given `key` (if you're using PeerServer cloud) or server.
-
-In the options, either a PeerServer Cloud `key` must be provided or `host` and `port` for your own PeerServer. **Note that the server is only for brokering connections and does not proxy data between peers.**
-
-The `config` object is passed straight into instances of `RTCPeerConnection`. For compatibility with symmetric NATs, you can provide your own TURN server. By default the STUN server provided by Google is used.
-
-### Peer.browser
-
-The type of browser the client is on. Currently WebRTC DataChannels are not
-interoperable so different browser types should not be connected.
-
-### peer.id
-
-The given id of this peer.
-
-If no id was specified in the constructor, this value will be `undefined` util the `open` event fires.
-
-### peer.connections
-
-A hash of all current connections with the current peer. Keys are ids and values are hashes of label => `DataConnection` pairs.
-
-**You are recommended to keep track of connections yourself rather than to manipulate this hash.**
-
-### peer.connect(id, [options])
-
-Connects to the remote peer specified by `id`.
-
-This function can be called multiple times for multiplexed connections between
-two peers. In Firefox however this functionality is only available before the
-first connection is established.
-
-Returns a `DataConnection` object.
-
-* `id` String. The id of the remote peer to connect to.
-* `options` Object.
-  * `label` Optional label for the underlying DataChannel, to differentiate between DataConnections between the same two peers. If left unspecified, a label will be assigned at random.
-  * `metadata` Optional metadata to pass to the remote peer. Can be any serializable type.
-  * `serialization` String, which can be `binary`, `binary-utf8`, `json`, or `none`. This will be the serialization format of all data sent over the P2P DataConnection. Defaults to `binary`.
-  * `reliable` Boolean, which if `true` activates experimental reliable transfer in Chrome (while waiting for actual reliable transfer to be implemented in Chrome). Defaults to `false` until Chrome implements reliable/large data transfer. Defaults to true in Firefox.
-
-Before writing to / data will be emitted from the `DataConnection` object that is returned, the `open` event must fire. Also the `error` event should be checked in case a connection cannot be made.
-
-### peer.destroy()
-
-Close the connection to the server and terminate all connections.
-
-**Warning: This cannot be undone; the respective peer object will no longer be able to create or receive any connections and its ID will be forfeited on the (cloud) server.**
-
-### peer.disconnect()
-
-Close the connection to the server, leaving all existing DataConnections intact.
-
-**Warning: This cannot be undone; the respective peer object will no longer be able to create or receive any connections and its ID will be forfeited on the (cloud) server.**
-
-### peer.disconnected
-
-Is false if there is an active connection to the PeerServer.
-
-### peer.destroyed
-
-Is true if this peer is destroyed and can no longer be used.
-
-### Event: 'connection'
-
-`function (connection, meta) { }`
-
-When a new connection is established from another peer to this peer, the `DataConnection` object is emitted with this event. The `meta` argument contains whatever metadata values passed into `peer.connection(...)` by the remote peer.
-
-**Note:** the `open` event must fire on the `DataConnection` before it is ready to read/write.
-
-### Event: 'open'
-
-`function(id) { }`
-
-Fired when the PeerServer connection is succesfully, fully, open.
-This event does not need to fire before creating or receiving connections.
-**You should not wait for open before connecting to other peers or expecting to receive connections if connection speed is important.**
-
-`id` is the id of this `Peer` object, either provided in the constructor, or generated automatically by the PeerServer.
-
-### Event: 'error'
-
-`function (error) { }`
-
-Emitted when an unexpected event occurs. Errors on the Peer are **always
-fatal**. Errors from the underlying socket and PeerConnections are forwarded here.
-
-The `error` object also has a `type` parameter that may be helpful in responding to client errors properly:
-* `browser-incompatible`: The client's browser does not support some or all WebRTC features that you are trying to use.
-* `invalid-id`: The ID passed into the Peer constructor contains illegal characters.
-* `invalid-key`: The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only).
-* `unavailable-id`: The ID passed into the Peer constructor is already taken.
-* `firefoxism`: The operation you're trying to perform is not supported in firefox.
-* `ssl-unavailable`: PeerJS is being used securely, but the cloud server does not support SSL. Use a custom PeerServer.
-* Errors types that shouldn't regularly appear:
-  * `server-error`: Unable to reach the server.
-  * `socket-error`: An error from the underlying socket.
-  * `socket-closed`: The underlying socket closed unexpectedly.
-* (The Peer object is destroyed after one of the errors above are emitted.)
-* `server-disconnected`: A Peer that has been disconnected is being used to try to connect.
-
-### Event: 'close'
-
-`function () { }`
-
-Emitted when the Peer object has closed its connection with PeerServer so no more remote peer connections can be made or received.
-
-To be extra certain that Peer objects clean up cleanly (and because it takes the WS server and DataChannel some time to realize that a Peer has disconnected), it is best to call `destroy()` on a Peer when it is no longer needed.
-
-## Class: peerjs.DataConnection
-
-This class is the interface two communicate between two peers. It is an `EventEmitter`.
-
-There is no constructor. A `DataConnection` object must be obtained in the callback of `peer.connect(...)` when initiating a peer-to-peer connection or emitted in the `peer.on('connection', ...)` event when receiving a connection.
-
-### EXPERIMENTAL reliable and large file transfer:
-
-(CHROME ONLY. Firefox has reliable transport built in and reliable transfer is the default option.) Simply pass in `reliable: true` when calling `.connect(...)`. This module is experimental, temporary, and exists here: https://github.com/michellebu/reliable
-
-*Caveat:* With reliable enabled in Chrome, you can no longer customize the serialization format used for data transfer.
-
-### connection.peer
-
-The id of the remote peer this connection is connected to.
-
-### connection.open
-
-Whether the connection is open (ready for read and write).
-
-### connection.metadata
-
-The metadata passed in when the connection was created with `peer.connect(...)`.
-
-### connection.label
-
-The optional label passed in or assigned by PeerJS when the connection was created with `peer.connect(...)`.
-
-### connection.serialization
-
-The serialization format of the connection. Can be `binary`, `binary-utf8`, `json`, or `none`
-for no serialization. Default serialization format is `binary`.
-
-**Note:** `binary-utf8` will take a performance hit because of the way utf8 strings are packed into binary.
-
-### connection.send(data)
-
-Accepts data of any JSON type or binary type.
-
-To configure which serialization format to use, specify `binary`, `binary-utf8`, `json`, or `none` as the `serialization` property of the `options` object in `peer.connect(...)`.
-
-Data is serialized using BinaryPack (`binary`) by default and then sent to the remote peer.
-
-### connection.close()
-
-Gracefully closes the connection.
-
-### Event: 'data'
-
-`function (data) { }`
-
-Emitted when data is received from the remote peer. 
-
-The `data` parameter contains values exactly as put into the `connection.send(...)`. Binary types will have been deserialized to `ArrayBuffer`.
-
-### Event: 'open'
-
-`function () { }`
-
-Emitted when the connection is established and ready for writing. `data` from the remote peer will also start to be emitted.
-
-### Event: 'error'
-
-`function (error) { }`
-
-If the connection emits an error, this event is emitted (errors from the underlying `DataChannel` are forwarded here).
-
-`error` is an `Error` object.
-
-### Event: 'close'
-
-`function () { }`
-
-Is emitted when the connection is closed.
-
-The `close` event is also emitted when the remote peer closes the connection.
-
-
+We've moved! <a href="http://peerjs.com/docs#api">Check out our new API
+reference.</a>

+ 8 - 0
docs/build.js

@@ -0,0 +1,8 @@
+var fs = require('fs');
+var handlebars = require('handlebars');
+var reference = require('reference');
+
+var file = fs.readFileSync('./api.json');
+
+var template = handlebars.compile(fs.readFileSync('./template.html', {encoding: 'utf8'}));
+fs.writeFile('./index.html', template({html: reference(file, {anchor: true})}));

File diff suppressed because it is too large
+ 170 - 0
docs/index.html


+ 79 - 0
docs/index.js

@@ -0,0 +1,79 @@
+$(document).ready(function() {
+  var $api = $('.api');
+  var $start = $('.start');
+  var $show = $('.left .show');
+  var $hide = $('.left .hide');
+  var width = $(window).width();
+  var height = $(window).height();
+  var THRESHOLD = 700;
+
+  init();
+
+  $(window).on('resize', function() {
+    width = $(window).width();
+    height = $(window).height();
+
+    init();
+  });
+
+  var hash = window.location.hash;
+  if (hash === '#start' && width < THRESHOLD) {
+    hideAPI();
+  }
+
+
+  function init() {
+    if (width < THRESHOLD) {
+      $api.addClass('fullscreen');
+      $start.addClass('full');
+      $show.hide();
+      $hide.hide();
+    } else {
+      $start.removeClass('full');
+      $api.removeClass('fullscreen');
+      $show.show();
+      $hide.show();
+    }
+
+    if ($api.attr('class').indexOf('hidden') === -1) {
+      showAPI();
+    } else {
+      hideAPI();
+    }
+  }
+
+  function hideAPI() {
+    $api.addClass('hidden');
+    if (width >= THRESHOLD) {
+      $start.addClass('full');
+      $hide.hide();
+      $show.show();
+    }
+  }
+
+  function showAPI() {
+    if (width >= THRESHOLD) {
+      $start.removeClass('full');
+      $show.hide();
+      $hide.show();
+    }
+    $api.removeClass('hidden');
+  }
+
+  $('body').on('click', '.left', function() {
+    if ($api.attr('class').indexOf('hidden') !== -1) {
+      showAPI();
+    } else if ($api.attr('class').indexOf('fullscreen') === -1) {
+      // Now the headers are only links.
+      hideAPI();
+    }
+  });
+  $('body').on('click', '.right', function() {
+    hideAPI();
+  });
+  $('body').on('click', 'a', function() {
+    if ($(this).attr('href').indexOf('#') === 0) {
+      showAPI();
+    }
+  });
+});

+ 17 - 0
docs/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "docs",
+  "version": "0.0.0",
+  "description": "**Due to browsers' incomplete support of the WebRTC DataChannel specification, many features of PeerJS have caveats.\r [View the status page for full details](http://peerjs.com/status).**",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": "",
+  "author": "",
+  "license": "BSD",
+  "readmeFilename": "readme.md",
+  "dependencies": {
+    "reference": "0.0.0",
+    "handlebars": "~1.0.12"
+  }
+}

+ 2 - 6
docs/readme.md

@@ -1,11 +1,7 @@
 ## PeerJS Documentation
 
-**Due to browsers' incomplete support of the WebRTC DataChannel specification, many features of PeerJS have caveats.
-[View the status page for full details](http://peerjs.com/status).**
-
-###[API Reference](https://github.com/peers/peerjs/blob/master/docs/api.md)
-
-###[Getting started](http://peerjs.com/start)
+We've moved! <a href="http://peerjs.com/docs">Check out our new
+documentation.</a>
 
 ###[Browser compatibility and limitations](http://peerjs.com/status)
 

+ 393 - 0
docs/style.css

@@ -0,0 +1,393 @@
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{font-size:100%;}
+a {outline: none;} /* Gets rid of Firefox's dotted borders */
+a img {border: none;} /* Gets rid of IE's blue borders */
+
+
+body, html {
+  font-size: 14px;
+  line-height: 24px;
+  font-family: "Lato", Helvetica, sans-serif;
+  color: #454545;
+}
+
+header.right, header.left {
+  cursor: pointer;
+  z-index: 100;
+  position: fixed;
+  top: 0;
+  height: 35px;
+  box-sizing: border-box;
+  -moz-box-sizing: border-box;
+}
+
+header.left {
+  border-bottom: 1px solid rgba(0,0,0,0.1);
+  border-right: 4px solid rgba(0,0,0,0.1);
+  background-color: #50484e;
+  text-align: right;
+  left: 0;
+  width: 47%;
+}
+header.right {
+  border-bottom: 1px solid rgba(0,0,0,0.1);
+  border-left: 4px solid rgba(0,0,0,0.1);
+  background-color: #eee;
+  text-align: left;
+  right: 0;
+  width: 53%;
+}
+
+.left h2, .right h2 {
+  color: #E2A62E;
+  letter-spacing: 1px;
+  text-transform: uppercase;
+  font-size: 14px;
+  margin: 0;
+  padding: 6px 10px;
+  text-shadow: 0 -1px 0 rgba(0,0,0,0.2);
+}
+
+.right h2 {
+  color: #5a5157;
+  text-shadow: 0 -1px 0 #fff;
+}
+
+.left .icon {
+  color: #E2A62E;
+  cursor: pointer;
+  font-family: monospace;
+  font-size: 20px;
+  display: inline-block;
+  margin-left: 5px;
+}
+
+.icon.show {
+  display: none;
+}
+
+
+.api, .start {
+  position: absolute;
+  top: 0px;
+  bottom: 0px;
+  overflow-y: scroll;
+  overflow-x: hidden;
+  box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  transition: left 300ms;
+}
+
+.api.fullscreen {
+  width: 100%;
+}
+
+.start.full {
+  left: 30px;
+  width: auto;
+}
+
+/** hiding */
+.api.hidden {
+  left: -370px;
+  width: 400px;
+  overflow: hidden;
+}
+
+.api.hidden > div {
+  opacity: 0.6;
+}
+
+.start {
+  background-color: #fcfcfc;
+  width: 53%;
+  right: 0px;
+  top: 35px;
+  text-shadow: 0px -1px 0 #fff;
+  border-top: 1px solid #fff;
+  z-index: 99;
+  color: #645b61;
+}
+
+.start h1 {
+  margin: 0;
+  background-color: #fff;
+  font-size: 40px;
+  line-height: 40px;
+  padding: 30px 20px;
+  border-bottom: 1px solid #eee;
+  color: #5a5157;
+  border-left: 4px solid #ddd;
+}
+
+/** Code stylings */
+section.start pre {
+  font-family: Consolas, Inconsolata, 'Bitstream Vera Sans Mono', Menlo, Monaco, 'Andale Mono', 'Courier New', monospace;
+  font-size: 12px;
+  background-color: #474045;
+  border-left: 4px solid #40393e;
+  padding: 15px 20px 15px 25px;
+  color: #e7e0e5;
+  text-shadow: 0 -1px 0 rgba(0,0,0,0.1);
+  max-width: 100%;
+  overflow: auto;
+}
+/** /code */
+
+h1 a {
+  color: #5a5157;
+  text-decoration: none;
+  transition: color 300ms;
+}
+
+h1 a:hover {
+  color: #745e6d;
+}
+
+h1 .title {
+  color: #fff;
+  display: inline-block;
+  font-size: 15px;
+  text-transform: uppercase;
+  font-variant: small-caps;
+  background-color: #E2A62E;
+  padding: 2px 4px;
+  line-height: 15px;
+  border-radius: 2px;
+  border: 2px solid rgba(0,0,0,0.2);
+  text-shadow: 0 -1px 0 rgba(0,0,0,0.2);
+}
+
+.start > p, .start > div {
+  padding: 5px 20px 5px 25px;
+  border-left: 4px solid #ddd;
+}
+
+.start p.red {
+  color: #8F4537;
+}
+
+.start h2, .start h3, .start h4 {
+  color: #5a5157;
+  padding: 12px 20px 5px 15px;
+  margin: 0;
+  font-size: 20px;
+  border-left: 4px solid #ccc;
+}
+
+.start h3 {
+  font-size: 15px;
+  padding: 12px 20px 5px 15px;
+  border-bottom: 1px solid #eee;
+}
+
+a {
+  font-weight: 600;
+  color: #1295D8;
+  text-decoration: none;
+  transition: color 200ms;
+}
+a:hover {
+  color: #33a2dc;
+}
+
+
+.api {
+  color: #f5eff3;
+  font-weight: 300;
+  top: 35px;
+  left: 0px;
+  width: 47%;
+  border-right: 4px solid #474046;
+  border-top: 1px solid rgba(255,255,255,0.1);
+  background-color: #5a5157;
+  text-shadow: 0px -1px 0 #474045;
+  z-index: 100;
+}
+
+.child {
+  padding: 5px 0 5px 12px;
+}
+
+.bracket {
+  font-weight: 800;
+  display: inline-block;
+  margin: 0 2px;
+  color: rgba(255,255,255,0.2);
+}
+
+.toplevel {
+  border-bottom: 1px solid rgba(0,0,0,0.1);
+  padding: 30px 20px;
+}
+
+.api:not(:first-child) {
+  border-top: 1px solid rgba(255,255,255,0.1);
+}
+
+.toplevel > .children > .child {
+  padding-right: 5px;
+}
+
+.children {
+  border-left: 1px dashed rgba(255,255,255,0.2);
+}
+
+.beta_030 .children {
+  border-left: 1px dashed #E96151;
+}
+
+.api div p {
+  margin: 0 0 5px 0;
+}
+
+.child:hover {
+  background-color: #635960;
+}
+
+/** Label stylings */
+.tag {
+  display: inline-block;
+  background-color: #454545;
+  border-radius: 2px;
+  color: rgba(255,255,255,0.8);
+  line-height: 11px;
+  padding: 1px 2px;
+  margin-left: 5px;
+  font-size: 9px;
+  letter-spacing: 1px;
+  font-weight: 600;
+  font-family: "Lato", Helvetica, sans-serif;
+  text-transform: uppercase;
+  border: 1px solid rgba(0,0,0,0.4);
+  text-shadow: 0px -1px 0px rgba(0,0,0,0.2);
+}
+
+.tag.type {
+  background-color: #757E2B;
+}
+
+.tag.beta_030 {
+  background-color: #E96151;
+}
+
+.tag.method, .tag.function {
+  background-color: #E2A62E;
+}
+
+.tag.class, .tag.constructor, .tag.utility {
+  background-color: #468F81;
+}
+
+.tag.error {
+  background-color: #8F4537;
+}
+
+.tag.event {
+  background-color: #1295D8;
+}
+
+.toplevel > .name {
+  font-size: 20px;
+}
+
+/** /Label stylings */
+
+.api .snippet {
+  color: #a2949d;
+  border-left: 1px solid #40393e;
+  border-top: 1px solid #40393e;
+  border-right: 1px solid #6b666a;
+  border-bottom: 1px solid #6b666a;
+  text-shadow: 0 -1px 0 #393438;
+  font-size: 12px;
+  font-family: Consolas, Inconsolata, 'Bitstream Vera Sans Mono', Menlo, Monaco, 'Andale Mono', 'Courier New', monospace;
+  font-weight: 400;
+  display: inline-block;
+  background-color: #474045;
+  padding: 3px 8px;
+  margin: 5px 0 0 10px;
+}
+
+/* We want to be able to use the names as an anchor. */
+.api .name {
+  font-weight: 600;
+  display: inline-block;
+  margin-bottom: 5px;
+}
+
+.name a {
+  cursor: pointer;
+  color: #fff;
+  text-decoration: none;
+  transition: color 300ms;
+}
+
+.name a:hover {
+  color: #e7e0e5;
+}
+/* /name */
+
+.tip, .warn {
+  opacity: 0.9;
+  display: block;
+  background-color: #d1c7be;
+  font-size: 13px;
+  line-height: 18px;
+  border-radius: 2px;
+  padding: 5px 8px;
+  border: 2px solid rgba(0,0,0,0.2);
+  margin: 8px 8px 0 0;
+}
+
+.warn {
+  background-color: #8F4537;
+}
+.tip {
+  color: #544e4a;
+  text-shadow: 0px -1px 0px rgba(255,255,255,0.2);
+  font-weight: 600;
+}
+
+#peer-options-debug .child {
+  padding: 0 15px;
+}
+
+#peer-options-debug .child .description {
+  display: inline-block;
+  margin-left: 10px;
+}
+
+.start code, .api code {
+  font-family: Consolas, Inconsolata, 'Bitstream Vera Sans Mono', Menlo, Monaco, 'Andale Mono', 'Courier New', monospace;
+  border-radius: 2px;
+  border: 1px solid rgba(255,255,255,0.2);
+  background: rgba(255,255,255,0.1);
+  font-size: 12px;
+  padding: 2px;
+}
+
+.start code {
+  background-color: #f6eee8;
+  border: 1px solid #d1c7be;
+}
+
+.start > .two-col {
+  padding-left: 0;
+  padding-right: 0;
+}
+.two-col .col{
+  float: left;
+  width: 50%;
+}
+
+.two-col .col.col-header {
+  font-weight: 600;
+  box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  padding-left: 20px;
+  padding-right: 20px;
+}
+
+.clear {
+  clear: both;
+}

+ 174 - 0
docs/template.html

@@ -0,0 +1,174 @@
+<head>
+  <title>PeerJS Documentation</title>
+  <meta name="viewport" content="width=device-width, maximum-scale=1">
+  <link href='http://fonts.googleapis.com/css?family=Lato:300,400,700,900' rel='stylesheet' type='text/css'>
+  <link href="./style.css" rel="stylesheet" type="text/css">
+  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
+  <script type="text/javascript" src="./index.js"></script>
+</head>
+
+<body>
+  <section class="start">
+    <h1><a href="/">PeerJS</a> <span class="title">docs</span></h1>
+    <p><br>PeerJS simplifies peer-to-peer data, video, and audio calls.</p>
+      <p>This guide will show you the basic concepts of the PeerJS API. If
+      you learn better from seeing a working app, <a href="/examples">see the examples</a> page.</P>
+      <h2>Setup</h2>
+      <h3>1. Include the Javascript client</h3>
+      <p>Add the PeerJS client library to your webpage.</p>
+      <pre>&lt;script src="http://cdn.peerjs.com/0.3/peer.min.js"&gt;&lt;/script&gt;</pre>
+      <p>If you prefer, you can host it yourself: <a download
+        href="http://cdn.peerjs.com/0.3/peer.js">peer.js</a>, <a download
+      href="http://cdn.peerjs.com/0.3/peer.min.js">peer.min.js</a>, or <a
+      href="https://github.com/peers/peerjs">fork us on Github</a>.</p>
+      <h3>2. Create the Peer object</h3>
+      <p>The Peer object is where we create and receive connections.</p>
+      <pre>var peer = new Peer({key: 'lwjd5qra8257b9'});</pre>
+      <p>The 'key' we're passing in to the Peer constructor is a PeerServer
+      cloud API key. You can use ours for now, but you should <a
+      href="http://peerjs.com/peerserver">sign up for your own free key</a>.
+    PeerJS uses PeerServer for session metadata and candidate signaling. You can
+    also <a href="http://github.com/peers/peerjs-server">run your own PeerServer</a> if you don't like the cloud.</p>
+      <p>We're now ready to start making connections!</p>
+
+      <h2>Usage</h2>
+      <p>Every Peer object is assigned a random, unique ID when it's created.</p>
+      <pre>peer.on('open', function(id){
+  console.log('My peer ID is: ' + id);
+});</pre>
+      <p>When we want to connect to another peer, we'll need to know their peer
+      id. You're in charge of communicating the peer IDs between users of your
+      site. Optionally, you can pass in your own IDs to the <a href="#peer"><code>Peer</code>
+        constructor</a>.</p>
+
+      <p>Read the <a href="#peer">Peer API reference</a> for complete
+      information on its <a href="#peer-options">options</a>, methods, <a href="#peeron">events</a>, and <a
+        href="#peeron-error">error handling</a>.</p>
+
+      <h3>Data connections</h3>
+      <p>Start a data connection by calling <code>peer.connect</code> with the peer ID of the
+      destination peer. Anytime another peer attempts to connect to your peer
+      ID, you'll receive a <code>connection</code> event. </p>
+      <div class="two-col">
+        <div class="col col-header">Start connection</div>
+        <div class="col col-header">Receive connection</div>
+        <div class="col"><pre>var conn = peer.connect('dest-peer-id');</pre></div>
+        <div class="col"><pre>peer.on('connection', function(conn) { ... });</pre></div>
+        <div class="clear"></div>
+      </div>
+      <p><code>peer.connect</code> and the callback of the
+      <code>connection</code> event will both provide a
+      <code>DataConnection</code> object. This object will allow you to send and receive data:</p>
+      <pre>conn.on('open', function(){
+  // Receive messages
+  conn.on('data', function(data){
+    console.log('Received', data);
+  });
+
+  // Send messages
+  conn.send('Hello!');
+});</pre>
+      <p>Read the <a href="#dataconnection">DataConnection API reference</a> for complete details on its methods and events.</p>
+      <h3>Video/audio calls</h3>
+      <p>Call another peer by calling <code>peer.call</code> with the peer ID of the destination
+      peer. When a peer calls you, the <code>call</code> event is emitted.</p>
+      <p>Unlike data connections, when receiving a <code>call</code> event, the call must be answered or no connection is established.</p>
+      <div class="two-col">
+        <div class="col col-header">Start call</div>
+        <div class="col col-header">Answer call</div>
+        <div class="col"><pre>// Call a peer, providing our mediaStream
+var call = peer.call('dest-peer-id',
+  mediaStream);
+
+</pre></div>
+        <div class="col"><pre>peer.on('call', function(call){
+  // Answer the call, providing our mediaStream
+  call.answer(mediaStream);
+});</pre></div>
+        <div class="clear"></div>
+      </div>
+      <p>When calling or answering a call, a MediaStream should be provided. The
+      MediaStream represents the local video (webcam) or audio stream and can be
+      obtained with some (browser-specific) version of <a
+        href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getUserMedia"><code>navigator.getUserMedia</code></a>. When answering a
+      call, the MediaStream is optional and if none is provided then a one-way
+      call is established. Once the call is established, its <code>open</code> property is
+      set to true.</p>
+      <p><code>peer.call</code> and the callback of the <code>call</code> event
+      provide a MediaConnection object. The MediaConnection object itself emits
+      a <code>stream</code> event whose callback includes the video/audio stream of the other peer.</p>
+      <pre>call.on('stream', function(stream){
+  // `stream` is the MediaStream of the remote peer.
+  // Here you'd add it to an HTML video/canvas element.
+});</pre>
+      <p>Read the <a href="#mediaconnection">MediaConnection API reference</a> for complete details on its methods and events.</p>
+
+      <h2>Common questions</h2>
+
+        <h3>What kind of data can I send?</h3>
+
+        <p>PeerJS has the <a
+          href="https://github.com/binaryjs/js-binarypack">BinaryPack</a>
+        serialization format built-in. This means you can send any JSON type as
+        well as binary Blobs and ArrayBuffers. Simply send arbitrary data and
+        you'll get it out the
+        other side:</p>
+          <pre>
+conn.send({
+  strings: 'hi!',
+  numbers: 150,
+  arrays: [1,2,3],
+  evenBinary: new Blob([1,2,3]),
+  andMore: {bool: true}
+});</pre>
+
+        <h3>Are there any caveats?</h3>
+
+          <p>A very low percentage of users are behind symmetric NATs. When two
+          symmetric NAT users try to connect to each other, NAT traversal is
+        impossible and no connection can be made. A workaround is to proxy
+      through what is known as a TURN server. The PeerServer cloud service does
+    not provide a TURN server. You'll have to find your own. You can pass a TURN
+  server into the <code>Peer</code> object options. This will allow your PeerJS app to work seamlessly for this situation</p>
+
+        <h3>What is the current state of browser compatibility?</h3>
+
+          <p>We keep an frequently-updated catalogue of WebRTC compatibility
+          information and caveats <a href="/status">here</a>.</p>
+
+        <h3>What if my peer has not yet connected to the server when I attempt
+          to connect to it?</h3>
+
+          <p>When you try to connect to a peer, PeerServer will hold a
+          connection offer for up to 5 seconds before rejecting it. This is useful if you want to reconnect to a peer as it disconnects and reconnects rapidly between web pages.</p>
+
+        <h3>What about latency/bandwidth?</h3>
+
+          <p>Data sent between the two peers do not touch any other servers, so the connection speed is limited only by the upload and download rates of the two peers. This also means you don't have the additional latency of an intermediary server.</p>
+          <p>The latency to establish a connection can be split into two
+          components:
+          the brokering of data and the identification of clients. PeerJS has been
+          designed to minimize the time you spend in these two areas. For
+          brokering, data is sent through an XHR streaming request before a WebSocket
+          connection is established, then through WebSockets. For client
+          identification, we provide you the ability to pass in your own peer
+          IDs, thus eliminating the RTT for retrieving an ID from the server.</p>
+
+        <h3>More questions?</h3>
+          <p><a
+            href="https://groups.google.com/forum/?fromgroups#!forum/peerjs">Discuss
+            PeerJS on our Google Group.</a><br><br></p>
+  </section>
+
+  <header class="left">
+    <h2>API Reference<a class="hide icon">&laquo;</a><a class="show icon">&raquo;</a></h2>
+  </header>
+  <header class="right">
+    <h2>Getting Started</h2>
+  </header>
+
+  <section class="api">
+    {{{html}}}
+  </section>
+
+</body>

+ 48 - 27
examples/chat.html

@@ -6,21 +6,27 @@
 <meta http-equiv="Content-Language" content="en-us"> 
 
 <link href="fancy.css" rel="stylesheet" type="text/css">
-<script>
-  // Just for demo.
-  console._log = console.log;
-  console.log = function() {
-    var copy = Array.prototype.slice.call(arguments).join(' ');
-    $('.log').append(copy + '<br>');
-    console._log(copy);
-  };
-</script>
 
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script> 
-<script type="text/javascript" src="http://cdn.peerjs.com/0/peer.js"></script>
+<script type="text/javascript" src="http://cdn.peerjs.com/0.3/peer.min.js"></script>
 <script>
 // Connect to PeerJS, have server assign an ID instead of providing one
-var peer = new Peer({key: 'lwjd5qra8257b9', debug: true});
+// Showing off some of the configs available with PeerJS :).
+var peer = new Peer({
+  // Set API key for cloud server (you don't need this if you're running your
+  // own.
+  key: 'lwjd5qra8257b9',
+
+  // Set highest debug level (log everything!).
+  debug: 3,
+
+  // Set a logging function:
+  logFunction: function() {
+    var copy = Array.prototype.slice.call(arguments).join(' ');
+    $('.log').append(copy + '<br>');
+  }
+});
+var connectedPeers = {};
 
 // Show this peer's ID.
 peer.on('open', function(id){
@@ -61,6 +67,7 @@ function connect(c) {
           if ($('.connection').length === 0) {
             $('.filler').show();
           }
+          delete connectedPeers[c.peer];
         });
   } else if (c.label === 'file') {
     c.on('data', function(data) {
@@ -98,17 +105,26 @@ $(document).ready(function() {
 
   // Connect to a peer
   $('#connect').click(function() {
-    // Create 2 connections, one labelled chat and another labelled file.
-    var c = peer.connect($('#rid').val(), { label: 'chat' });
-    c.on('open', function() {
-      connect(c);
-    });
-    c.on('error', function(err) { alert(err); });
-    var f = peer.connect($('#rid').val(), { reliable: true, label: 'file' });
-    f.on('open', function() {
-      connect(f);
-    });
-    f.on('error', function(err) { alert(err); });
+    requestedPeer = $('#rid').val();
+    if (!connectedPeers[requestedPeer]) {
+      // Create 2 connections, one labelled chat and another labelled file.
+      var c = peer.connect(requestedPeer, {
+        label: 'chat',
+        serialization: 'none',
+        reliable: false,
+        metadata: {message: 'hi i want to chat with you!'}
+      });
+      c.on('open', function() {
+        connect(c);
+      });
+      c.on('error', function(err) { alert(err); });
+      var f = peer.connect(requestedPeer, { label: 'file' });
+      f.on('open', function() {
+        connect(f);
+      });
+      f.on('error', function(err) { alert(err); });
+    }
+    connectedPeers[requestedPeer] = 1;
   });
 
   // Close a connection.
@@ -137,14 +153,19 @@ $(document).ready(function() {
   // Goes through each active peer and calls FN on its connections.
   function eachActiveConnection(fn) {
     var actives = $('.active');
+    var checkedIds = {};
     actives.each(function() {
       var peerId = $(this).attr('id');
-      var conns = peer.connections[peerId];
-      var labels = Object.keys(conns);
-      for (var i = 0, ii = labels.length; i < ii; i += 1) {
-        var conn = conns[labels[i]];
-        fn(conn, $(this));
+
+      if (!checkedIds[peerId]) {
+        var conns = peer.connections[peerId];
+        for (var i = 0, ii = conns.length; i < ii; i += 1) {
+          var conn = conns[i];
+          fn(conn, $(this));
+        }
       }
+
+      checkedIds[peerId] = 1;
     });
   }
 

+ 3 - 14
examples/helloworld.html

@@ -5,30 +5,19 @@
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
 <meta http-equiv="Content-Language" content="en-us"> 
 
-<script>
-  // Just for demo.
-  console._log = console.log;
-  console.error = console.log = function() {
-    var copy = Array.prototype.slice.call(arguments).join(' ');
-    $('.log').append(copy + '<br>');
-    console._log(copy);
-  };
-</script>
-
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> 
-<script type="text/javascript" src="../dist/peer.js"></script>
+<script type="text/javascript" src="http://cdn.peerjs.com/0.3/peer.min.js"></script>
 <script>
   // This is a very simple code example. See chat.html for a more involved
   // example.
 
   $(document).ready(function() {
-    var peer1, peer2, peerId1;
 
     // Create a new Peer with our demo API key, with debug set to true so we can
     // see what's going on.
-    peer1 = new Peer({ key: 'lwjd5qra8257b9', debug: true });
+    peer1 = new Peer({ key: 'lwjd5qra8257b9', debug: 3});
     // Create another Peer with our demo API key to connect to.
-    peer2 = new Peer({ key: 'lwjd5qra8257b9', debug: true });
+    peer2 = new Peer({ key: 'lwjd5qra8257b9', debug: 3});
 
     // The `open` event signifies that the Peer is ready to connect with other
     // Peers and, if we didn't provide the Peer with an ID, that an ID has been

+ 121 - 0
examples/helloworldv.html

@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+<title>PeerJS Hello World Code Example</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta http-equiv="Content-Language" content="en-us">
+
+<script>
+  // Just for demo.
+  console._log = console.log;
+  console.error = console.log = function() {
+    var copy = Array.prototype.slice.call(arguments);
+    $('.log').append(copy.join(' ') + '<br>');
+    console._log.apply(console, copy);
+  };
+</script>
+
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+<script type="text/javascript" src="../dist/peer.js"></script>
+<script>
+  // This is a very simple code example. See chat.html for a more involved
+  // example.
+
+  $(document).ready(function() {
+    navigator.webkitGetUserMedia({audio: true, video: true}, function(s){
+      window.ls = s;
+      // Create a new Peer with our demo API key, with debug set to true so we can
+      // see what's going on.
+      peer1 = new Peer(window.location.hash.substr(1), { key: 'lwjd5qra8257b9', debug: 3});
+      peer1.on('error', console.log);
+      peer1.on('close', function(){
+        console.log('peer closed')
+      });
+      peer1.on('call', function(c){
+        window.c = c;
+        c.answer(s);
+        c.on('stream', function(s){
+          window.s = s;
+          z = $('<video></video>', {src: URL.createObjectURL(s), autoplay: true}).appendTo('body');
+        });
+        c.on('close', function(){
+          console.log('call closed')
+        });
+      });
+
+
+
+
+    }, function(){});
+
+  });
+  function call (name) {
+      mc = peer1.call(name, ls);
+      mc.on('stream', function(s){
+        window.remote = s;
+          z = $('<video></video>', {src: URL.createObjectURL(s), autoplay: true}).appendTo('body');
+      });
+      mc.on('close', function(){
+        console.log('my call closed');
+      });
+  }
+
+
+</script>
+<style>
+  #helloworld {
+    font-weight: 600;
+    font-size: 30px;
+    padding: 20px;
+    background-color: #4dace2;
+    border: 1px solid #0C6BA1;
+    max-width: 600px;
+  }
+  #browsers {
+    font-weight: 600;
+  }
+  .warning {
+    max-width: 600px;
+    padding: 20px;
+    background-color: #eee;
+    border: 1px solid #ccc;
+    font-size: 18px;
+  }
+  .browserinfo {
+    padding: 20px;
+    border: 1px solid #ccc;
+    background-color: #f8f8f8;
+  }
+  a {
+    font-weight: 600;
+  }
+</style>
+</head>
+
+<body>
+  <a href="https://github.com/peers/peerjs"><img style="position: absolute; top: 0; right: 0; border: 0;"
+    src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"
+    alt="Fork me on GitHub"></a>
+  <div id="helloworld"></div>
+  <div class="warning browser"><div class="important">
+      Good news! If you can see the text in the blue box above, your Chrome is up to
+      date (version 26) and you can now use WebRTC P2P
+      DataChannels.
+      <br>
+      Open up your Chrome inspector to see what's going on under the hood.
+      <br><br>
+      Not cool enough? Try out <a
+        href="http://cdn.peerjs.com/demo/chat.html">a chat demo</a>
+      with a friend.
+      <br>
+      This demo was built with <a href="http://peerjs.com">PeerJS.</a><br><br>
+      <div class="browserinfo">
+      Your browser version: <span id="browsers"></span><br>
+  Currently <strong>Firefox 22+ and Google Chrome 26.0.1403.0 or above</strong> is
+  required.</strong></div><br>For more up to date compatibility
+information see <a href="http://peerjs.com/status">PeerJS WebRTC
+  Status</a><br>Note that this demo may also fail if you are behind
+stringent firewalls.</div></div>
+<div class="log" style="color:#FF7500;text-shadow:none;padding:15px;"><strong>Connection status</strong>:<br></div>
+</body>
+</html>

+ 0 - 2
examples/readme.md

@@ -11,8 +11,6 @@
 
 ###[Wikipedia Golf](http://wikigolf.herokuapp.com/)
 
-A Game with a Purpose (GWAP) that is designed to provide meaningful data useful for NLP programs.
-
 ### More on the way!
 
 As we build more examples and demos they will be posted here. If you have built something cool, feel free to make a pull request to this directory.

+ 139 - 0
examples/videochat/index.html

@@ -0,0 +1,139 @@
+<html>
+<head>
+  <title>PeerJS - Video chat example</title>
+  <link rel="stylesheet" href="style.css">
+  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
+  <script type="text/javascript" src="http://cdn.peerjs.com/0.3/peer.min.js"></script>
+  <script>
+
+    // Compatibility shim
+    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+
+    // PeerJS object
+    var peer = new Peer({ key: 'lwjd5qra8257b9', debug: 3});
+
+    peer.on('open', function(){
+      $('#my-id').text(peer.id);
+    });
+
+    // Receiving a call
+    peer.on('call', function(call){
+      // Answer the call automatically (instead of prompting user) for demo purposes
+      call.answer(window.localStream);
+      step3(call);
+    });
+    peer.on('error', function(err){
+      alert(err.message);
+      // Return to step 2 if error occurs
+      step2();
+    });
+
+    // Click handlers setup
+    $(function(){
+      $('#make-call').click(function(){
+        // Initiate a call!
+        var call = peer.call($('#callto-id').val(), window.localStream);
+
+        step3(call);
+      });
+
+      $('#end-call').click(function(){
+        window.existingCall.close();
+        step2();
+      });
+
+      // Retry if getUserMedia fails
+      $('#step1-retry').click(function(){
+        $('#step1-error').hide();
+        step1();
+      });
+
+      // Get things started
+      step1();
+    });
+
+    function step1 () {
+      // Get audio/video stream
+      navigator.getUserMedia({audio: true, video: true}, function(stream){
+        // Set your video displays
+        $('#my-video').prop('src', URL.createObjectURL(stream));
+
+        window.localStream = stream;
+        step2();
+      }, function(){ $('#step1-error').show(); });
+    }
+
+    function step2 () {
+      $('#step1, #step3').hide();
+      $('#step2').show();
+    }
+
+    function step3 (call) {
+      // Hang up on an existing call if present
+      if (window.existingCall) {
+        window.existingCall.close();
+      }
+
+      // Wait for stream on the call, then set peer video display
+      call.on('stream', function(stream){
+        $('#their-video').prop('src', URL.createObjectURL(stream));
+      });
+
+      // UI stuff
+      window.existingCall = call;
+      $('#their-id').text(call.peer);
+      call.on('close', step2);
+      $('#step1, #step2').hide();
+      $('#step3').show();
+    }
+
+  </script>
+
+
+</head>
+
+<body>
+
+  <div class="pure-g">
+
+      <!-- Video area -->
+      <div class="pure-u-2-3" id="video-container">
+        <video id="their-video" autoplay></video>
+        <video id="my-video" muted="true" autoplay></video>
+      </div>
+
+      <!-- Steps -->
+      <div class="pure-u-1-3">
+        <h2>PeerJS Video Chat</h2>
+
+        <!-- Get local audio/video stream -->
+        <div id="step1">
+          <p>Please click `allow` on the top of the screen so we can access your webcam and microphone for calls.</p>
+          <div id="step1-error">
+            <p>Failed to access the webcam and microphone. Make sure to run this demo on an http server and click allow when asked for permission by the browser.</p>
+            <a href="#" class="pure-button pure-button-error" id="step1-retry">Try again</a>
+          </div>
+        </div>
+
+        <!-- Make calls to others -->
+        <div id="step2">
+          <p>Your id: <span id="my-id">...</span></p>
+          <p>Share this id with others so they can call you.</p>
+          <h3>Make a call</h3>
+          <div class="pure-form">
+            <input type="text" placeholder="Call user id..." id="callto-id">
+            <a href="#" class="pure-button pure-button-success" id="make-call">Call</a>
+          </div>
+        </div>
+
+        <!-- Call in progress -->
+        <div id="step3">
+          <p>Currently in call with <span id="their-id">...</span></p>
+          <p><a href="#" class="pure-button pure-button-error" id="end-call">End call</a></p>
+        </div>
+      </div>
+  </div>
+
+
+</body>
+</html>

File diff suppressed because it is too large
+ 10 - 0
examples/videochat/style.css


+ 1 - 8
lib/adapter.js

@@ -1,10 +1,3 @@
-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;
+exports.RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;

+ 0 - 332
lib/connectionmanager.js

@@ -1,332 +0,0 @@
-/**
- * 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);
-    });
-  }, function(err) {
-    self.emit('error', err);
-    util.log('Failed to createOffer, ', 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];
-  }
-};

+ 66 - 77
lib/dataconnection.js

@@ -1,34 +1,50 @@
 /**
  * Wraps a DataChannel between two Peers.
  */
-function DataConnection(peer, dc, options) {
-  if (!(this instanceof DataConnection)) return new DataConnection(peer, dc, options);
+function DataConnection(peer, provider, options) {
+  if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options);
   EventEmitter.call(this);
 
-  options = util.extend({
-    serialization: 'binary'
+  // TODO: perhaps default serialization should be binary-utf8?
+  this.options = util.extend({
+    serialization: 'binary',
+    reliable: true
   }, options);
 
   // Connection is not open yet.
   this.open = false;
-
-  this.label = options.label;
-  this.metadata = options.metadata;
-  this.serialization = options.serialization;
+  this.type = 'data';
   this.peer = peer;
-  this.reliable = options.reliable;
+  this.provider = provider;
 
-  this._dc = dc;
-  if (!!this._dc) {
-    this._configureDataChannel();
-  }
-};
+  this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken();
+
+  this.label = this.options.label || this.id;
+  this.metadata = this.options.metadata;
+  this.serialization = this.options.serialization;
+  this.reliable = this.options.reliable;
+
+  Negotiator.startConnection(
+    this,
+    this.options._payload || {
+      originator: true
+    }
+  );
+}
 
 util.inherits(DataConnection, EventEmitter);
 
+DataConnection._idPrefix = 'dc_';
+
+/** Called by the Negotiator when the DataChannel is ready. */
+DataConnection.prototype.initialize = function(dc) {
+  this._dc = dc;
+  this._configureDataChannel();
+}
+
 DataConnection.prototype._configureDataChannel = function() {
   var self = this;
-  if (util.browserisms !== 'Webkit') {
+  if (util.supports.binary) {
     // Webkit doesn't support binary yet
     this._dc.binaryType = 'arraybuffer';
   }
@@ -36,10 +52,10 @@ DataConnection.prototype._configureDataChannel = 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') {
+  if (!util.supports.reliable && this.reliable) {
     this._reliable = new Reliable(this._dc, util.debug);
   }
 
@@ -53,20 +69,10 @@ DataConnection.prototype._configureDataChannel = function() {
     };
   }
   this._dc.onclose = function(e) {
-    util.log('DataChannel closed.');
+    util.log('DataChannel closed for:', self.peer);
     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) {
@@ -92,13 +98,7 @@ DataConnection.prototype._handleDataMessage = function(e) {
     data = JSON.parse(data);
   }
   this.emit('data', data);
-};
-
-DataConnection.prototype.addDC = function(dc) {
-  this._dc = dc;
-  this._configureDataChannel();
-};
-
+}
 
 /**
  * Exposed functionality for users.
@@ -109,66 +109,55 @@ DataConnection.prototype.close = function() {
   if (!this.open) {
     return;
   }
-  this._cleanup();
-};
+  this.open = false;
+  Negotiator.cleanup(this);
+  this.emit('close');
+}
 
 /** Allows user to send data. */
 DataConnection.prototype.send = function(data) {
   if (!this.open) {
-    this.emit('error', new Error('Connection no longer open.'));
+    this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.'));
+    return;
   }
   if (this._reliable) {
-    // Note: reliable sending will make it so that you cannot customize
+    // Note: reliable shim 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') {
+  if (this.serialization === 'json') {
     this._dc.send(JSON.stringify(data));
-  } else {
+  } else if ('binary-utf8'.indexOf(this.serialization) !== -1) {
     var utf8 = (this.serialization === 'binary-utf8');
     var blob = util.pack(data, utf8);
     // DataChannel currently only supports strings.
-    if (util.browserisms === 'Webkit') {
+    if (!util.supports.binary) {
       util.blobToBinaryString(blob, function(str){
         self._dc.send(str);
       });
     } else {
       this._dc.send(blob);
     }
+  } else {
+    this._dc.send(data);
   }
-};
-
-/**
- * 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;
-};
+}
+
+DataConnection.prototype.handleMessage = function(message) {
+  var payload = message.payload;
+
+  switch (message.type) {
+    case 'ANSWER':
+      // Forward to negotiator
+      Negotiator.handleSDP(message.type, this, payload.sdp);
+      break;
+    case 'CANDIDATE':
+      Negotiator.handleCandidate(this, payload.candidate);
+      break;
+    default:
+      util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
+      break;
+  }
+}

+ 89 - 0
lib/mediaconnection.js

@@ -0,0 +1,89 @@
+/**
+ * Wraps the streaming interface between two Peers.
+ */
+function MediaConnection(peer, provider, options) {
+  if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options);
+  EventEmitter.call(this);
+
+  this.options = util.extend({}, options);
+
+  this.open = false;
+  this.type = 'media';
+  this.peer = peer;
+  this.provider = provider;
+  this.metadata = this.options.metadata;
+  this.localStream = this.options._stream;
+
+  this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken();
+  if (this.localStream) {
+    Negotiator.startConnection(
+      this,
+      {_stream: this.localStream, originator: true}
+    );
+  }
+};
+
+util.inherits(MediaConnection, EventEmitter);
+
+MediaConnection._idPrefix = 'mc_';
+
+MediaConnection.prototype.addStream = function(remoteStream) {
+  util.log('Receiving stream', remoteStream);
+
+  this.remoteStream = remoteStream;
+  this.emit('stream', remoteStream); // Should we call this `open`?
+
+};
+
+MediaConnection.prototype.handleMessage = function(message) {
+  var payload = message.payload;
+
+  switch (message.type) {
+    case 'ANSWER':
+      // Forward to negotiator
+      Negotiator.handleSDP(message.type, this, payload.sdp);
+      this.open = true;
+      break;
+    case 'CANDIDATE':
+      Negotiator.handleCandidate(this, payload.candidate);
+      break;
+    default:
+      util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
+      break;
+  }
+}
+
+MediaConnection.prototype.answer = function(stream) {
+  if (this.localStream) {
+    util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?');
+    return;
+  }
+
+  this.options._payload._stream = stream;
+
+  this.localStream = stream;
+  Negotiator.startConnection(
+    this,
+    this.options._payload
+  )
+  // Retrieve lost messages stored because PeerConnection not set up.
+  var messages = this.provider._getMessages(this.id);
+  for (var i = 0, ii = messages.length; i < ii; i += 1) {
+    this.handleMessage(messages[i]);
+  }
+  this.open = true;
+};
+
+/**
+ * Exposed functionality for users.
+ */
+
+/** Allows user to close connection. */
+MediaConnection.prototype.close = function() {
+  if (!this.open) {
+    return;
+  }
+  this.open = false;
+  Negotiator.cleanup(this);
+  this.emit('close')
+};

+ 288 - 0
lib/negotiator.js

@@ -0,0 +1,288 @@
+/**
+ * Manages all negotiations between Peers.
+ */
+var Negotiator = {
+  pcs: {
+    data: {},
+    media: {}
+  }, // type => {peerId: {pc_id: pc}}.
+  //providers: {}, // provider's id => providers (there may be multiple providers/client.
+  queue: [] // connections that are delayed due to a PC being in use.
+}
+
+Negotiator._idPrefix = 'pc_';
+
+/** Returns a PeerConnection object set up correctly (for data, media). */
+Negotiator.startConnection = function(connection, options) {
+  var pc = Negotiator._getPeerConnection(connection, options);
+
+  if (connection.type === 'media' && options._stream) {
+    // Add the stream.
+    pc.addStream(options._stream);
+  }
+
+  // Set the connection's PC.
+  connection.pc = pc;
+  // What do we need to do now?
+  if (options.originator) {
+    if (connection.type === 'data') {
+      // Create the datachannel.
+      var config = {};
+      if (util.supports.reliable && !options.reliable) {
+        // If we have canonical reliable support...
+        config = {maxRetransmits: false}
+      } else if (!util.supports.reliable) {
+        config = {reliable: options.reliable};
+      }
+      var dc = pc.createDataChannel(connection.label, config);
+      connection.initialize(dc);
+    }
+
+    if (!util.supports.onnegotiationneeded) {
+      Negotiator._makeOffer(connection);
+    }
+  } else {
+    Negotiator.handleSDP('OFFER', connection, options.sdp);
+  }
+}
+
+Negotiator._getPeerConnection = function(connection, options) {
+  if (!Negotiator.pcs[connection.type]) {
+    util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.');
+  }
+
+  if (!Negotiator.pcs[connection.type][connection.peer]) {
+    Negotiator.pcs[connection.type][connection.peer] = {};
+  }
+  var peerConnections = Negotiator.pcs[connection.type][connection.peer];
+
+  var pc;
+  // Not multiplexing while FF and Chrome have not-great support for it.
+  /*if (options.multiplex) {
+    ids = Object.keys(peerConnections);
+    for (var i = 0, ii = ids.length; i < ii; i += 1) {
+      pc = peerConnections[ids[i]];
+      if (pc.signalingState === 'stable') {
+        break; // We can go ahead and use this PC.
+      }
+    }
+  } else */
+  if (options.pc) { // Simplest case: PC id already provided for us.
+    pc = Negotiator.pcs[connection.type][connection.peer][options.pc];
+  }
+
+  if (!pc || pc.signalingState !== 'stable') {
+    pc = Negotiator._startPeerConnection(connection);
+  }
+  return pc;
+}
+
+/*
+Negotiator._addProvider = function(provider) {
+  if ((!provider.id && !provider.disconnected) || !provider.socket.open) {
+    // Wait for provider to obtain an ID.
+    provider.on('open', function(id) {
+      Negotiator._addProvider(provider);
+    });
+  } else {
+    Negotiator.providers[provider.id] = provider;
+  }
+}*/
+
+
+/** Start a PC. */
+Negotiator._startPeerConnection = function(connection) {
+  util.log('Creating RTCPeerConnection.');
+
+  var id = Negotiator._idPrefix + util.randomToken();
+  var optional = {};
+
+  if (connection.type === 'data' && !util.supports.reliable) {
+    optional = {optional: [{RtpDataChannels: true}]};
+  } else if (connection.type === 'media') {
+    // Interop req for chrome.
+    optional = {optional: [{DtlsSrtpKeyAgreement: true}]};
+  }
+
+  pc = new RTCPeerConnection(connection.provider.options.config, optional);
+  Negotiator.pcs[connection.type][connection.peer][id] = pc;
+
+  Negotiator._setupListeners(connection, pc, id);
+
+  return pc;
+}
+
+/** Set up various WebRTC listeners. */
+Negotiator._setupListeners = function(connection, pc, pc_id) {
+  var peerId = connection.peer;
+  var connectionId = connection.id;
+  var provider = connection.provider;
+
+  // ICE CANDIDATES.
+  util.log('Listening for ICE candidates.');
+  pc.onicecandidate = function(evt) {
+    if (evt.candidate) {
+      util.log('Received ICE candidates for:', connection.peer);
+      provider.socket.send({
+        type: 'CANDIDATE',
+        payload: {
+          candidate: evt.candidate,
+          type: connection.type,
+          connectionId: connection.id
+        },
+        dst: peerId,
+      });
+    }
+  };
+
+  pc.oniceconnectionstatechange = function() {
+    switch (pc.iceConnectionState) {
+      case 'disconnected':
+      case 'failed':
+        util.log('iceConnectionState is disconnected, closing connections to ' + peerId);
+        connection.close();
+        break;
+      case 'completed':
+        pc.onicecandidate = util.noop;
+        break;
+    }
+  };
+
+  // Fallback for older Chrome impls.
+  pc.onicechange = pc.oniceconnectionstatechange;
+
+  // ONNEGOTIATIONNEEDED (Chrome)
+  util.log('Listening for `negotiationneeded`');
+  pc.onnegotiationneeded = function() {
+    util.log('`negotiationneeded` triggered');
+    if (pc.signalingState == 'stable') {
+      Negotiator._makeOffer(connection);
+    } else {
+      util.log('onnegotiationneeded triggered when not stable. Is another connection being established?');
+    }
+  };
+
+  // DATACONNECTION.
+  util.log('Listening for data channel');
+  // Fired between offer and answer, so options should already be saved
+  // in the options hash.
+  pc.ondatachannel = function(evt) {
+    util.log('Received data channel');
+    var dc = evt.channel;
+    var connection = provider.getConnection(peerId, connectionId);
+    connection.initialize(dc);
+  };
+
+  // MEDIACONNECTION.
+  util.log('Listening for remote stream');
+  pc.onaddstream = function(evt) {
+    util.log('Received remote stream');
+    var stream = evt.stream;
+    provider.getConnection(peerId, connectionId).addStream(stream);
+  };
+}
+
+Negotiator.cleanup = function(connection) {
+  util.log('Cleaning up PeerConnection to ' + connection.peer);
+
+  var pc = connection.pc;
+
+  if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) {
+    pc.close();
+    connection.pc = null;
+  }
+}
+
+Negotiator._makeOffer = function(connection) {
+  var pc = connection.pc;
+  pc.createOffer(function(offer) {
+    util.log('Created offer.');
+
+    if (!util.supports.reliable && connection.type === 'data') {
+      offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
+    }
+
+    pc.setLocalDescription(offer, function() {
+      util.log('Set localDescription: offer', 'for:', connection.peer);
+      connection.provider.socket.send({
+        type: 'OFFER',
+        payload: {
+          sdp: offer,
+          type: connection.type,
+          label: connection.label,
+          reliable: connection.reliable,
+          serialization: connection.serialization,
+          metadata: connection.metadata,
+          connectionId: connection.id
+        },
+        dst: connection.peer,
+      });
+    }, function(err) {
+      connection.provider.emit('error', err);
+      util.log('Failed to setLocalDescription, ', err);
+    });
+  }, function(err) {
+    connection.provider.emit('error', err);
+    util.log('Failed to createOffer, ', err);
+  }, connection.options.constraints);
+}
+
+Negotiator._makeAnswer = function(connection) {
+  var pc = connection.pc;
+
+  pc.createAnswer(function(answer) {
+    util.log('Created answer.');
+
+    if (!util.supports.reliable && connection.type === 'data') {
+      answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
+    }
+
+    pc.setLocalDescription(answer, function() {
+      util.log('Set localDescription: answer', 'for:', connection.peer);
+      connection.provider.socket.send({
+        type: 'ANSWER',
+        payload: {
+          sdp: answer,
+          type: connection.type,
+          connectionId: connection.id
+        },
+        dst: connection.peer
+      });
+    }, function(err) {
+      connection.provider.emit('error', err);
+      util.log('Failed to setLocalDescription, ', err);
+    });
+  }, function(err) {
+    connection.provider.emit('error', err);
+    util.log('Failed to create answer, ', err);
+  });
+}
+
+/** Handle an SDP. */
+Negotiator.handleSDP = function(type, connection, sdp) {
+  sdp = new RTCSessionDescription(sdp);
+  var pc = connection.pc;
+
+  util.log('Setting remote description', sdp);
+  pc.setRemoteDescription(sdp, function() {
+    util.log('Set remoteDescription:', type, 'for:', connection.peer);
+
+    if (type === 'OFFER') {
+      Negotiator._makeAnswer(connection);
+    }
+  }, function(err) {
+    connection.provider.emit('error', err);
+    util.log('Failed to setRemoteDescription, ', err);
+  });
+}
+
+/** Handle a candidate. */
+Negotiator.handleCandidate = function(connection, ice) {
+  var candidate = ice.candidate;
+  var sdpMLineIndex = ice.sdpMLineIndex;
+  connection.pc.addIceCandidate(new RTCIceCandidate({
+    sdpMLineIndex: sdpMLineIndex,
+    candidate: candidate
+  }));
+  util.log('Added ICE candidate for:', connection.peer);
+}

+ 284 - 243
lib/peer.js

@@ -2,292 +2,327 @@
  * A peer who can initiate connections with other peers.
  */
 function Peer(id, options) {
+  if (!(this instanceof Peer)) return new Peer(id, options);
+  EventEmitter.call(this);
+
+  // Deal with overloading
   if (id && id.constructor == Object) {
     options = id;
     id = undefined;
+  } else if (id) {
+    // Ensure id is a string
+    id = id.toString();
   }
-  if (!(this instanceof Peer)) return new Peer(id, options);
-  EventEmitter.call(this);
-
+  //
 
+  // Configurize options
   options = util.extend({
-    debug: false,
-    host: '0.peerjs.com',
-    port: 9000,
+    debug: 0, // 1: Errors, 2: Warnings, 3: All logs
+    host: util.CLOUD_HOST,
+    port: util.CLOUD_PORT,
     key: 'peerjs',
-    config: { 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] }
+    config: util.defaultConfig
   }, 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;
-  }
-
+  this.options = options;
   // Detect relative URL host.
   if (options.host === '/') {
     options.host = window.location.hostname;
   }
+  // Set whether we use SSL to same as current host
+  if (options.secure === undefined && options.host !== util.CLOUD_HOST) {
+    options.secure = util.isSecure();
+  }
+  // Set a custom log function if present
+  if (options.logFunction) {
+    util.setLogFunction(options.logFunction);
+  }
+  util.setLogLevel(options.debug);
+  //
 
-  // 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');
-    });
+  // Sanity checks
+  // Ensure WebRTC supported
+  if (!util.supports.audioVideo && !util.supports.data ) {
+    this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC');
     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');
-    });
+  // Ensure alphanumeric id
+  if (!util.validateId(id)) {
+    this._delayedAbort('invalid-id', 'ID "' + id + '" 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.');
-    });
+  // Ensure valid key
+  if (!util.validateKey(options.key)) {
+    this._delayedAbort('invalid-key', 'API KEY "' + options.key + '" is invalid');
     return;
   }
+  // Ensure not using unsecure cloud server on SSL page
+  if (options.secure && options.host === '0.peerjs.com') {
+    this._delayedAbort('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 = [];
+  this.destroyed = false; // Connections have been killed
+  this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active
+  this.open = false; // Sockets and such are not yet open.
+  //
+
+  // References
+  this.connections = {}; // DataConnections for this peer.
+  this._lostMessages = {}; // src => [list of messages]
+  //
+
+  // Initialize the 'socket' (which is actually a mix of XHR streaming and
+  // websockets.)
+  var self = this;
+  this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.key);
+  this.socket.on('message', function(data) {
+    self._handleMessage(data);
+  });
+  this.socket.on('error', function(error) {
+    self._abort('socket-error', error);
+  });
+  this.socket.on('close', function() {
+    if (!self.disconnected) { // If we haven't explicitly disconnected, emit error.
+      self._abort('socket-closed', 'Underlying socket is already closed.');
+    }
+  });
+  //
 
-  // Init immediately if ID is given, otherwise ask server for ID
+  // Start the connections
   if (id) {
-    this.id = id;
-    this._init();
+    this._initialize(id);
   } else {
-    this.id = null;
     this._retrieveId();
   }
+  //
 };
 
 util.inherits(Peer, EventEmitter);
 
+/** Get a unique ID from the server via XHR. */
 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');
+  var http = new XMLHttpRequest();
+  var protocol = this.options.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.onerror = function(e) {
+    util.error('Error retrieving ID', e);
+    self._abort('server-error', 'Could not get an ID from the server');
   }
+  http.onreadystatechange = function() {
+    if (http.readyState !== 4) {
+      return;
+    }
+    if (http.status !== 200) {
+      http.onerror();
+      return;
+    }
+    self._initialize(http.responseText);
+  };
+  http.send(null);
 };
 
-
-Peer.prototype._init = function() {
+/** Initialize a connection with the server. */
+Peer.prototype._initialize = function(id) {
   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();
+  this.id = id;
+  this.socket.start(this.id);
 }
 
-
-Peer.prototype._handleServerJSONMessage = function(message) {
-  var peer = message.src;
-  var manager = this.managers[peer];
+/** Handles messages from the server. */
+Peer.prototype._handleMessage = function(message) {
+  var type = message.type;
   var payload = message.payload;
-  switch (message.type) {
-    case 'OPEN':
-      this._processQueue();
+  var peer = message.src;
+
+  switch (type) {
+    case 'OPEN': // The connection to the server is open.
       this.emit('open', this.id);
+      this.open = true;
       break;
-    case 'ERROR':
+    case 'ERROR': // Server 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);
+    case 'ID-TAKEN': // The selected ID is taken.
+      this._abort('unavailable-id', 'ID `' + this.id + '` is taken');
       break;
-    case 'EXPIRE':
-      if (manager) {
-        manager.close();
-        manager.emit('error', new Error('Could not connect to peer ' + manager.peer));
-      }
+    case 'INVALID-KEY': // The given API key cannot be found.
+      this._abort('invalid-key', 'API KEY "' + this._key + '" is invalid');
       break;
-    case 'ANSWER':
-      if (manager) {
-        manager.handleSDP(payload.sdp, message.type);
-      }
+
+    //
+    case 'LEAVE': // Another peer has closed its connection to this peer.
+      util.log('Received leave message from', peer);
+      this._cleanupPeer(peer);
       break;
-    case 'CANDIDATE':
-      if (manager) {
-        manager.handleCandidate(payload);
-      }
+
+    case 'EXPIRE': // The offer sent to a peer has expired without response.
+      this.emit('error', new Error('Could not connect to peer ' + peer));
       break;
-    case 'LEAVE':
-      if (manager) {
-        manager.handleLeave();
+    case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
+      var connectionId = payload.connectionId;
+      var connection = this.getConnection(peer, connectionId);
+
+      if (connection) {
+        util.warn('Offer received for existing Connection ID:', connectionId);
+        //connection.handleMessage(message);
+      } else {
+        // Create a new connection.
+        if (payload.type === 'media') {
+          var connection = new MediaConnection(peer, this, {
+            connectionId: connectionId,
+            _payload: payload,
+            metadata: payload.metadata,
+          });
+          this._addConnection(peer, connection);
+          this.emit('call', connection);
+        } else if (payload.type === 'data') {
+          connection = new DataConnection(peer, this, {
+            connectionId: connectionId,
+            _payload: payload,
+            metadata: payload.metadata,
+            label: payload.label,
+            serialization: payload.serialization,
+            reliable: payload.reliable
+          });
+          this._addConnection(peer, connection);
+          this.emit('connection', connection);
+        } else {
+          util.warn('Received malformed connection type:', payload.type);
+          return;
+        }
+        // Find messages.
+        var messages = this._getMessages(connectionId);
+        for (var i = 0, ii = messages.length; i < ii; i += 1) {
+          connection.handleMessage(messages[i]);
+        }
       }
       break;
-    case 'INVALID-KEY':
-      this._abort('invalid-key', 'API KEY "' + this._key + '" is invalid');
-      break;
     default:
-      util.log('Unrecognized message type:', message.type);
+      if (!payload) {
+        util.warn('You received a malformed message from ' + peer + ' of type ' + type);
+        return;
+      }
+
+      var id = payload.connectionId;
+      var connection = this.getConnection(peer, id);
+
+      if (connection && connection.pc) {
+        // Pass it on.
+        connection.handleMessage(message);
+      } else if (id) {
+        // Store for possible later use
+        this._storeMessage(id, message);
+      } else {
+        util.warn('You received an unrecognized message:', message);
+      }
       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);
+/** Stores messages without a set up connection, to be claimed later. */
+Peer.prototype._storeMessage = function(connectionId, message) {
+  if (!this._lostMessages[connectionId]) {
+    this._lostMessages[connectionId] = [];
   }
-};
-
-/** 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);
-};
+  this._lostMessages[connectionId].push(message);
+}
 
-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();
-    }
+/** Retrieve messages from lost message store */
+Peer.prototype._getMessages = function(connectionId) {
+  var messages = this._lostMessages[connectionId];
+  if (messages) {
+    delete this._lostMessages[connectionId];
+    return messages;
+  } else {
+    return [];
   }
-  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. */
+/**
+ * Returns a DataConnection to the specified peer. See documentation for a
+ * complete list of options.
+ */
 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);
+    util.warn('You cannot connect to a new Peer because you called '
+        + '.disconnect() on this Peer and ended your connection with the'
+        + ' server. You can create a new Peer to reconnect.');
+    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
     return;
   }
+  var connection = new DataConnection(peer, this, options);
+  this._addConnection(peer, connection);
+  return connection;
+}
 
-  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);
+/**
+ * Returns a MediaConnection to the specified peer. See documentation for a
+ * complete list of options.
+ */
+Peer.prototype.call = function(peer, stream, options) {
+  if (this.disconnected) {
+    util.warn('You cannot connect to a new Peer because you called '
+        + '.disconnect() on this Peer and ended your connection with the'
+        + ' server. You can create a new Peer to reconnect.');
+    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
     return;
   }
-
-  if (!manager) {
-    manager = new ConnectionManager(this.id, peer, this._socket, options);
-    this._attachManagerListeners(manager);
-    this.managers[peer] = manager;
-    this.connections[peer] = manager.connections;
+  if (!stream) {
+    util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.');
+    return;
   }
+  options = options || {};
+  options._stream = stream;
+  var call = new MediaConnection(peer, this, options);
+  this._addConnection(peer, call);
+  return call;
+}
 
-  var connection = manager.connect(options);
+/** Add a data/media connection to this peer. */
+Peer.prototype._addConnection = function(peer, connection) {
+  if (!this.connections[peer]) {
+    this.connections[peer] = [];
+  }
+  this.connections[peer].push(connection);
+}
 
-  if (!this.id) {
-    this._queued.push(manager);
+/** Retrieve a data/media connection for this peer. */
+Peer.prototype.getConnection = function(peer, id) {
+  var connections = this.connections[peer];
+  if (!connections) {
+    return null;
   }
-  return connection;
-};
+  for (var i = 0, ii = connections.length; i < ii; i++) {
+    if (connections[i].id === id) {
+      return connections[i];
+    }
+  }
+  return null;
+}
 
-/**
- * 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;
+Peer.prototype._delayedAbort = function(type, message) {
+  var self = this;
+  util.setZeroTimeout(function(){
+    self._abort(type, message);
+  });
+}
+
+/** Destroys the Peer and emits an error message. */
+Peer.prototype._abort = function(type, message) {
+  util.error('Aborting. Error:', message);
+  var err = new Error(message);
+  err.type = type;
+  this.destroy();
+  this.emit('error', err);
 };
 
 /**
@@ -299,9 +334,29 @@ Peer.prototype.getId = function() {
 Peer.prototype.destroy = function() {
   if (!this.destroyed) {
     this._cleanup();
+    this.disconnect();
     this.destroyed = true;
   }
-};
+}
+
+
+/** Disconnects every connection on this peer. */
+Peer.prototype._cleanup = function() {
+  var peers = Object.keys(this.connections);
+  for (var i = 0, ii = peers.length; i < ii; i++) {
+    this._cleanupPeer(peers[i]);
+  }
+
+  this.emit('close');
+}
+
+/** Closes all connections to this peer. */
+Peer.prototype._cleanupPeer = function(peer) {
+  var connections = this.connections[peer];
+  for (var j = 0, jj = connections.length; j < jj; j += 1) {
+    connections[j].close();
+  }
+}
 
 /**
  * Disconnects the Peer's connection to the PeerServer. Does not close any
@@ -310,31 +365,17 @@ Peer.prototype.destroy = function() {
  *  disconnected. It also cannot reconnect to the server.
  */
 Peer.prototype.disconnect = function() {
-  if (!this.disconnected) {
-    if (!!this._socket) {
-      this._socket.close();
+  var self = this;
+  util.setZeroTimeout(function(){
+    if (!self.disconnected) {
+      self.disconnected = true;
+      self.open = false;
+      if (self.socket) {
+        self.socket.close();
+      }
+      self.id = null;
     }
-    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;

+ 49 - 29
lib/socket.js

@@ -2,37 +2,42 @@
  * 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);
+function Socket(secure, host, port, key) {
+  if (!(this instanceof Socket)) return new Socket(secure, host, port, key);
 
-  this._id = id;
-  var token = util.randomToken();
+  EventEmitter.call(this);
 
+  // Disconnected manually.
   this.disconnected = false;
+  this._queue = [];
 
-  var secure = util.isSecure();
-  var protocol = secure ? 'https://' : 'http://';
+  var httpProtocol = 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;
-};
+  this._httpUrl = httpProtocol + host + ':' + port + '/' + key;
+  this._wsUrl = wsProtocol + host + ':' + port + '/peerjs?key=' + key;
+}
 
 util.inherits(Socket, EventEmitter);
 
 
 /** Check in with ID or get one from server. */
-Socket.prototype.start = function() {  
+Socket.prototype.start = function(id) {  
+  this.id = id;
+
+  var token = util.randomToken();
+  this._httpUrl += '/' + id + '/' + token;
+  this._wsUrl += '&id='+id+'&token='+token;
+
   this._startXhrStream();
   this._startWebSocket();
-};
+}
 
 
 /** Start up websocket communications. */
-Socket.prototype._startWebSocket = function() {
+Socket.prototype._startWebSocket = function(id) {
   var self = this;
 
-  if (!!this._socket) {
+  if (this._socket) {
     return;
   }
 
@@ -52,16 +57,17 @@ Socket.prototype._startWebSocket = function() {
   // Take care of the queue of connections if necessary and make sure Peer knows
   // socket is open.
   this._socket.onopen = function() {
-    if (!!self._timeout) {
+    if (self._timeout) {
       clearTimeout(self._timeout);
       setTimeout(function(){
         self._http.abort();
         self._http = null;
       }, 5000);
     }
+    self._sendQueuedMessages();
     util.log('Socket open');
   };
-};
+}
 
 /** Start XHR streaming. */
 Socket.prototype._startXhrStream = function(n) {
@@ -72,11 +78,11 @@ Socket.prototype._startXhrStream = function(n) {
     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) {
+      if (this.readyState == 2 && this.old) {
         this.old.abort();
         delete this.old;
       }
-      if (this.readyState > 2 && this.status == 200 && !!this.responseText) {
+      if (this.readyState > 2 && this.status == 200 && this.responseText) {
         self._handleStream(this);
       }
     };
@@ -85,7 +91,7 @@ Socket.prototype._startXhrStream = function(n) {
   } catch(e) {
     util.log('XMLHttpRequest not available; defaulting to WebSockets');
   }
-};
+}
 
 
 /** Handles onreadystatechange response as a stream. */
@@ -94,7 +100,7 @@ Socket.prototype._handleStream = function(http) {
   var messages = http.responseText.split('\n');
 
   // Check to see if anything needs to be processed on buffer.
-  if (!!http._buffer) {
+  if (http._buffer) {
     while (http._buffer.length > 0) {
       var index = http._buffer.shift();
       var bufferedMessage = messages[index];
@@ -109,7 +115,7 @@ Socket.prototype._handleStream = function(http) {
   }
 
   var message = messages[http._index];
-  if (!!message) {
+  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
@@ -129,7 +135,7 @@ Socket.prototype._handleStream = function(http) {
       this.emit('message', message);
     }
   }
-};
+}
 
 Socket.prototype._setHTTPTimeout = function() {
   var self = this;
@@ -137,17 +143,24 @@ Socket.prototype._setHTTPTimeout = function() {
     var old = self._http;
     if (!self._wsOpen()) {
       self._startXhrStream(old._streamIndex + 1);
-      self._http.old = old;        
+      self._http.old = old;
     } else {
       old.abort();
     }
   }, 25000);
-};
-
+}
 
+/** Is the websocket currently open? */
 Socket.prototype._wsOpen = function() {
-  return !!this._socket && this._socket.readyState == 1;
-};
+  return this._socket && this._socket.readyState == 1;
+}
+
+/** Send queued messages. */
+Socket.prototype._sendQueuedMessages = function() {
+  for (var i = 0, ii = this._queue.length; i < ii; i += 1) {
+    this.send(this._queue[i]);
+  }
+}
 
 /** Exposed send for DC & Peer. */
 Socket.prototype.send = function(data) {
@@ -155,6 +168,13 @@ Socket.prototype.send = function(data) {
     return;
   }
 
+  // If we didn't get an ID yet, we can't yet send anything so we should queue
+  // up these messages.
+  if (!this.id) {
+    this._queue.push(data);
+    return;
+  }
+
   if (!data.type) {
     this.emit('error', 'Invalid message');
     return;
@@ -170,11 +190,11 @@ Socket.prototype.send = function(data) {
     http.setRequestHeader('Content-Type', 'application/json');
     http.send(message);
   }
-};
+}
 
 Socket.prototype.close = function() {
   if (!this.disconnected && this._wsOpen()) {
     this._socket.close();
     this.disconnected = true;
   }
-};
+}

+ 181 - 23
lib/util.js

@@ -1,12 +1,183 @@
+var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]};
 var util = {
+  noop: function() {},
+
+  CLOUD_HOST: '0.peerjs.com',
+  CLOUD_PORT: 9000,
+
+  // Logging logic
+  logLevel: 0,
+  setLogLevel: function(level) {
+    var debugLevel = parseInt(level, 10);
+    if (!isNaN(parseInt(level, 10))) {
+      util.logLevel = debugLevel;
+    } else {
+      // If they are using truthy/falsy values for debug
+      util.logLevel = level ? 3 : 0;
+    }
+    util.log = util.warn = util.error = util.noop;
+    if (util.logLevel > 0) {
+      util.error = util._printWith('ERROR');
+    }
+    if (util.logLevel > 1) {
+      util.warn = util._printWith('WARNING');
+    }
+    if (util.logLevel > 2) {
+      util.log = util._print;
+    }
+  },
+  setLogFunction: function(fn) {
+    if (fn.constructor !== Function) {
+      util.warn('The log function you passed in is not a function. Defaulting to regular logs.');
+    } else {
+      util._print = fn;
+    }
+  },
+
+  _printWith: function(prefix) {
+    return function() {
+      var copy = Array.prototype.slice.call(arguments);
+      copy.unshift(prefix);
+      util._print.apply(util, copy);
+    };
+  },
+  _print: function () {
+    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);  
+  },
+  //
+
+  // Returns browser-agnostic default config
+  defaultConfig: defaultConfig,
+  //
+
+  // Returns the current browser.
+  browser: (function() {
+    if (window.mozRTCPeerConnection) {
+      return 'Firefox';
+    } else if (window.webkitRTCPeerConnection) {
+      return 'Chrome';
+    } else if (window.RTCPeerConnection) {
+      return 'Supported';
+    } else {
+      return 'Unsupported';
+    }
+  })(),
+  //
+
+  // Lists which features are supported
+  supports: (function() {
+    var data = true;
+    var audioVideo = true;
+
+    var pc, dc;
+    try {
+      pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
+    } catch (e) {
+      data = false;
+      audioVideo = false;
+    }
+
+    if (data) {
+      try {
+        dc = pc.createDataChannel('_PEERJSDATATEST');
+      } catch (e) {
+        data = false;
+      }
+    }
+    // FIXME: not really the best check...
+    if (audioVideo) {
+      audioVideo = !!pc.addStream;
+    }
+
+    pc.close();
+    dc.close();
+
+    return {
+      audioVideo: audioVideo,
+      data: data,
+      binary: data && (function() {
+        var pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
+        var dc = pc.createDataChannel('_PEERJSBINARYTEST');
+
+        try {
+          dc.binaryType = 'blob';
+        } catch (e) {
+          pc.close();
+          if (e.name === 'NotSupportedError') {
+            return false
+          }
+        }
+        pc.close();
+        dc.close();
+
+        return true;
+      })(),
+
+      reliable: data && (function() {
+        // Reliable (not RTP).
+        var pc = new RTCPeerConnection(defaultConfig, {});
+        var dc;
+        try {
+          dc = pc.createDataChannel('_PEERJSRELIABLETEST', {maxRetransmits: 0});
+        } catch (e) {
+          pc.close();
+          if (e.name === 'NotSupportedError') {
+            return false
+          }
+        }
+        pc.close();
+        dc.close();
+
+        return true;
+      })(),
+
+      onnegotiationneeded: (data || audioVideo) && (function() {
+        var pc = new RTCPeerConnection(defaultConfig, {});
+        // sync default check.
+        var called = false;
+        var pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
+        pc.onnegotiationneeded = function() {
+          called = true;
+          // async check.
+          if (util && util.supports) {
+            util.supports.onnegotiationneeded = true;
+          }
+        };
+        // FIXME: this is not great because in theory it doesn't work for
+        // av-only browsers (?).
+        var dc = pc.createDataChannel('_PEERJSRELIABLETEST');
+
+        pc.close();
+        dc.close();
+
+        return called;
+      })()
+    };
+  }()),
+  //
+
+  // Ensure alphanumeric ids
+  validateId: function(id) {
+    // Allow empty ids
+    return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id);
+  },
+
+  validateKey: function(key) {
+    // Allow empty keys
+    return !key || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(key);
+  },
 
-  chromeCompatible: true,
-  firefoxCompatible: true,
-  chromeVersion: 26,
-  firefoxVersion: 22,
 
   debug: false,
-  browserisms: '',
 
   inherits: function(ctor, superCtor) {
     ctor.super_ = superCtor;
@@ -75,6 +246,7 @@ var util = {
     return setZeroTimeoutPostMessage;
   }(this)),
 
+  // Binary stuff
   blobToArrayBuffer: function(blob, cb){
     var fr = new FileReader();
     fr.onload = function(evt) {
@@ -99,25 +271,11 @@ var util = {
   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:';
   }
 };
+
+exports.util = util;

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "peerjs",
-  "version": "0.2.8",
+  "version": "0.3.0",
   "description": "PeerJS client library",
   "devDependencies": {
     "uglify-js": "~1.3.4",

+ 0 - 4
test/adapter.js

@@ -1,9 +1,5 @@
 describe('adapter', function() {
 
-  it('sets browerisms', function() {
-    expect(exports.util.browserisms).to.match(/^Firefox||Webkit$/);
-  });
-
   it('sets RTCPeerConnection', function() {
     expect(RTCPeerConnection).to.be.a('function');
   });

+ 0 - 41
test/connectionmanager.js

@@ -1,41 +0,0 @@
-describe('ConnectionManager', function() {
-
-  describe('constructor', function() {
-  });
-
-  it('inherits from EventEmitter');
-
-  describe('#_setupDataChannel', function() {
-  });
-
-  describe('#_makeOffer', function() {
-  });
-
-  describe('#_makeAnswer', function() {
-  });
-
-  describe('#_cleanup', function() {
-  });
-
-  describe('#_attachConnectionListeners', function() {
-  });
-
-  describe('#handleSDP', function() {
-  });
-
-  describe('#handleCandidate', function() {
-  });
-
-  describe('#handleLeave', function() {
-  });
-
-  describe('#close', function() {
-  });
-
-  describe('#connect', function() {
-  });
-
-  describe('#update', function() {
-  });
-
-});

+ 0 - 126
test/dataconnection.js

@@ -1,126 +0,0 @@
-describe('DataConnection', function() {
-
-  // Only run tests on compatible browser.
-  if (util.isBrowserCompatible()) {
-
-    var dc, pdc;
-
-    function DataChannelStub() {};
-    DataChannelStub.prototype = {
-      close: function() {
-        if (this.readyState === 'closed') {
-          throw Error();
-        }
-        this.readyState = 'closed';
-      },
-      // Only sends to peer's dc.
-      send: function(msg) {
-        pdc._handleDataMessage({ data: msg });
-      }
-    };
-
-    describe('constructor', function() {
-      it('should accept options properly', function() {
-        // Test without 'new' keyword.
-        dc = DataConnection('peer', null,
-          { serialization: 'json',
-            metadata: { message: 'I\'m testing!'} });
-
-        expect(dc.peer).to.be('peer');
-        expect(dc.serialization).to.be('json');
-        expect(dc.metadata.message).to.be('I\'m testing!');
-
-        expect(dc._dc).to.be(null);
-        dc._dc = new DataChannelStub();
-      });
-    });
-
-    it('inherits from EventEmitter');
-
-    before(function() {
-      dc = DataConnection('peer', null,
-        { serialization: 'json',
-          metadata: { message: 'I\'m testing!'} });
-      dc._dc = new DataChannelStub();
-    });
-
-    describe('#_configureDataChannel', function() {
-      it('should set the correct binaryType', function() {
-        dc._configureDataChannel();
-
-        if (util.browserisms === 'Firefox') {
-          expect(dc._dc.binaryType).to.be('arraybuffer');
-        } else {
-          expect(dc._reliable).to.be(undefined);
-        }
-      });
-
-      it('should fire an `open` event', function(done) {
-        dc.on('open', function() {
-          expect(dc.open).to.be(true)
-          done();
-        });
-        dc._dc.onopen();
-      });
-    });
-
-    describe('#_handleDataMessage', function() {
-
-    });
-
-    describe('#addDC', function() {
-      it('should add a DataConnection properly', function() {
-        pdc = new DataConnection('ignore', null, { serialization: 'json', reliable: true });
-        pdc.addDC(new DataChannelStub());
-
-        expect(pdc._dc).not.to.be(null);
-      });
-    });
-
-    describe('#send', function() {
-      it('should send data to the peer', function(done) {
-        pdc = new DataConnection('ignore', null, { serialization: 'json' });
-        pdc.on('data', function(data) {
-          expect(data.hello).to.be('peer-tester');
-          done();
-        });
-        dc.send({ hello: 'peer-tester' });
-      });
-    });
-
-    describe('#_cleanup', function() {
-      it('should emit a `close` event', function(done) {
-        var first = true;
-        dc.on('close', function() {
-          expect(dc.open).to.be(false)
-
-          // Calling it twice should be fine.
-          if (first) {
-            first = false;
-            dc._cleanup();
-          }
-
-          done();
-        });
-
-        dc._cleanup();
-      });
-    });
-
-    // Hacks hacks
-    describe('#close', function() {
-      it('should not call _cleanup', function() {
-        dc._cleanup = function() {
-          throw Error();
-        }
-
-        // Should not call _cleanup again.
-        dc.close();
-      });
-    });
-
-
-  } else {
-    it('should not work.', function() {});
-  }
-});

+ 0 - 16
test/peer.js

@@ -1,16 +0,0 @@
-describe('Peer', function() {
-
-  describe('constructor', function() {
-  });
-
-  it('inherits from EventEmitter');
-
-  describe('.browser', function() {
-    it('should be the current browser', function() {
-      var browser = window.mozRTCPeerConnection ? 'Firefox' : 'Unknown';
-      browser = window.webkitRTCPeerConnection ? 'Webkit' : browser;
-      expect(Peer.browser).to.be(browser);
-    });
-  });
-
-});

+ 0 - 32
test/socket.js

@@ -1,32 +0,0 @@
-describe('Socket', function() {
-
-  describe('constructor', function() {
-  });
-
-  it('inherits from EventEmitter');
-
-  describe('#start', function() {
-  });
-
-  describe('#_startWebSocket', function() {
-  });
-
-  describe('#_startXhrStream', function() {
-  });
-
-  describe('#_handleStream', function() {
-  });
-
-  describe('#_setHTTPTimeout', function() {
-  });
-
-  describe('#send', function() {
-  });
-
-  describe('#close', function() {
-  });
-
-  describe('#_wsOpen', function() {
-  });
-
-});

+ 1 - 1
test/test.html

@@ -12,8 +12,8 @@
   <script src="../deps/js-binarypack/lib/bufferbuilder.js"></script>
   <script src="../deps/js-binarypack/lib/binarypack.js"></script>
   <script src="../deps/EventEmitter/EventEmitter.js"></script>
-  <script src="../lib/util.js"></script>
   <script src="../lib/adapter.js"></script>
+  <script src="../lib/util.js"></script>
   <script src="../lib/dataconnection.js"></script>
   <script src="../lib/peer.js"></script>
   <script src="../lib/socket.js"></script>

Some files were not shown because too many files changed in this diff