diff --git a/README.webrtc b/README.webrtc
new file mode 100644
index 0000000..858299e
--- /dev/null
+++ b/README.webrtc
@@ -0,0 +1,82 @@
+
+SylkServer WebRTC gateway
+-------------------------
+
+The WebRTC gateway application enables web based clients to communicate
+transparently with SIP endpoints using WebRTC suite of standards and
+protocols.
+
+To perform this function SylkServer must run on a computer with a public
+reachable IP adddress.
+
+
+Architecture
+------------
+
++--------+               +-------------+ +----------+     +-------------+
+|        |               |             | |          |     |     SIP     |
+| WebRTC |  SylkRTC API  |             | |  Janus   +<--->+ audio/video |
+| client +<------------->+  SylkServer | |          |     |    calls    |
+|        |               |   WebRTC    | +----------+     +-------------+
++--------+               |   gateway   | +----------+     +-------------+
+                         |             | | Presence |     |   SIMPLE    |
+                         |             | | and chat +<--->+     IM      |
+                         |             | | gateway  |     |   Presence  |
+                         +-------------+ +----------+     +-------------+
+
+
+End-point requirements
+----------------------
+
+The WebRTC gateway doesn't do media transcoding, so after taking care of the
+WebRTC-specific media features (ICE and DTLS mainly), endpoints will
+exchange plain RTP media between them, therefore SIP end-points must have a
+set of compatible audio (Opus) and video codecs (H.264 and VP8).
+
+As a WebRTC client, a standard browser with WebRTC support can be used
+(e.g.  Firefox or Google Chrome).
+
+
+Features
+--------
+
+ * SIP account registration
+ * Audio / video calling
+ * Codec agnostic
+
+
+Roadmap
+-------
+
+ * Chat sessions
+ * Presence
+ * Contacts management
+ * SDES Encryption
+
+
+SylkRTC API
+-----------
+
+A JavaScript library implementing SylkRTC API is available at:
+
+http://projects.ag-projects.com/projects/sylkrtc
+
+
+Janus
+-----
+
+For implementing WebRTC capabilities, SylkServer uses an external component
+called Janus.
+
+https://github.com/meetecho/janus-gateway
+
+Janus can be installed by following the instructions on its README file:
+
+https://github.com/meetecho/janus-gateway/blob/master/README.md
+
+The only requirement is that Janus is compiled with WebSocket supports,
+since that's the transport SylkServer uses in order to communicate with
+Janus.
+
+Note: libsrtp >= 1.5.0 is recommended.
+
diff --git a/TODO b/TODO
index f96a0e6..325e5e2 100644
--- a/TODO
+++ b/TODO
@@ -1,8 +1,7 @@
 Roadmap
 -------
 
  - Registrar application
  - Presence Agent application
  - Offline storage application
- - WebRTC gateway
 
diff --git a/debian/control b/debian/control
index 8d11f77..1916b10 100644
--- a/debian/control
+++ b/debian/control
@@ -1,26 +1,35 @@
 Source: sylkserver
 Section: net
 Priority: optional
 Maintainer: Saul Ibarra <saul@ag-projects.com>
 Uploaders: Dan Pascu <dan@ag-projects.com>, Adrian Georgescu <ag@ag-projects.com>
 Build-Depends: debhelper (>= 7.3.5), dh-python, dh-systemd, python-all (>= 2.7)
 Standards-Version: 3.9.6
 
 Package: sylkserver
 Architecture: all
 Depends: ${python:Depends}, ${misc:Depends}, python-application (>= 1.4.0), python-eventlib, python-lxml, python-sipsimple (>= 2.4.0), python-twisted-web, python-werkzeug
-Suggests: libavahi-compat-libdnssd1, python-twisted-words, python-wokkel (>= 0.7.0)
+Suggests: libavahi-compat-libdnssd1, python-twisted-words, python-wokkel (>= 0.7.0), python-autobahn (>= 0.10.3), janus
 Recommends: sylkserver-sounds
 Description: A state of the art, extensible SIP Application Server
  SylkServer is an application server that can be programmed to perform SIP
  end-point applications and act as a gateway between SIP and XMPP domains.
 
 Package: sylkserver-sounds
 Architecture: all
 Depends: ${misc:Depends}, sylkserver
 Description: A state of the art, extensible SIP Application Server
  SylkServer is an application server that can be programmed to perform SIP
  end-point applications and act as a gateway between SIP and XMPP domains.
  .
  This package contains sounds used by SylkServer.
 
+Package: sylkserver-webrtc-gateway
+Architecture: all
+Depends: ${misc:Depends}, sylkserver, janus, python-autobahn (>= 0.10.3)
+Description: A state of the art, extensible SIP Application Server
+ SylkServer is an application server that can be programmed to perform SIP
+ end-point applications and act as a gateway between SIP and XMPP domains.
+ .
+ This is a meta-package containing the dependencies required to run the
+ WebRTC gateway application.
diff --git a/debian/rules b/debian/rules
index f8aa5d4..e3a9221 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,18 +1,19 @@
 #!/usr/bin/make -f
 
 #export DH_VERBOSE=1
 
 %:
 	dh  $@ --with python2 --with systemd
 
 override_dh_clean:
 	dh_clean
 	rm -rf build dist MANIFEST
 
 override_dh_install:
+	install -m 0644 janus-config/* debian/sylkserver-webrtc-gateway/etc/janus/
 	dh_install
 
 override_dh_installinit:
 	dh_installinit --no-start
 
 .PHONY: override_dh_clean override_dh_install override_dh_installinit
diff --git a/debian/sylkserver-webrtc-gateway.dirs b/debian/sylkserver-webrtc-gateway.dirs
new file mode 100644
index 0000000..f22849c
--- /dev/null
+++ b/debian/sylkserver-webrtc-gateway.dirs
@@ -0,0 +1 @@
+etc/janus
diff --git a/debian/sylkserver.install b/debian/sylkserver.install
index 25dfca5..bef76d0 100644
--- a/debian/sylkserver.install
+++ b/debian/sylkserver.install
@@ -1,3 +1,4 @@
 usr/bin
 usr/lib
 etc/sylkserver
+usr/share/sylkserver/html
diff --git a/janus-config/janus.cfg b/janus-config/janus.cfg
new file mode 100644
index 0000000..17b51db
--- /dev/null
+++ b/janus-config/janus.cfg
@@ -0,0 +1,30 @@
+; Janus configuration file for use with SylkServer
+
+[general]
+; Configuration files folder
+configs_folder = /etc/janus
+
+; Plugins folder
+plugins_folder = /usr/lib/janus/plugins
+
+; Interface to use (will be used in SDP)
+;interface =
+
+; Debug/logging level, valid values are 0-7
+debug_level = 3
+
+[webserver]
+http = no
+https = no
+ws = yes
+ws_port = 8188
+ws_ssl = no
+
+[certificates]
+; Certificate and key to use for DTLS and/or HTTPS/WSS.
+cert_pem = /usr/share/janus/certs/mycert.pem
+cert_key = /usr/share/janus/certs/mycert.key
+
+[plugins]
+disable = libjanus_voicemail.so,libjanus_recordplay.so,libjanus_streaming.so,libjanus_echotest.so,libjanus_videocall.so,libjanus_videoroom.so
+
diff --git a/janus-config/janus.plugin.sip.cfg b/janus-config/janus.plugin.sip.cfg
new file mode 100644
index 0000000..f634880
--- /dev/null
+++ b/janus-config/janus.plugin.sip.cfg
@@ -0,0 +1,18 @@
+; Janus configuration file for use with SylkServer
+
+[general]
+; Specify which local IP address to use. If not set it will be automatically
+; guessed from the system
+; local_ip =
+
+; Enable local keep-alives to keep the registration open. Keep-alives are
+; sent in the form of OPTIONS requests, at the given interval inseconds.
+; (0 to disable)
+keepalive_interval = 0
+
+; User-Agent string to be used
+user_agent = SylkServer (WebRTC gateway)
+
+; Default expiration time for registrations
+register_ttl = 600
+
diff --git a/resources/html/webrtcgateway/index.html b/resources/html/webrtcgateway/index.html
new file mode 100644
index 0000000..443f897
--- /dev/null
+++ b/resources/html/webrtcgateway/index.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>SylkServer WebRTC gateway</title>
+
+    <!-- Bootstrap -->
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
+
+  </head>
+  <body>
+      <div class="container">
+          <h1>SylkServer WebRTC gateway</h1>
+          <h2>Overview</h2>
+          <p>
+              Welcome to the SylkServer WebRTC gateway application!
+          </p>
+          <p>
+              This application enables web based clients to communicate
+              transparently with SIP endpoints using <a href="http://www.w3.org/TR/webrtc/">WebRTC</a>
+              suite of standards and protocols.
+          </p>
+          <h3>Features</h3>
+              <ul>
+                  <li>SIP account registration</li>
+                  <li>Audio and video calls</li>
+              </ul>
+          <h3>Server software</h3>
+              <ul>
+                  <li><a href="http://sylkserver.com">SylkServer</a> &#8212; If you see this page you are running SylkServer!</li>
+                  <li><a href="https://github.com/meetecho/janus-gateway">Janus</a> &#8212; WebRTC backend</li>
+              </ul>
+          <h3>Client software</h3>
+              <ul>
+                  <li><a href="http://projects.ag-projects.com/projects/sylkserver/wiki/WebRTC">SylkRTC API</a> &#8212; specification of the API implemented by SylkServer WebRTC gateway</li>
+                  <li><a href="http://projects.ag-projects.com/projects/sylkrtc">sylkrtc.js</a> &#8212; a JavaScript client library implementing SylkRTC API, for embedding it into a web application </li>
+                  <li><a href="test">SylkRTC Test suite</a> &#8212; sample application using sylkrtc.js and <a href="http://facebook.github.io/react/">React framework</a> to test this installation</li>
+              </ul>
+          <h3>Live service</h3>
+              <ul>
+                  <li><a href="https://webrtc.sipthor.net">SIP2SIP WebRTC client</a>: an implementation in production at <a href="http://sip2sip.info">sip2sip.info</a></li>
+              </ul>
+          </p>
+      </div>
+  </body>
+</html>
diff --git a/resources/html/webrtcgateway/js/sylkrtc.js b/resources/html/webrtcgateway/js/sylkrtc.js
new file mode 100644
index 0000000..bfaef70
--- /dev/null
+++ b/resources/html/webrtcgateway/js/sylkrtc.js
@@ -0,0 +1,4454 @@
+/*
+ * sylkrtc.js v0.0.1
+ * SylkServer WebRTC Gateway client library
+ * Copyright 2015 AG Projects
+ * License MIT
+ */
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.sylkrtc = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+    value: true
+});
+
+var _createClass = (function () {
+    function defineProperties(target, props) {
+        for (var i = 0; i < props.length; i++) {
+            var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ('value' in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);
+        }
+    }return function (Constructor, protoProps, staticProps) {
+        if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;
+    };
+})();
+
+var _get = function get(_x2, _x3, _x4) {
+    var _again = true;_function: while (_again) {
+        var object = _x2,
+            property = _x3,
+            receiver = _x4;desc = parent = getter = undefined;_again = false;if (object === null) object = Function.prototype;var desc = Object.getOwnPropertyDescriptor(object, property);if (desc === undefined) {
+            var parent = Object.getPrototypeOf(object);if (parent === null) {
+                return undefined;
+            } else {
+                _x2 = parent;_x3 = property;_x4 = receiver;_again = true;continue _function;
+            }
+        } else if ('value' in desc) {
+            return desc.value;
+        } else {
+            var getter = desc.get;if (getter === undefined) {
+                return undefined;
+            }return getter.call(receiver);
+        }
+    }
+};
+
+function _interopRequireDefault(obj) {
+    return obj && obj.__esModule ? obj : { 'default': obj };
+}
+
+function _classCallCheck(instance, Constructor) {
+    if (!(instance instanceof Constructor)) {
+        throw new TypeError('Cannot call a class as a function');
+    }
+}
+
+function _inherits(subClass, superClass) {
+    if (typeof superClass !== 'function' && superClass !== null) {
+        throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass);
+    }subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });if (superClass) subClass.__proto__ = superClass;
+}
+
+var _debug = require('debug');
+
+var _debug2 = _interopRequireDefault(_debug);
+
+var _blueimpMd5 = require('blueimp-md5');
+
+var _events = require('events');
+
+var _call = require('./call');
+
+var DEBUG = (0, _debug2['default'])('sylkrtc:Account');
+
+var Account = (function (_EventEmitter) {
+    _inherits(Account, _EventEmitter);
+
+    function Account(id, password, connection) {
+        _classCallCheck(this, Account);
+
+        if (id.indexOf('@') === -1) {
+            throw new Error('Invalid account id specified');
+        }
+        _get(Object.getPrototypeOf(Account.prototype), 'constructor', this).call(this);
+        var username = id.substring(0, id.indexOf('@'));
+        var domain = id.substring(id.indexOf('@') + 1);
+        this._id = id;
+        this._password = (0, _blueimpMd5.md5)(username + ':' + domain + ':' + password);
+        this._connection = connection;
+        this._registrationState = null;
+        this._calls = new Map();
+    }
+
+    _createClass(Account, [{
+        key: 'register',
+        value: function register() {
+            var _this = this;
+
+            var req = {
+                sylkrtc: 'account-register',
+                account: this._id
+            };
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('Register error: %s', error);
+                    var oldState = _this._registrationState;
+                    var newState = 'failed';
+                    var data = { reason: error.toString() };
+                    _this._registrationState = newState;
+                    _this.emit('registrationStateChanged', oldState, newState, data);
+                }
+            });
+        }
+    }, {
+        key: 'unregister',
+        value: function unregister() {
+            var _this2 = this;
+
+            var req = {
+                sylkrtc: 'account-unregister',
+                account: this._id
+            };
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('Unregister error: %s', error);
+                }
+                var oldState = _this2._registrationState;
+                var newState = null;
+                _this2._registrationState = newState;
+                _this2.emit('registrationStateChanged', oldState, newState, {});
+            });
+        }
+    }, {
+        key: 'call',
+        value: function call(uri) {
+            var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+            var call = new _call.Call(this);
+            call._initOutgoing(uri, options);
+            this._calls.set(call.id, call);
+            this.emit('outgoingCall', call);
+            return call;
+        }
+    }, {
+        key: '_handleEvent',
+
+        // Private API
+
+        value: function _handleEvent(message) {
+            DEBUG('Received account event: %s', message.event);
+            switch (message.event) {
+                case 'registration_state':
+                    var oldState = this._registrationState;
+                    var newState = message.data.state;
+                    var data = {};
+                    this._registrationState = newState;
+                    if (newState === 'failed') {
+                        data.reason = message.data.reason;
+                    }
+                    this.emit('registrationStateChanged', oldState, newState, data);
+                    break;
+                case 'incoming_session':
+                    var call = new _call.Call(this);
+                    call._initIncoming(message.session, message.data.originator, message.data.sdp);
+                    this._calls.set(call.id, call);
+                    this.emit('incomingCall', call);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }, {
+        key: '_sendRequest',
+        value: function _sendRequest(req, cb) {
+            this._connection._sendRequest(req, cb);
+        }
+    }, {
+        key: 'id',
+        get: function get() {
+            return this._id;
+        }
+    }, {
+        key: 'password',
+        get: function get() {
+            return this._password;
+        }
+    }, {
+        key: 'registrationState',
+        get: function get() {
+            return this._registrationState;
+        }
+    }]);
+
+    return Account;
+})(_events.EventEmitter);
+
+exports.Account = Account;
+
+},{"./call":2,"blueimp-md5":5,"debug":9,"events":6}],2:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+    value: true
+});
+
+var _createClass = (function () {
+    function defineProperties(target, props) {
+        for (var i = 0; i < props.length; i++) {
+            var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ('value' in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);
+        }
+    }return function (Constructor, protoProps, staticProps) {
+        if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;
+    };
+})();
+
+var _get = function get(_x3, _x4, _x5) {
+    var _again = true;_function: while (_again) {
+        var object = _x3,
+            property = _x4,
+            receiver = _x5;desc = parent = getter = undefined;_again = false;if (object === null) object = Function.prototype;var desc = Object.getOwnPropertyDescriptor(object, property);if (desc === undefined) {
+            var parent = Object.getPrototypeOf(object);if (parent === null) {
+                return undefined;
+            } else {
+                _x3 = parent;_x4 = property;_x5 = receiver;_again = true;continue _function;
+            }
+        } else if ('value' in desc) {
+            return desc.value;
+        } else {
+            var getter = desc.get;if (getter === undefined) {
+                return undefined;
+            }return getter.call(receiver);
+        }
+    }
+};
+
+function _interopRequireDefault(obj) {
+    return obj && obj.__esModule ? obj : { 'default': obj };
+}
+
+function _classCallCheck(instance, Constructor) {
+    if (!(instance instanceof Constructor)) {
+        throw new TypeError('Cannot call a class as a function');
+    }
+}
+
+function _inherits(subClass, superClass) {
+    if (typeof superClass !== 'function' && superClass !== null) {
+        throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass);
+    }subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });if (superClass) subClass.__proto__ = superClass;
+}
+
+var _debug = require('debug');
+
+var _debug2 = _interopRequireDefault(_debug);
+
+var _nodeUuid = require('node-uuid');
+
+var _nodeUuid2 = _interopRequireDefault(_nodeUuid);
+
+var _rtcninja = require('rtcninja');
+
+var _rtcninja2 = _interopRequireDefault(_rtcninja);
+
+var _events = require('events');
+
+var DEBUG = (0, _debug2['default'])('sylkrtc:Call');
+
+var Call = (function (_EventEmitter) {
+    _inherits(Call, _EventEmitter);
+
+    function Call(account) {
+        _classCallCheck(this, Call);
+
+        _get(Object.getPrototypeOf(Call.prototype), 'constructor', this).call(this);
+        this._account = account;
+        this._id = null;
+        this._direction = null;
+        this._pc = null;
+        this._state = null;
+        this._terminated = false;
+        this._incomingSdp = null;
+        this._remoteIdentity = null;
+    }
+
+    _createClass(Call, [{
+        key: 'getLocalStreams',
+        value: function getLocalStreams() {
+            if (this._pc !== null) {
+                return this._pc.getLocalStreams();
+            } else {
+                return [];
+            }
+        }
+    }, {
+        key: 'getRemoteStreams',
+        value: function getRemoteStreams() {
+            if (this._pc !== null) {
+                return this._pc.getRemoteStreams();
+            } else {
+                return [];
+            }
+        }
+    }, {
+        key: 'answer',
+        value: function answer() {
+            var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+            if (this._state !== 'incoming') {
+                throw new Error('Call is not in the incoming state: ' + this._state);
+            }
+
+            var self = this;
+            var pcConfig = options.pcConfig || { iceServers: [] };
+            var mediaConstraints = options.mediaConstraints || { audio: true, video: true };
+            var answerOptions = options.answerOptions;
+
+            // Create the RTCPeerConnection
+            this._initRTCPeerConnection(pcConfig);
+
+            // Get the user media
+            _rtcninja2['default'].getUserMedia(mediaConstraints, userMediaSucceeded, userMediaFailed);
+
+            function userMediaSucceeded(stream) {
+                // adding a local stream doesn't trigger the 'onaddstream' callback
+                self._pc.addStream(stream);
+                self.emit('localStreamAdded', stream);
+
+                self._pc.setRemoteDescription(new _rtcninja2['default'].RTCSessionDescription({ type: 'offer', sdp: self._incomingSdp }),
+                // success
+                function () {
+                    self._createLocalSDP('answer', answerOptions,
+                    // success
+                    function (sdp) {
+                        DEBUG('Local SDP: %s', sdp);
+                        self._sendAnswer(sdp);
+                    },
+                    // failure
+                    function (error) {
+                        DEBUG('Error creating local SDP: %s', error);
+                        self.terminate();
+                    });
+                },
+                // failure
+                function (error) {
+                    DEBUG('Error setting remote description: %s', error);
+                    self.terminate();
+                });
+            }
+
+            function userMediaFailed(error) {
+                DEBUG('Error getting user media: %s', error);
+                self.terminate();
+            }
+        }
+    }, {
+        key: 'terminate',
+        value: function terminate() {
+            if (this._terminated) {
+                return;
+            }
+
+            this._sendTerminate();
+        }
+    }, {
+        key: '_initOutgoing',
+
+        // Private API
+
+        value: function _initOutgoing(uri) {
+            var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
+
+            if (uri.indexOf('@') === -1) {
+                throw new Error('Invalid URI');
+            }
+
+            this._id = _nodeUuid2['default'].v4();
+            this._direction = 'outgoing';
+
+            var self = this;
+            var pcConfig = options.pcConfig || { iceServers: [] };
+            var mediaConstraints = options.mediaConstraints || { audio: true, video: true };
+            var offerOptions = options.offerOptions;
+
+            // Create the RTCPeerConnection
+            this._initRTCPeerConnection(pcConfig);
+
+            // Get the user media
+            _rtcninja2['default'].getUserMedia(mediaConstraints, userMediaSucceeded, userMediaFailed);
+
+            function userMediaSucceeded(stream) {
+                // adding a local stream doesn't trigger the 'onaddstream' callback
+                self._pc.addStream(stream);
+                self.emit('localStreamAdded', stream);
+
+                self._createLocalSDP('offer', offerOptions,
+                // success
+                function (sdp) {
+                    DEBUG('Local SDP: %s', sdp);
+                    self._sendCall(uri, sdp);
+                },
+                // failure
+                function (error) {
+                    DEBUG('Error creating local SDP: %s', error);
+                    self._localTerminate(error);
+                });
+            }
+
+            function userMediaFailed(error) {
+                DEBUG('Error getting user media: %s', error);
+                self._localTerminate(error);
+            }
+        }
+    }, {
+        key: '_initIncoming',
+        value: function _initIncoming(id, caller, sdp) {
+            this._id = id;
+            this._remoteIdentity = caller;
+            this._incomingSdp = sdp;
+            this._direction = 'incoming';
+            this._state = 'incoming';
+        }
+    }, {
+        key: '_handleEvent',
+        value: function _handleEvent(message) {
+            var _this = this;
+
+            DEBUG('Call event: %o', message);
+            switch (message.event) {
+                case 'state':
+                    var oldState = this._state;
+                    var newState = message.data.state;
+                    this._state = newState;
+                    var data = {};
+
+                    if (newState === 'accepted' && this._direction === 'outgoing') {
+                        (function () {
+                            var self = _this;
+                            _this._pc.setRemoteDescription(new _rtcninja2['default'].RTCSessionDescription({ type: 'answer', sdp: message.data.sdp }),
+                            // success
+                            function () {
+                                DEBUG('Call accepted');
+                                self.emit('stateChanged', oldState, newState, data);
+                            },
+                            // failure
+                            function (error) {
+                                DEBUG('Error accepting call: %s', error);
+                                self.terminate();
+                            });
+                        })();
+                    } else {
+                        if (newState === 'terminated') {
+                            data.reason = message.data.reason;
+                            this._terminated = true;
+                        }
+                        this.emit('stateChanged', oldState, newState, data);
+                        if (newState === 'terminated') {
+                            this._closeRTCPeerConnection();
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }, {
+        key: '_initRTCPeerConnection',
+        value: function _initRTCPeerConnection(pcConfig) {
+            if (this._pc !== null) {
+                throw new Error('RTCPeerConnection already initialized');
+            }
+
+            var self = this;
+            this._pc = new _rtcninja2['default'].RTCPeerConnection(pcConfig);
+            this._pc.onaddstream = function (event, stream) {
+                DEBUG('Stream added');
+                self.emit('streamAdded', stream);
+            };
+            this._pc.onicecandidate = function (event) {
+                var candidate = null;
+                if (event.candidate !== null) {
+                    candidate = {
+                        'candidate': event.candidate.candidate,
+                        'sdpMid': event.candidate.sdpMid,
+                        'sdpMLineIndex': event.candidate.sdpMLineIndex
+                    };
+                    DEBUG('New ICE candidate %o', candidate);
+                }
+                self._sendTrickle(candidate);
+            };
+        }
+    }, {
+        key: '_createLocalSDP',
+        value: function _createLocalSDP(type, options, onSuccess, onFailure) {
+            var self = this;
+
+            if (type === 'offer') {
+                this._pc.createOffer(
+                // success
+                createSucceeded,
+                // failure
+                failure,
+                // options
+                options);
+            } else if (type === 'answer') {
+                this._pc.createAnswer(
+                // success
+                createSucceeded,
+                // failure
+                failure,
+                // options
+                options);
+            } else {
+                throw new Error('type must be "offer" or "answer", but "' + type + '" was given');
+            }
+
+            function createSucceeded(desc) {
+                self._pc.setLocalDescription(desc,
+                // success
+                function () {
+                    onSuccess(self._pc.localDescription.sdp);
+                },
+                // failure
+                failure);
+            }
+
+            function failure(error) {
+                onFailure(error);
+            }
+        }
+    }, {
+        key: '_sendRequest',
+        value: function _sendRequest(req, cb) {
+            this._account._sendRequest(req, cb);
+        }
+    }, {
+        key: '_sendCall',
+        value: function _sendCall(uri, sdp) {
+            var req = {
+                sylkrtc: 'session-create',
+                account: this.account.id,
+                session: this.id,
+                uri: uri,
+                sdp: sdp
+            };
+            var self = this;
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('Call error: %s', error);
+                    self._localTerminate(error);
+                }
+            });
+        }
+    }, {
+        key: '_sendTerminate',
+        value: function _sendTerminate() {
+            var req = {
+                sylkrtc: 'session-terminate',
+                session: this.id
+            };
+            var self = this;
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('Error terminating call: %s', error);
+                    self._localTerminate(error);
+                }
+                self._terminated = true;
+            });
+        }
+    }, {
+        key: '_sendTrickle',
+        value: function _sendTrickle(candidate) {
+            var req = {
+                sylkrtc: 'session-trickle',
+                session: this.id,
+                candidates: candidate !== null ? [candidate] : []
+            };
+            this._sendRequest(req, null);
+        }
+    }, {
+        key: '_sendAnswer',
+        value: function _sendAnswer(sdp) {
+            var req = {
+                sylkrtc: 'session-answer',
+                session: this.id,
+                sdp: sdp
+            };
+            var self = this;
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('Answer error: %s', error);
+                    self.terminate();
+                }
+            });
+        }
+    }, {
+        key: '_closeRTCPeerConnection',
+        value: function _closeRTCPeerConnection() {
+            DEBUG('Closing RTCPeerConnection');
+            if (this._pc !== null) {
+                var _iteratorNormalCompletion = true;
+                var _didIteratorError = false;
+                var _iteratorError = undefined;
+
+                try {
+                    for (var _iterator = this._pc.getLocalStreams()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+                        var stream = _step.value;
+
+                        _rtcninja2['default'].closeMediaStream(stream);
+                    }
+                } catch (err) {
+                    _didIteratorError = true;
+                    _iteratorError = err;
+                } finally {
+                    try {
+                        if (!_iteratorNormalCompletion && _iterator['return']) {
+                            _iterator['return']();
+                        }
+                    } finally {
+                        if (_didIteratorError) {
+                            throw _iteratorError;
+                        }
+                    }
+                }
+
+                var _iteratorNormalCompletion2 = true;
+                var _didIteratorError2 = false;
+                var _iteratorError2 = undefined;
+
+                try {
+                    for (var _iterator2 = this._pc.getRemoteStreams()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+                        var stream = _step2.value;
+
+                        _rtcninja2['default'].closeMediaStream(stream);
+                    }
+                } catch (err) {
+                    _didIteratorError2 = true;
+                    _iteratorError2 = err;
+                } finally {
+                    try {
+                        if (!_iteratorNormalCompletion2 && _iterator2['return']) {
+                            _iterator2['return']();
+                        }
+                    } finally {
+                        if (_didIteratorError2) {
+                            throw _iteratorError2;
+                        }
+                    }
+                }
+
+                this._pc.close();
+                this._pc = null;
+            }
+        }
+    }, {
+        key: '_localTerminate',
+        value: function _localTerminate(error) {
+            if (this._terminated) {
+                return;
+            }
+            this._account._calls['delete'](this.id);
+            this._terminated = true;
+            var oldState = this._state;
+            var newState = 'terminated';
+            var data = {
+                reason: error.toString()
+            };
+            this.emit('stateChanged', oldState, newState, data);
+            this._closeRTCPeerConnection();
+        }
+    }, {
+        key: 'account',
+        get: function get() {
+            return this._account;
+        }
+    }, {
+        key: 'id',
+        get: function get() {
+            return this._id;
+        }
+    }, {
+        key: 'direction',
+        get: function get() {
+            return this._direction;
+        }
+    }, {
+        key: 'state',
+        get: function get() {
+            return this._state;
+        }
+    }, {
+        key: 'localIdentity',
+        get: function get() {
+            return this._account.id;
+        }
+    }, {
+        key: 'remoteIdentity',
+        get: function get() {
+            return this._remoteIdentity;
+        }
+    }]);
+
+    return Call;
+})(_events.EventEmitter);
+
+exports.Call = Call;
+
+},{"debug":9,"events":6,"node-uuid":12,"rtcninja":15}],3:[function(require,module,exports){
+(function (process){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+    value: true
+});
+
+var _createClass = (function () {
+    function defineProperties(target, props) {
+        for (var i = 0; i < props.length; i++) {
+            var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ('value' in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);
+        }
+    }return function (Constructor, protoProps, staticProps) {
+        if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;
+    };
+})();
+
+var _get = function get(_x5, _x6, _x7) {
+    var _again = true;_function: while (_again) {
+        var object = _x5,
+            property = _x6,
+            receiver = _x7;desc = parent = getter = undefined;_again = false;if (object === null) object = Function.prototype;var desc = Object.getOwnPropertyDescriptor(object, property);if (desc === undefined) {
+            var parent = Object.getPrototypeOf(object);if (parent === null) {
+                return undefined;
+            } else {
+                _x5 = parent;_x6 = property;_x7 = receiver;_again = true;continue _function;
+            }
+        } else if ('value' in desc) {
+            return desc.value;
+        } else {
+            var getter = desc.get;if (getter === undefined) {
+                return undefined;
+            }return getter.call(receiver);
+        }
+    }
+};
+
+function _interopRequireDefault(obj) {
+    return obj && obj.__esModule ? obj : { 'default': obj };
+}
+
+function _classCallCheck(instance, Constructor) {
+    if (!(instance instanceof Constructor)) {
+        throw new TypeError('Cannot call a class as a function');
+    }
+}
+
+function _inherits(subClass, superClass) {
+    if (typeof superClass !== 'function' && superClass !== null) {
+        throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass);
+    }subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });if (superClass) subClass.__proto__ = superClass;
+}
+
+var _debug = require('debug');
+
+var _debug2 = _interopRequireDefault(_debug);
+
+var _nodeUuid = require('node-uuid');
+
+var _nodeUuid2 = _interopRequireDefault(_nodeUuid);
+
+var _events = require('events');
+
+var _timers = require('timers');
+
+var _websocket = require('websocket');
+
+var _account = require('./account');
+
+var SYLKRTC_PROTO = 'sylkRTC-1';
+var DEBUG = (0, _debug2['default'])('sylkrtc:Connection');
+var INITIAL_DELAY = 0.5 * 1000;
+
+var Connection = (function (_EventEmitter) {
+    _inherits(Connection, _EventEmitter);
+
+    function Connection() {
+        var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+        _classCallCheck(this, Connection);
+
+        if (!options.server) {
+            throw new Error('"server" must be specified');
+        }
+        _get(Object.getPrototypeOf(Connection.prototype), 'constructor', this).call(this);
+        this._wsUri = options.server;
+        this._sock = null;
+        this._state = null;
+        this._closed = false;
+        this._timer = null;
+        this._delay = INITIAL_DELAY;
+        this._accounts = new Map();
+        this._requests = new Map();
+    }
+
+    _createClass(Connection, [{
+        key: 'close',
+        value: function close() {
+            var _this = this;
+
+            if (this._closed) {
+                return;
+            }
+            this._closed = true;
+            if (this._timer) {
+                clearTimeout(this._timer);
+                this._timer = null;
+            }
+            if (this._sock) {
+                this._sock.close();
+                this._sock = null;
+            } else {
+                (0, _timers.setImmediate)(function () {
+                    _this._setState('closed');
+                });
+            }
+        }
+    }, {
+        key: 'addAccount',
+        value: function addAccount() {
+            var _this2 = this;
+
+            var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+            var cb = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
+
+            if (typeof options.account !== 'string' || typeof options.password !== 'string') {
+                throw new Error('Invalid options, "account" and "password" must be supplied');
+            }
+            if (this._accounts.has(options.account)) {
+                throw new Error('Account already added');
+            }
+
+            var acc = new _account.Account(options.account, options.password, this);
+            // add it early to the set so we don't add it more than once, ever
+            this._accounts.set(acc.id, acc);
+
+            var req = {
+                sylkrtc: 'account-add',
+                account: acc.id,
+                password: acc.password
+            };
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('add_account error: %s', error);
+                    _this2._accounts['delete'](acc.id);
+                    acc = null;
+                }
+                if (cb) {
+                    cb(error, acc);
+                }
+            });
+        }
+    }, {
+        key: 'removeAccount',
+        value: function removeAccount(account) {
+            var cb = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
+
+            var acc = this._accounts.get(account.id);
+            if (account !== acc) {
+                throw new Error('Unknown account');
+            }
+
+            // delete the account from the mapping, regardless of the result
+            this._accounts['delete'](account.id);
+
+            var req = {
+                sylkrtc: 'account-remove',
+                account: acc.id
+            };
+            this._sendRequest(req, function (error) {
+                if (error) {
+                    DEBUG('remove_account error: %s', error);
+                }
+                if (cb) {
+                    cb();
+                }
+            });
+        }
+    }, {
+        key: '_initialize',
+
+        // Private API
+
+        value: function _initialize() {
+            var _this3 = this;
+
+            if (this._sock !== null) {
+                throw new Error('WebSocket already initialized');
+            }
+            if (this._timer !== null) {
+                throw new Error('Initialize is in progress');
+            }
+
+            DEBUG('Initializing');
+
+            if (process.browser) {
+                window.addEventListener('beforeunload', function () {
+                    if (_this3._sock !== null) {
+                        var noop = function noop() {};
+                        _this3._sock.onerror = noop;
+                        _this3._sock.onmessage = noop;
+                        _this3._sock.onclose = noop;
+                        _this3._sock.close();
+                    }
+                });
+            }
+
+            this._timer = setTimeout(function () {
+                _this3._connect();
+            }, this._delay);
+        }
+    }, {
+        key: '_connect',
+        value: function _connect() {
+            var _this4 = this;
+
+            DEBUG('WebSocket connecting');
+            this._setState('connecting');
+
+            this._sock = new _websocket.w3cwebsocket(this._wsUri, SYLKRTC_PROTO);
+            this._sock.onopen = function () {
+                DEBUG('WebSocket connection open');
+                _this4._onOpen();
+            };
+            this._sock.onerror = function () {
+                DEBUG('WebSocket connection got error');
+            };
+            this._sock.onclose = function (event) {
+                DEBUG('WebSocket connection closed: %d: (reason="%s", clean=%s)', event.code, event.reason, event.wasClean);
+                _this4._onClose();
+            };
+            this._sock.onmessage = function (event) {
+                DEBUG('WebSocket received message: %o', event);
+                _this4._onMessage(event);
+            };
+        }
+    }, {
+        key: '_sendRequest',
+        value: function _sendRequest(req, cb) {
+            var transaction = _nodeUuid2['default'].v4();
+            req.transaction = transaction;
+            if (this._state !== 'ready') {
+                (0, _timers.setImmediate)(function () {
+                    cb(new Error('Connection is not ready'));
+                });
+                return;
+            }
+            this._requests.set(transaction, { req: req, cb: cb });
+            this._sock.send(JSON.stringify(req));
+        }
+    }, {
+        key: '_setState',
+        value: function _setState(newState) {
+            DEBUG('Set state: %s -> %s', this._state, newState);
+            var oldState = this._state;
+            this._state = newState;
+            this.emit('stateChanged', oldState, newState);
+        }
+    }, {
+        key: '_onOpen',
+
+        // WebSocket callbacks
+
+        value: function _onOpen() {
+            clearTimeout(this._timer);
+            this._timer = null;
+            this._delay = INITIAL_DELAY;
+            this._setState('connected');
+        }
+    }, {
+        key: '_onClose',
+        value: function _onClose() {
+            var _this5 = this;
+
+            this._sock = null;
+            if (this._timer) {
+                clearTimeout(this._timer);
+                this._timer = null;
+            }
+
+            // remove all accounts, the server no longer has them anyway
+            this._accounts.clear();
+
+            this._setState('disconnected');
+            if (!this._closed) {
+                this._delay = this._delay * 2;
+                if (this._delay > Number.MAX_VALUE) {
+                    this._delay = INITIAL_DELAY;
+                }
+                DEBUG('Retrying connection in %s seconds', this._delay / 1000);
+                this._timer = setTimeout(function () {
+                    _this5._connect();
+                }, this._delay);
+            } else {
+                this._setState('closed');
+            }
+        }
+    }, {
+        key: '_onMessage',
+        value: function _onMessage(event) {
+            var message = JSON.parse(event.data);
+            if (typeof message.sylkrtc === 'undefined') {
+                DEBUG('Unrecognized message received');
+                return;
+            }
+
+            DEBUG('Received "%s" message: %o', message.sylkrtc, message);
+
+            if (message.sylkrtc === 'event') {
+                DEBUG('Received event: "%s"', message.event);
+                switch (message.event) {
+                    case 'ready':
+                        this._setState('ready');
+                        break;
+                    default:
+                        break;
+                }
+            } else if (message.sylkrtc === 'account_event') {
+                var acc = this._accounts.get(message.account);
+                if (!acc) {
+                    DEBUG('Account %s not found', message.account);
+                    return;
+                }
+                acc._handleEvent(message);
+            } else if (message.sylkrtc === 'session_event') {
+                var sessionId = message.session;
+                var _iteratorNormalCompletion = true;
+                var _didIteratorError = false;
+                var _iteratorError = undefined;
+
+                try {
+                    for (var _iterator = this._accounts.values()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+                        var acc = _step.value;
+
+                        var call = acc._calls.get(sessionId);
+                        if (call) {
+                            call._handleEvent(message);
+                            break;
+                        }
+                    }
+                } catch (err) {
+                    _didIteratorError = true;
+                    _iteratorError = err;
+                } finally {
+                    try {
+                        if (!_iteratorNormalCompletion && _iterator['return']) {
+                            _iterator['return']();
+                        }
+                    } finally {
+                        if (_didIteratorError) {
+                            throw _iteratorError;
+                        }
+                    }
+                }
+            } else if (message.sylkrtc === 'ack' || message.sylkrtc === 'error') {
+                var transaction = message.transaction;
+                var data = this._requests.get(transaction);
+                if (!data) {
+                    DEBUG('Could not find transaction %s', transaction);
+                    return;
+                }
+                this._requests['delete'](transaction);
+                DEBUG('Received "%s" for request: %o', message.sylkrtc, data.req);
+                if (data.cb) {
+                    if (message.sylkrtc === 'ack') {
+                        data.cb(null);
+                    } else {
+                        data.cb(new Error(message.error));
+                    }
+                }
+            }
+        }
+    }, {
+        key: 'state',
+        get: function get() {
+            return this._state;
+        }
+    }]);
+
+    return Connection;
+})(_events.EventEmitter);
+
+exports.Connection = Connection;
+
+}).call(this,require('_process'))
+
+},{"./account":1,"_process":7,"debug":9,"events":6,"node-uuid":12,"timers":8,"websocket":20}],4:[function(require,module,exports){
+'use strict';
+
+Object.defineProperty(exports, '__esModule', {
+    value: true
+});
+
+function _interopRequireDefault(obj) {
+    return obj && obj.__esModule ? obj : { 'default': obj };
+}
+
+var _debug = require('debug');
+
+var _debug2 = _interopRequireDefault(_debug);
+
+var _rtcninja = require('rtcninja');
+
+var _rtcninja2 = _interopRequireDefault(_rtcninja);
+
+var _connection = require('./connection');
+
+// Public API
+
+function createConnection() {
+    var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+    if (!_rtcninja2['default'].hasWebRTC()) {
+        throw new Error('WebRTC support not detected');
+    }
+
+    var conn = new _connection.Connection(options);
+    conn._initialize();
+    return conn;
+}
+
+// Some proxied functions from rtcninja
+
+function isWebRTCSupported() {
+    return _rtcninja2['default'].hasWebRTC();
+}
+
+function attachMediaStream(element, stream) {
+    _rtcninja2['default'].attachMediaStream(element, stream);
+}
+
+function closeMediaStream(stream) {
+    _rtcninja2['default'].closeMediaStream(stream);
+}
+
+exports.createConnection = createConnection;
+exports.debug = _debug2['default'];
+exports.attachMediaStream = attachMediaStream;
+exports.closeMediaStream = closeMediaStream;
+exports.isWebRTCSupported = isWebRTCSupported;
+
+},{"./connection":3,"debug":9,"rtcninja":15}],5:[function(require,module,exports){
+/*
+ * JavaScript MD5 1.0.1
+ * https://github.com/blueimp/JavaScript-MD5
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ * 
+ * Based on
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*jslint bitwise: true */
+/*global unescape, define */
+
+(function ($) {
+    'use strict';
+
+    /*
+    * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+    * to work around bugs in some JS interpreters.
+    */
+    function safe_add(x, y) {
+        var lsw = (x & 0xFFFF) + (y & 0xFFFF),
+            msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+        return (msw << 16) | (lsw & 0xFFFF);
+    }
+
+    /*
+    * Bitwise rotate a 32-bit number to the left.
+    */
+    function bit_rol(num, cnt) {
+        return (num << cnt) | (num >>> (32 - cnt));
+    }
+
+    /*
+    * These functions implement the four basic operations the algorithm uses.
+    */
+    function md5_cmn(q, a, b, x, s, t) {
+        return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
+    }
+    function md5_ff(a, b, c, d, x, s, t) {
+        return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+    }
+    function md5_gg(a, b, c, d, x, s, t) {
+        return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+    }
+    function md5_hh(a, b, c, d, x, s, t) {
+        return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+    }
+    function md5_ii(a, b, c, d, x, s, t) {
+        return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+    }
+
+    /*
+    * Calculate the MD5 of an array of little-endian words, and a bit length.
+    */
+    function binl_md5(x, len) {
+        /* append padding */
+        x[len >> 5] |= 0x80 << (len % 32);
+        x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+        var i, olda, oldb, oldc, oldd,
+            a =  1732584193,
+            b = -271733879,
+            c = -1732584194,
+            d =  271733878;
+
+        for (i = 0; i < x.length; i += 16) {
+            olda = a;
+            oldb = b;
+            oldc = c;
+            oldd = d;
+
+            a = md5_ff(a, b, c, d, x[i],       7, -680876936);
+            d = md5_ff(d, a, b, c, x[i +  1], 12, -389564586);
+            c = md5_ff(c, d, a, b, x[i +  2], 17,  606105819);
+            b = md5_ff(b, c, d, a, x[i +  3], 22, -1044525330);
+            a = md5_ff(a, b, c, d, x[i +  4],  7, -176418897);
+            d = md5_ff(d, a, b, c, x[i +  5], 12,  1200080426);
+            c = md5_ff(c, d, a, b, x[i +  6], 17, -1473231341);
+            b = md5_ff(b, c, d, a, x[i +  7], 22, -45705983);
+            a = md5_ff(a, b, c, d, x[i +  8],  7,  1770035416);
+            d = md5_ff(d, a, b, c, x[i +  9], 12, -1958414417);
+            c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
+            b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
+            a = md5_ff(a, b, c, d, x[i + 12],  7,  1804603682);
+            d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
+            c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
+            b = md5_ff(b, c, d, a, x[i + 15], 22,  1236535329);
+
+            a = md5_gg(a, b, c, d, x[i +  1],  5, -165796510);
+            d = md5_gg(d, a, b, c, x[i +  6],  9, -1069501632);
+            c = md5_gg(c, d, a, b, x[i + 11], 14,  643717713);
+            b = md5_gg(b, c, d, a, x[i],      20, -373897302);
+            a = md5_gg(a, b, c, d, x[i +  5],  5, -701558691);
+            d = md5_gg(d, a, b, c, x[i + 10],  9,  38016083);
+            c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
+            b = md5_gg(b, c, d, a, x[i +  4], 20, -405537848);
+            a = md5_gg(a, b, c, d, x[i +  9],  5,  568446438);
+            d = md5_gg(d, a, b, c, x[i + 14],  9, -1019803690);
+            c = md5_gg(c, d, a, b, x[i +  3], 14, -187363961);
+            b = md5_gg(b, c, d, a, x[i +  8], 20,  1163531501);
+            a = md5_gg(a, b, c, d, x[i + 13],  5, -1444681467);
+            d = md5_gg(d, a, b, c, x[i +  2],  9, -51403784);
+            c = md5_gg(c, d, a, b, x[i +  7], 14,  1735328473);
+            b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
+
+            a = md5_hh(a, b, c, d, x[i +  5],  4, -378558);
+            d = md5_hh(d, a, b, c, x[i +  8], 11, -2022574463);
+            c = md5_hh(c, d, a, b, x[i + 11], 16,  1839030562);
+            b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
+            a = md5_hh(a, b, c, d, x[i +  1],  4, -1530992060);
+            d = md5_hh(d, a, b, c, x[i +  4], 11,  1272893353);
+            c = md5_hh(c, d, a, b, x[i +  7], 16, -155497632);
+            b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
+            a = md5_hh(a, b, c, d, x[i + 13],  4,  681279174);
+            d = md5_hh(d, a, b, c, x[i],      11, -358537222);
+            c = md5_hh(c, d, a, b, x[i +  3], 16, -722521979);
+            b = md5_hh(b, c, d, a, x[i +  6], 23,  76029189);
+            a = md5_hh(a, b, c, d, x[i +  9],  4, -640364487);
+            d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
+            c = md5_hh(c, d, a, b, x[i + 15], 16,  530742520);
+            b = md5_hh(b, c, d, a, x[i +  2], 23, -995338651);
+
+            a = md5_ii(a, b, c, d, x[i],       6, -198630844);
+            d = md5_ii(d, a, b, c, x[i +  7], 10,  1126891415);
+            c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
+            b = md5_ii(b, c, d, a, x[i +  5], 21, -57434055);
+            a = md5_ii(a, b, c, d, x[i + 12],  6,  1700485571);
+            d = md5_ii(d, a, b, c, x[i +  3], 10, -1894986606);
+            c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
+            b = md5_ii(b, c, d, a, x[i +  1], 21, -2054922799);
+            a = md5_ii(a, b, c, d, x[i +  8],  6,  1873313359);
+            d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
+            c = md5_ii(c, d, a, b, x[i +  6], 15, -1560198380);
+            b = md5_ii(b, c, d, a, x[i + 13], 21,  1309151649);
+            a = md5_ii(a, b, c, d, x[i +  4],  6, -145523070);
+            d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
+            c = md5_ii(c, d, a, b, x[i +  2], 15,  718787259);
+            b = md5_ii(b, c, d, a, x[i +  9], 21, -343485551);
+
+            a = safe_add(a, olda);
+            b = safe_add(b, oldb);
+            c = safe_add(c, oldc);
+            d = safe_add(d, oldd);
+        }
+        return [a, b, c, d];
+    }
+
+    /*
+    * Convert an array of little-endian words to a string
+    */
+    function binl2rstr(input) {
+        var i,
+            output = '';
+        for (i = 0; i < input.length * 32; i += 8) {
+            output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);
+        }
+        return output;
+    }
+
+    /*
+    * Convert a raw string to an array of little-endian words
+    * Characters >255 have their high-byte silently ignored.
+    */
+    function rstr2binl(input) {
+        var i,
+            output = [];
+        output[(input.length >> 2) - 1] = undefined;
+        for (i = 0; i < output.length; i += 1) {
+            output[i] = 0;
+        }
+        for (i = 0; i < input.length * 8; i += 8) {
+            output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32);
+        }
+        return output;
+    }
+
+    /*
+    * Calculate the MD5 of a raw string
+    */
+    function rstr_md5(s) {
+        return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
+    }
+
+    /*
+    * Calculate the HMAC-MD5, of a key and some data (raw strings)
+    */
+    function rstr_hmac_md5(key, data) {
+        var i,
+            bkey = rstr2binl(key),
+            ipad = [],
+            opad = [],
+            hash;
+        ipad[15] = opad[15] = undefined;
+        if (bkey.length > 16) {
+            bkey = binl_md5(bkey, key.length * 8);
+        }
+        for (i = 0; i < 16; i += 1) {
+            ipad[i] = bkey[i] ^ 0x36363636;
+            opad[i] = bkey[i] ^ 0x5C5C5C5C;
+        }
+        hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
+        return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
+    }
+
+    /*
+    * Convert a raw string to a hex string
+    */
+    function rstr2hex(input) {
+        var hex_tab = '0123456789abcdef',
+            output = '',
+            x,
+            i;
+        for (i = 0; i < input.length; i += 1) {
+            x = input.charCodeAt(i);
+            output += hex_tab.charAt((x >>> 4) & 0x0F) +
+                hex_tab.charAt(x & 0x0F);
+        }
+        return output;
+    }
+
+    /*
+    * Encode a string as utf-8
+    */
+    function str2rstr_utf8(input) {
+        return unescape(encodeURIComponent(input));
+    }
+
+    /*
+    * Take string arguments and return either raw or hex encoded strings
+    */
+    function raw_md5(s) {
+        return rstr_md5(str2rstr_utf8(s));
+    }
+    function hex_md5(s) {
+        return rstr2hex(raw_md5(s));
+    }
+    function raw_hmac_md5(k, d) {
+        return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d));
+    }
+    function hex_hmac_md5(k, d) {
+        return rstr2hex(raw_hmac_md5(k, d));
+    }
+
+    function md5(string, key, raw) {
+        if (!key) {
+            if (!raw) {
+                return hex_md5(string);
+            }
+            return raw_md5(string);
+        }
+        if (!raw) {
+            return hex_hmac_md5(key, string);
+        }
+        return raw_hmac_md5(key, string);
+    }
+
+    if (typeof define === 'function' && define.amd) {
+        define(function () {
+            return md5;
+        });
+    } else {
+        $.md5 = md5;
+    }
+}(this));
+
+},{}],6:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+function EventEmitter() {
+  this._events = this._events || {};
+  this._maxListeners = this._maxListeners || undefined;
+}
+module.exports = EventEmitter;
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+EventEmitter.defaultMaxListeners = 10;
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function(n) {
+  if (!isNumber(n) || n < 0 || isNaN(n))
+    throw TypeError('n must be a positive number');
+  this._maxListeners = n;
+  return this;
+};
+
+EventEmitter.prototype.emit = function(type) {
+  var er, handler, len, args, i, listeners;
+
+  if (!this._events)
+    this._events = {};
+
+  // If there is no 'error' event listener then throw.
+  if (type === 'error') {
+    if (!this._events.error ||
+        (isObject(this._events.error) && !this._events.error.length)) {
+      er = arguments[1];
+      if (er instanceof Error) {
+        throw er; // Unhandled 'error' event
+      }
+      throw TypeError('Uncaught, unspecified "error" event.');
+    }
+  }
+
+  handler = this._events[type];
+
+  if (isUndefined(handler))
+    return false;
+
+  if (isFunction(handler)) {
+    switch (arguments.length) {
+      // fast cases
+      case 1:
+        handler.call(this);
+        break;
+      case 2:
+        handler.call(this, arguments[1]);
+        break;
+      case 3:
+        handler.call(this, arguments[1], arguments[2]);
+        break;
+      // slower
+      default:
+        len = arguments.length;
+        args = new Array(len - 1);
+        for (i = 1; i < len; i++)
+          args[i - 1] = arguments[i];
+        handler.apply(this, args);
+    }
+  } else if (isObject(handler)) {
+    len = arguments.length;
+    args = new Array(len - 1);
+    for (i = 1; i < len; i++)
+      args[i - 1] = arguments[i];
+
+    listeners = handler.slice();
+    len = listeners.length;
+    for (i = 0; i < len; i++)
+      listeners[i].apply(this, args);
+  }
+
+  return true;
+};
+
+EventEmitter.prototype.addListener = function(type, listener) {
+  var m;
+
+  if (!isFunction(listener))
+    throw TypeError('listener must be a function');
+
+  if (!this._events)
+    this._events = {};
+
+  // To avoid recursion in the case that type === "newListener"! Before
+  // adding it to the listeners, first emit "newListener".
+  if (this._events.newListener)
+    this.emit('newListener', type,
+              isFunction(listener.listener) ?
+              listener.listener : listener);
+
+  if (!this._events[type])
+    // Optimize the case of one listener. Don't need the extra array object.
+    this._events[type] = listener;
+  else if (isObject(this._events[type]))
+    // If we've already got an array, just append.
+    this._events[type].push(listener);
+  else
+    // Adding the second element, need to change to array.
+    this._events[type] = [this._events[type], listener];
+
+  // Check for listener leak
+  if (isObject(this._events[type]) && !this._events[type].warned) {
+    var m;
+    if (!isUndefined(this._maxListeners)) {
+      m = this._maxListeners;
+    } else {
+      m = EventEmitter.defaultMaxListeners;
+    }
+
+    if (m && m > 0 && this._events[type].length > m) {
+      this._events[type].warned = true;
+      console.error('(node) warning: possible EventEmitter memory ' +
+                    'leak detected. %d listeners added. ' +
+                    'Use emitter.setMaxListeners() to increase limit.',
+                    this._events[type].length);
+      if (typeof console.trace === 'function') {
+        // not supported in IE 10
+        console.trace();
+      }
+    }
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener) {
+  if (!isFunction(listener))
+    throw TypeError('listener must be a function');
+
+  var fired = false;
+
+  function g() {
+    this.removeListener(type, g);
+
+    if (!fired) {
+      fired = true;
+      listener.apply(this, arguments);
+    }
+  }
+
+  g.listener = listener;
+  this.on(type, g);
+
+  return this;
+};
+
+// emits a 'removeListener' event iff the listener was removed
+EventEmitter.prototype.removeListener = function(type, listener) {
+  var list, position, length, i;
+
+  if (!isFunction(listener))
+    throw TypeError('listener must be a function');
+
+  if (!this._events || !this._events[type])
+    return this;
+
+  list = this._events[type];
+  length = list.length;
+  position = -1;
+
+  if (list === listener ||
+      (isFunction(list.listener) && list.listener === listener)) {
+    delete this._events[type];
+    if (this._events.removeListener)
+      this.emit('removeListener', type, listener);
+
+  } else if (isObject(list)) {
+    for (i = length; i-- > 0;) {
+      if (list[i] === listener ||
+          (list[i].listener && list[i].listener === listener)) {
+        position = i;
+        break;
+      }
+    }
+
+    if (position < 0)
+      return this;
+
+    if (list.length === 1) {
+      list.length = 0;
+      delete this._events[type];
+    } else {
+      list.splice(position, 1);
+    }
+
+    if (this._events.removeListener)
+      this.emit('removeListener', type, listener);
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+  var key, listeners;
+
+  if (!this._events)
+    return this;
+
+  // not listening for removeListener, no need to emit
+  if (!this._events.removeListener) {
+    if (arguments.length === 0)
+      this._events = {};
+    else if (this._events[type])
+      delete this._events[type];
+    return this;
+  }
+
+  // emit removeListener for all listeners on all events
+  if (arguments.length === 0) {
+    for (key in this._events) {
+      if (key === 'removeListener') continue;
+      this.removeAllListeners(key);
+    }
+    this.removeAllListeners('removeListener');
+    this._events = {};
+    return this;
+  }
+
+  listeners = this._events[type];
+
+  if (isFunction(listeners)) {
+    this.removeListener(type, listeners);
+  } else {
+    // LIFO order
+    while (listeners.length)
+      this.removeListener(type, listeners[listeners.length - 1]);
+  }
+  delete this._events[type];
+
+  return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+  var ret;
+  if (!this._events || !this._events[type])
+    ret = [];
+  else if (isFunction(this._events[type]))
+    ret = [this._events[type]];
+  else
+    ret = this._events[type].slice();
+  return ret;
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+  var ret;
+  if (!emitter._events || !emitter._events[type])
+    ret = 0;
+  else if (isFunction(emitter._events[type]))
+    ret = 1;
+  else
+    ret = emitter._events[type].length;
+  return ret;
+};
+
+function isFunction(arg) {
+  return typeof arg === 'function';
+}
+
+function isNumber(arg) {
+  return typeof arg === 'number';
+}
+
+function isObject(arg) {
+  return typeof arg === 'object' && arg !== null;
+}
+
+function isUndefined(arg) {
+  return arg === void 0;
+}
+
+},{}],7:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+    draining = false;
+    if (currentQueue.length) {
+        queue = currentQueue.concat(queue);
+    } else {
+        queueIndex = -1;
+    }
+    if (queue.length) {
+        drainQueue();
+    }
+}
+
+function drainQueue() {
+    if (draining) {
+        return;
+    }
+    var timeout = setTimeout(cleanUpNextTick);
+    draining = true;
+
+    var len = queue.length;
+    while(len) {
+        currentQueue = queue;
+        queue = [];
+        while (++queueIndex < len) {
+            currentQueue[queueIndex].run();
+        }
+        queueIndex = -1;
+        len = queue.length;
+    }
+    currentQueue = null;
+    draining = false;
+    clearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+    var args = new Array(arguments.length - 1);
+    if (arguments.length > 1) {
+        for (var i = 1; i < arguments.length; i++) {
+            args[i - 1] = arguments[i];
+        }
+    }
+    queue.push(new Item(fun, args));
+    if (queue.length === 1 && !draining) {
+        setTimeout(drainQueue, 0);
+    }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+    this.fun = fun;
+    this.array = array;
+}
+Item.prototype.run = function () {
+    this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+    throw new Error('process.binding is not supported');
+};
+
+// TODO(shtylman)
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+    throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
+},{}],8:[function(require,module,exports){
+var nextTick = require('process/browser.js').nextTick;
+var apply = Function.prototype.apply;
+var slice = Array.prototype.slice;
+var immediateIds = {};
+var nextImmediateId = 0;
+
+// DOM APIs, for completeness
+
+exports.setTimeout = function() {
+  return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout);
+};
+exports.setInterval = function() {
+  return new Timeout(apply.call(setInterval, window, arguments), clearInterval);
+};
+exports.clearTimeout =
+exports.clearInterval = function(timeout) { timeout.close(); };
+
+function Timeout(id, clearFn) {
+  this._id = id;
+  this._clearFn = clearFn;
+}
+Timeout.prototype.unref = Timeout.prototype.ref = function() {};
+Timeout.prototype.close = function() {
+  this._clearFn.call(window, this._id);
+};
+
+// Does not start the time, just sets up the members needed.
+exports.enroll = function(item, msecs) {
+  clearTimeout(item._idleTimeoutId);
+  item._idleTimeout = msecs;
+};
+
+exports.unenroll = function(item) {
+  clearTimeout(item._idleTimeoutId);
+  item._idleTimeout = -1;
+};
+
+exports._unrefActive = exports.active = function(item) {
+  clearTimeout(item._idleTimeoutId);
+
+  var msecs = item._idleTimeout;
+  if (msecs >= 0) {
+    item._idleTimeoutId = setTimeout(function onTimeout() {
+      if (item._onTimeout)
+        item._onTimeout();
+    }, msecs);
+  }
+};
+
+// That's not how node.js implements it but the exposed api is the same.
+exports.setImmediate = typeof setImmediate === "function" ? setImmediate : function(fn) {
+  var id = nextImmediateId++;
+  var args = arguments.length < 2 ? false : slice.call(arguments, 1);
+
+  immediateIds[id] = true;
+
+  nextTick(function onNextTick() {
+    if (immediateIds[id]) {
+      // fn.call() is faster so we optimize for the common use-case
+      // @see http://jsperf.com/call-apply-segu
+      if (args) {
+        fn.apply(null, args);
+      } else {
+        fn.call(null);
+      }
+      // Prevent ids from leaking
+      exports.clearImmediate(id);
+    }
+  });
+
+  return id;
+};
+
+exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : function(id) {
+  delete immediateIds[id];
+};
+},{"process/browser.js":7}],9:[function(require,module,exports){
+
+/**
+ * This is the web browser implementation of `debug()`.
+ *
+ * Expose `debug()` as the module.
+ */
+
+exports = module.exports = require('./debug');
+exports.log = log;
+exports.formatArgs = formatArgs;
+exports.save = save;
+exports.load = load;
+exports.useColors = useColors;
+exports.storage = 'undefined' != typeof chrome
+               && 'undefined' != typeof chrome.storage
+                  ? chrome.storage.local
+                  : localstorage();
+
+/**
+ * Colors.
+ */
+
+exports.colors = [
+  'lightseagreen',
+  'forestgreen',
+  'goldenrod',
+  'dodgerblue',
+  'darkorchid',
+  'crimson'
+];
+
+/**
+ * Currently only WebKit-based Web Inspectors, Firefox >= v31,
+ * and the Firebug extension (any Firefox version) are known
+ * to support "%c" CSS customizations.
+ *
+ * TODO: add a `localStorage` variable to explicitly enable/disable colors
+ */
+
+function useColors() {
+  // is webkit? http://stackoverflow.com/a/16459606/376773
+  return ('WebkitAppearance' in document.documentElement.style) ||
+    // is firebug? http://stackoverflow.com/a/398120/376773
+    (window.console && (console.firebug || (console.exception && console.table))) ||
+    // is firefox >= v31?
+    // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
+    (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31);
+}
+
+/**
+ * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
+ */
+
+exports.formatters.j = function(v) {
+  return JSON.stringify(v);
+};
+
+
+/**
+ * Colorize log arguments if enabled.
+ *
+ * @api public
+ */
+
+function formatArgs() {
+  var args = arguments;
+  var useColors = this.useColors;
+
+  args[0] = (useColors ? '%c' : '')
+    + this.namespace
+    + (useColors ? ' %c' : ' ')
+    + args[0]
+    + (useColors ? '%c ' : ' ')
+    + '+' + exports.humanize(this.diff);
+
+  if (!useColors) return args;
+
+  var c = 'color: ' + this.color;
+  args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1));
+
+  // the final "%c" is somewhat tricky, because there could be other
+  // arguments passed either before or after the %c, so we need to
+  // figure out the correct index to insert the CSS into
+  var index = 0;
+  var lastC = 0;
+  args[0].replace(/%[a-z%]/g, function(match) {
+    if ('%%' === match) return;
+    index++;
+    if ('%c' === match) {
+      // we only are interested in the *last* %c
+      // (the user may have provided their own)
+      lastC = index;
+    }
+  });
+
+  args.splice(lastC, 0, c);
+  return args;
+}
+
+/**
+ * Invokes `console.log()` when available.
+ * No-op when `console.log` is not a "function".
+ *
+ * @api public
+ */
+
+function log() {
+  // this hackery is required for IE8/9, where
+  // the `console.log` function doesn't have 'apply'
+  return 'object' === typeof console
+    && console.log
+    && Function.prototype.apply.call(console.log, console, arguments);
+}
+
+/**
+ * Save `namespaces`.
+ *
+ * @param {String} namespaces
+ * @api private
+ */
+
+function save(namespaces) {
+  try {
+    if (null == namespaces) {
+      exports.storage.removeItem('debug');
+    } else {
+      exports.storage.debug = namespaces;
+    }
+  } catch(e) {}
+}
+
+/**
+ * Load `namespaces`.
+ *
+ * @return {String} returns the previously persisted debug modes
+ * @api private
+ */
+
+function load() {
+  var r;
+  try {
+    r = exports.storage.debug;
+  } catch(e) {}
+  return r;
+}
+
+/**
+ * Enable namespaces listed in `localStorage.debug` initially.
+ */
+
+exports.enable(load());
+
+/**
+ * Localstorage attempts to return the localstorage.
+ *
+ * This is necessary because safari throws
+ * when a user disables cookies/localstorage
+ * and you attempt to access it.
+ *
+ * @return {LocalStorage}
+ * @api private
+ */
+
+function localstorage(){
+  try {
+    return window.localStorage;
+  } catch (e) {}
+}
+
+},{"./debug":10}],10:[function(require,module,exports){
+
+/**
+ * This is the common logic for both the Node.js and web browser
+ * implementations of `debug()`.
+ *
+ * Expose `debug()` as the module.
+ */
+
+exports = module.exports = debug;
+exports.coerce = coerce;
+exports.disable = disable;
+exports.enable = enable;
+exports.enabled = enabled;
+exports.humanize = require('ms');
+
+/**
+ * The currently active debug mode names, and names to skip.
+ */
+
+exports.names = [];
+exports.skips = [];
+
+/**
+ * Map of special "%n" handling functions, for the debug "format" argument.
+ *
+ * Valid key names are a single, lowercased letter, i.e. "n".
+ */
+
+exports.formatters = {};
+
+/**
+ * Previously assigned color.
+ */
+
+var prevColor = 0;
+
+/**
+ * Previous log timestamp.
+ */
+
+var prevTime;
+
+/**
+ * Select a color.
+ *
+ * @return {Number}
+ * @api private
+ */
+
+function selectColor() {
+  return exports.colors[prevColor++ % exports.colors.length];
+}
+
+/**
+ * Create a debugger with the given `namespace`.
+ *
+ * @param {String} namespace
+ * @return {Function}
+ * @api public
+ */
+
+function debug(namespace) {
+
+  // define the `disabled` version
+  function disabled() {
+  }
+  disabled.enabled = false;
+
+  // define the `enabled` version
+  function enabled() {
+
+    var self = enabled;
+
+    // set `diff` timestamp
+    var curr = +new Date();
+    var ms = curr - (prevTime || curr);
+    self.diff = ms;
+    self.prev = prevTime;
+    self.curr = curr;
+    prevTime = curr;
+
+    // add the `color` if not set
+    if (null == self.useColors) self.useColors = exports.useColors();
+    if (null == self.color && self.useColors) self.color = selectColor();
+
+    var args = Array.prototype.slice.call(arguments);
+
+    args[0] = exports.coerce(args[0]);
+
+    if ('string' !== typeof args[0]) {
+      // anything else let's inspect with %o
+      args = ['%o'].concat(args);
+    }
+
+    // apply any `formatters` transformations
+    var index = 0;
+    args[0] = args[0].replace(/%([a-z%])/g, function(match, format) {
+      // if we encounter an escaped % then don't increase the array index
+      if (match === '%%') return match;
+      index++;
+      var formatter = exports.formatters[format];
+      if ('function' === typeof formatter) {
+        var val = args[index];
+        match = formatter.call(self, val);
+
+        // now we need to remove `args[index]` since it's inlined in the `format`
+        args.splice(index, 1);
+        index--;
+      }
+      return match;
+    });
+
+    if ('function' === typeof exports.formatArgs) {
+      args = exports.formatArgs.apply(self, args);
+    }
+    var logFn = enabled.log || exports.log || console.log.bind(console);
+    logFn.apply(self, args);
+  }
+  enabled.enabled = true;
+
+  var fn = exports.enabled(namespace) ? enabled : disabled;
+
+  fn.namespace = namespace;
+
+  return fn;
+}
+
+/**
+ * Enables a debug mode by namespaces. This can include modes
+ * separated by a colon and wildcards.
+ *
+ * @param {String} namespaces
+ * @api public
+ */
+
+function enable(namespaces) {
+  exports.save(namespaces);
+
+  var split = (namespaces || '').split(/[\s,]+/);
+  var len = split.length;
+
+  for (var i = 0; i < len; i++) {
+    if (!split[i]) continue; // ignore empty strings
+    namespaces = split[i].replace(/\*/g, '.*?');
+    if (namespaces[0] === '-') {
+      exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
+    } else {
+      exports.names.push(new RegExp('^' + namespaces + '$'));
+    }
+  }
+}
+
+/**
+ * Disable debug output.
+ *
+ * @api public
+ */
+
+function disable() {
+  exports.enable('');
+}
+
+/**
+ * Returns true if the given mode name is enabled, false otherwise.
+ *
+ * @param {String} name
+ * @return {Boolean}
+ * @api public
+ */
+
+function enabled(name) {
+  var i, len;
+  for (i = 0, len = exports.skips.length; i < len; i++) {
+    if (exports.skips[i].test(name)) {
+      return false;
+    }
+  }
+  for (i = 0, len = exports.names.length; i < len; i++) {
+    if (exports.names[i].test(name)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+/**
+ * Coerce `val`.
+ *
+ * @param {Mixed} val
+ * @return {Mixed}
+ * @api private
+ */
+
+function coerce(val) {
+  if (val instanceof Error) return val.stack || val.message;
+  return val;
+}
+
+},{"ms":11}],11:[function(require,module,exports){
+/**
+ * Helpers.
+ */
+
+var s = 1000;
+var m = s * 60;
+var h = m * 60;
+var d = h * 24;
+var y = d * 365.25;
+
+/**
+ * Parse or format the given `val`.
+ *
+ * Options:
+ *
+ *  - `long` verbose formatting [false]
+ *
+ * @param {String|Number} val
+ * @param {Object} options
+ * @return {String|Number}
+ * @api public
+ */
+
+module.exports = function(val, options){
+  options = options || {};
+  if ('string' == typeof val) return parse(val);
+  return options.long
+    ? long(val)
+    : short(val);
+};
+
+/**
+ * Parse the given `str` and return milliseconds.
+ *
+ * @param {String} str
+ * @return {Number}
+ * @api private
+ */
+
+function parse(str) {
+  str = '' + str;
+  if (str.length > 10000) return;
+  var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(str);
+  if (!match) return;
+  var n = parseFloat(match[1]);
+  var type = (match[2] || 'ms').toLowerCase();
+  switch (type) {
+    case 'years':
+    case 'year':
+    case 'yrs':
+    case 'yr':
+    case 'y':
+      return n * y;
+    case 'days':
+    case 'day':
+    case 'd':
+      return n * d;
+    case 'hours':
+    case 'hour':
+    case 'hrs':
+    case 'hr':
+    case 'h':
+      return n * h;
+    case 'minutes':
+    case 'minute':
+    case 'mins':
+    case 'min':
+    case 'm':
+      return n * m;
+    case 'seconds':
+    case 'second':
+    case 'secs':
+    case 'sec':
+    case 's':
+      return n * s;
+    case 'milliseconds':
+    case 'millisecond':
+    case 'msecs':
+    case 'msec':
+    case 'ms':
+      return n;
+  }
+}
+
+/**
+ * Short format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function short(ms) {
+  if (ms >= d) return Math.round(ms / d) + 'd';
+  if (ms >= h) return Math.round(ms / h) + 'h';
+  if (ms >= m) return Math.round(ms / m) + 'm';
+  if (ms >= s) return Math.round(ms / s) + 's';
+  return ms + 'ms';
+}
+
+/**
+ * Long format for `ms`.
+ *
+ * @param {Number} ms
+ * @return {String}
+ * @api private
+ */
+
+function long(ms) {
+  return plural(ms, d, 'day')
+    || plural(ms, h, 'hour')
+    || plural(ms, m, 'minute')
+    || plural(ms, s, 'second')
+    || ms + ' ms';
+}
+
+/**
+ * Pluralization helper.
+ */
+
+function plural(ms, n, name) {
+  if (ms < n) return;
+  if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
+  return Math.ceil(ms / n) + ' ' + name + 's';
+}
+
+},{}],12:[function(require,module,exports){
+//     uuid.js
+//
+//     Copyright (c) 2010-2012 Robert Kieffer
+//     MIT License - http://opensource.org/licenses/mit-license.php
+
+(function() {
+  var _global = this;
+
+  // Unique ID creation requires a high quality random # generator.  We feature
+  // detect to determine the best RNG source, normalizing to a function that
+  // returns 128-bits of randomness, since that's what's usually required
+  var _rng;
+
+  // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
+  //
+  // Moderately fast, high quality
+  if (typeof(_global.require) == 'function') {
+    try {
+      var _rb = _global.require('crypto').randomBytes;
+      _rng = _rb && function() {return _rb(16);};
+    } catch(e) {}
+  }
+
+  if (!_rng && _global.crypto && crypto.getRandomValues) {
+    // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
+    //
+    // Moderately fast, high quality
+    var _rnds8 = new Uint8Array(16);
+    _rng = function whatwgRNG() {
+      crypto.getRandomValues(_rnds8);
+      return _rnds8;
+    };
+  }
+
+  if (!_rng) {
+    // Math.random()-based (RNG)
+    //
+    // If all else fails, use Math.random().  It's fast, but is of unspecified
+    // quality.
+    var  _rnds = new Array(16);
+    _rng = function() {
+      for (var i = 0, r; i < 16; i++) {
+        if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
+        _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
+      }
+
+      return _rnds;
+    };
+  }
+
+  // Buffer class to use
+  var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array;
+
+  // Maps for number <-> hex string conversion
+  var _byteToHex = [];
+  var _hexToByte = {};
+  for (var i = 0; i < 256; i++) {
+    _byteToHex[i] = (i + 0x100).toString(16).substr(1);
+    _hexToByte[_byteToHex[i]] = i;
+  }
+
+  // **`parse()` - Parse a UUID into it's component bytes**
+  function parse(s, buf, offset) {
+    var i = (buf && offset) || 0, ii = 0;
+
+    buf = buf || [];
+    s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
+      if (ii < 16) { // Don't overflow!
+        buf[i + ii++] = _hexToByte[oct];
+      }
+    });
+
+    // Zero out remaining bytes if string was short
+    while (ii < 16) {
+      buf[i + ii++] = 0;
+    }
+
+    return buf;
+  }
+
+  // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
+  function unparse(buf, offset) {
+    var i = offset || 0, bth = _byteToHex;
+    return  bth[buf[i++]] + bth[buf[i++]] +
+            bth[buf[i++]] + bth[buf[i++]] + '-' +
+            bth[buf[i++]] + bth[buf[i++]] + '-' +
+            bth[buf[i++]] + bth[buf[i++]] + '-' +
+            bth[buf[i++]] + bth[buf[i++]] + '-' +
+            bth[buf[i++]] + bth[buf[i++]] +
+            bth[buf[i++]] + bth[buf[i++]] +
+            bth[buf[i++]] + bth[buf[i++]];
+  }
+
+  // **`v1()` - Generate time-based UUID**
+  //
+  // Inspired by https://github.com/LiosK/UUID.js
+  // and http://docs.python.org/library/uuid.html
+
+  // random #'s we need to init node and clockseq
+  var _seedBytes = _rng();
+
+  // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
+  var _nodeId = [
+    _seedBytes[0] | 0x01,
+    _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
+  ];
+
+  // Per 4.2.2, randomize (14 bit) clockseq
+  var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
+
+  // Previous uuid creation time
+  var _lastMSecs = 0, _lastNSecs = 0;
+
+  // See https://github.com/broofa/node-uuid for API details
+  function v1(options, buf, offset) {
+    var i = buf && offset || 0;
+    var b = buf || [];
+
+    options = options || {};
+
+    var clockseq = options.clockseq != null ? options.clockseq : _clockseq;
+
+    // UUID timestamps are 100 nano-second units since the Gregorian epoch,
+    // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so
+    // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
+    // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
+    var msecs = options.msecs != null ? options.msecs : new Date().getTime();
+
+    // Per 4.2.1.2, use count of uuid's generated during the current clock
+    // cycle to simulate higher resolution clock
+    var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1;
+
+    // Time since last uuid creation (in msecs)
+    var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
+
+    // Per 4.2.1.2, Bump clockseq on clock regression
+    if (dt < 0 && options.clockseq == null) {
+      clockseq = clockseq + 1 & 0x3fff;
+    }
+
+    // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
+    // time interval
+    if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
+      nsecs = 0;
+    }
+
+    // Per 4.2.1.2 Throw error if too many uuids are requested
+    if (nsecs >= 10000) {
+      throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
+    }
+
+    _lastMSecs = msecs;
+    _lastNSecs = nsecs;
+    _clockseq = clockseq;
+
+    // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
+    msecs += 12219292800000;
+
+    // `time_low`
+    var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
+    b[i++] = tl >>> 24 & 0xff;
+    b[i++] = tl >>> 16 & 0xff;
+    b[i++] = tl >>> 8 & 0xff;
+    b[i++] = tl & 0xff;
+
+    // `time_mid`
+    var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
+    b[i++] = tmh >>> 8 & 0xff;
+    b[i++] = tmh & 0xff;
+
+    // `time_high_and_version`
+    b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
+    b[i++] = tmh >>> 16 & 0xff;
+
+    // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
+    b[i++] = clockseq >>> 8 | 0x80;
+
+    // `clock_seq_low`
+    b[i++] = clockseq & 0xff;
+
+    // `node`
+    var node = options.node || _nodeId;
+    for (var n = 0; n < 6; n++) {
+      b[i + n] = node[n];
+    }
+
+    return buf ? buf : unparse(b);
+  }
+
+  // **`v4()` - Generate random UUID**
+
+  // See https://github.com/broofa/node-uuid for API details
+  function v4(options, buf, offset) {
+    // Deprecated - 'format' argument, as supported in v1.2
+    var i = buf && offset || 0;
+
+    if (typeof(options) == 'string') {
+      buf = options == 'binary' ? new BufferClass(16) : null;
+      options = null;
+    }
+    options = options || {};
+
+    var rnds = options.random || (options.rng || _rng)();
+
+    // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
+    rnds[6] = (rnds[6] & 0x0f) | 0x40;
+    rnds[8] = (rnds[8] & 0x3f) | 0x80;
+
+    // Copy bytes to buffer, if provided
+    if (buf) {
+      for (var ii = 0; ii < 16; ii++) {
+        buf[i + ii] = rnds[ii];
+      }
+    }
+
+    return buf || unparse(rnds);
+  }
+
+  // Export public API
+  var uuid = v4;
+  uuid.v1 = v1;
+  uuid.v4 = v4;
+  uuid.parse = parse;
+  uuid.unparse = unparse;
+  uuid.BufferClass = BufferClass;
+
+  if (typeof(module) != 'undefined' && module.exports) {
+    // Publish as node.js module
+    module.exports = uuid;
+  } else  if (typeof define === 'function' && define.amd) {
+    // Publish as AMD module
+    define(function() {return uuid;});
+ 
+
+  } else {
+    // Publish as global (in browsers)
+    var _previousRoot = _global.uuid;
+
+    // **`noConflict()` - (browser only) to reset global 'uuid' var**
+    uuid.noConflict = function() {
+      _global.uuid = _previousRoot;
+      return uuid;
+    };
+
+    _global.uuid = uuid;
+  }
+}).call(this);
+
+},{}],13:[function(require,module,exports){
+(function (global){
+'use strict';
+
+// Expose the Adapter function/object.
+module.exports = Adapter;
+
+
+// Dependencies
+
+var browser = require('bowser').browser,
+	debug = require('debug')('rtcninja:Adapter'),
+	debugerror = require('debug')('rtcninja:ERROR:Adapter'),
+
+	// Internal vars
+	getUserMedia = null,
+	RTCPeerConnection = null,
+	RTCSessionDescription = null,
+	RTCIceCandidate = null,
+	MediaStreamTrack = null,
+	getMediaDevices = null,
+	attachMediaStream = null,
+	canRenegotiate = false,
+	oldSpecRTCOfferOptions = false,
+	browserVersion = Number(browser.version) || 0,
+	isDesktop = !!(!browser.mobile || !browser.tablet),
+	hasWebRTC = false,
+	virtGlobal, virtNavigator;
+
+debugerror.log = console.warn.bind(console);
+
+// Dirty trick to get this library working in a Node-webkit env with browserified libs
+virtGlobal = global.window || global;
+// Don't fail in Node
+virtNavigator = virtGlobal.navigator || {};
+
+
+// Constructor.
+
+function Adapter(options) {
+	// Chrome desktop, Chrome Android, Opera desktop, Opera Android, Android native browser
+	// or generic Webkit browser.
+	if (
+		(isDesktop && browser.chrome && browserVersion >= 32) ||
+		(browser.android && browser.chrome && browserVersion >= 39) ||
+		(isDesktop && browser.opera && browserVersion >= 27) ||
+		(browser.android && browser.opera && browserVersion >= 24) ||
+		(browser.android && browser.webkit && !browser.chrome && browserVersion >= 37) ||
+		(virtNavigator.webkitGetUserMedia && virtGlobal.webkitRTCPeerConnection)
+	) {
+		hasWebRTC = true;
+		getUserMedia = virtNavigator.webkitGetUserMedia.bind(virtNavigator);
+		RTCPeerConnection = virtGlobal.webkitRTCPeerConnection;
+		RTCSessionDescription = virtGlobal.RTCSessionDescription;
+		RTCIceCandidate = virtGlobal.RTCIceCandidate;
+		MediaStreamTrack = virtGlobal.MediaStreamTrack;
+		if (MediaStreamTrack && MediaStreamTrack.getSources) {
+			getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack);
+		} else if (virtNavigator.getMediaDevices) {
+			getMediaDevices = virtNavigator.getMediaDevices.bind(virtNavigator);
+		}
+		attachMediaStream = function (element, stream) {
+			element.src = URL.createObjectURL(stream);
+			return element;
+		};
+		canRenegotiate = true;
+		oldSpecRTCOfferOptions = false;
+	// Firefox desktop, Firefox Android.
+	} else if (
+		(isDesktop && browser.firefox && browserVersion >= 22) ||
+		(browser.android && browser.firefox && browserVersion >= 33) ||
+		(virtNavigator.mozGetUserMedia && virtGlobal.mozRTCPeerConnection)
+	) {
+		hasWebRTC = true;
+		getUserMedia = virtNavigator.mozGetUserMedia.bind(virtNavigator);
+		RTCPeerConnection = virtGlobal.mozRTCPeerConnection;
+		RTCSessionDescription = virtGlobal.mozRTCSessionDescription;
+		RTCIceCandidate = virtGlobal.mozRTCIceCandidate;
+		MediaStreamTrack = virtGlobal.MediaStreamTrack;
+		attachMediaStream = function (element, stream) {
+			element.src = URL.createObjectURL(stream);
+			return element;
+		};
+		canRenegotiate = false;
+		oldSpecRTCOfferOptions = false;
+		// WebRTC plugin required. For example IE or Safari with the Temasys plugin.
+	} else if (
+		options.plugin &&
+		typeof options.plugin.isRequired === 'function' &&
+		options.plugin.isRequired() &&
+		typeof options.plugin.isInstalled === 'function' &&
+		options.plugin.isInstalled()
+	) {
+		var pluginiface = options.plugin.interface;
+
+		hasWebRTC = true;
+		getUserMedia = pluginiface.getUserMedia;
+		RTCPeerConnection = pluginiface.RTCPeerConnection;
+		RTCSessionDescription = pluginiface.RTCSessionDescription;
+		RTCIceCandidate = pluginiface.RTCIceCandidate;
+		MediaStreamTrack = pluginiface.MediaStreamTrack;
+		if (MediaStreamTrack && MediaStreamTrack.getSources) {
+			getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack);
+		} else if (virtNavigator.getMediaDevices) {
+			getMediaDevices = virtNavigator.getMediaDevices.bind(virtNavigator);
+		}
+		attachMediaStream = pluginiface.attachMediaStream;
+		canRenegotiate = pluginiface.canRenegotiate;
+		oldSpecRTCOfferOptions = true;  // TODO: Update when fixed in the plugin.
+	// Best effort (may be adater.js is loaded).
+	} else if (virtNavigator.getUserMedia && virtGlobal.RTCPeerConnection) {
+		hasWebRTC = true;
+		getUserMedia = virtNavigator.getUserMedia.bind(virtNavigator);
+		RTCPeerConnection = virtGlobal.RTCPeerConnection;
+		RTCSessionDescription = virtGlobal.RTCSessionDescription;
+		RTCIceCandidate = virtGlobal.RTCIceCandidate;
+		MediaStreamTrack = virtGlobal.MediaStreamTrack;
+		if (MediaStreamTrack && MediaStreamTrack.getSources) {
+			getMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack);
+		} else if (virtNavigator.getMediaDevices) {
+			getMediaDevices = virtNavigator.getMediaDevices.bind(virtNavigator);
+		}
+		attachMediaStream = virtGlobal.attachMediaStream || function (element, stream) {
+			element.src = URL.createObjectURL(stream);
+			return element;
+		};
+		canRenegotiate = false;
+		oldSpecRTCOfferOptions = false;
+	}
+
+
+	function throwNonSupported(item) {
+		return function () {
+			throw new Error('rtcninja: WebRTC not supported, missing ' + item +
+			' [browser: ' + browser.name + ' ' + browser.version + ']');
+		};
+	}
+
+
+	// Public API.
+
+	// Expose a WebRTC checker.
+	Adapter.hasWebRTC = function () {
+		return hasWebRTC;
+	};
+
+	// Expose getUserMedia.
+	if (getUserMedia) {
+		Adapter.getUserMedia = function (constraints, successCallback, errorCallback) {
+			debug('getUserMedia() | constraints: %o', constraints);
+
+			try {
+				getUserMedia(constraints,
+					function (stream) {
+						debug('getUserMedia() | success');
+						if (successCallback) {
+							successCallback(stream);
+						}
+					},
+					function (error) {
+						debug('getUserMedia() | error:', error);
+						if (errorCallback) {
+							errorCallback(error);
+						}
+					}
+				);
+			}
+			catch (error) {
+				debugerror('getUserMedia() | error:', error);
+				if (errorCallback) {
+					errorCallback(error);
+				}
+			}
+		};
+	} else {
+		Adapter.getUserMedia = function (constraints, successCallback, errorCallback) {
+			debugerror('getUserMedia() | WebRTC not supported');
+			if (errorCallback) {
+				errorCallback(new Error('rtcninja: WebRTC not supported, missing ' +
+				'getUserMedia [browser: ' + browser.name + ' ' + browser.version + ']'));
+			} else {
+				throwNonSupported('getUserMedia');
+			}
+		};
+	}
+
+	// Expose RTCPeerConnection.
+	Adapter.RTCPeerConnection = RTCPeerConnection || throwNonSupported('RTCPeerConnection');
+
+	// Expose RTCSessionDescription.
+	Adapter.RTCSessionDescription = RTCSessionDescription || throwNonSupported('RTCSessionDescription');
+
+	// Expose RTCIceCandidate.
+	Adapter.RTCIceCandidate = RTCIceCandidate || throwNonSupported('RTCIceCandidate');
+
+	// Expose MediaStreamTrack.
+	Adapter.MediaStreamTrack = MediaStreamTrack || throwNonSupported('MediaStreamTrack');
+
+	// Expose getMediaDevices.
+	Adapter.getMediaDevices = getMediaDevices;
+
+	// Expose MediaStreamTrack.
+	Adapter.attachMediaStream = attachMediaStream || throwNonSupported('attachMediaStream');
+
+	// Expose canRenegotiate attribute.
+	Adapter.canRenegotiate = canRenegotiate;
+
+	// Expose closeMediaStream.
+	Adapter.closeMediaStream = function (stream) {
+		if (!stream) {
+			return;
+		}
+
+		// Latest spec states that MediaStream has no stop() method and instead must
+		// call stop() on every MediaStreamTrack.
+		if (MediaStreamTrack && MediaStreamTrack.prototype && MediaStreamTrack.prototype.stop) {
+			debug('closeMediaStream() | calling stop() on all the MediaStreamTrack');
+
+			var tracks, i, len;
+
+			if (stream.getTracks) {
+				tracks = stream.getTracks();
+				for (i = 0, len = tracks.length; i < len; i += 1) {
+					tracks[i].stop();
+				}
+			} else {
+				tracks = stream.getAudioTracks();
+				for (i = 0, len = tracks.length; i < len; i += 1) {
+					tracks[i].stop();
+				}
+
+				tracks = stream.getVideoTracks();
+				for (i = 0, len = tracks.length; i < len; i += 1) {
+					tracks[i].stop();
+				}
+			}
+		// Deprecated by the spec, but still in use.
+		} else if (typeof stream.stop === 'function') {
+			debug('closeMediaStream() | calling stop() on the MediaStream');
+
+			stream.stop();
+		}
+	};
+
+	// Expose fixPeerConnectionConfig.
+	Adapter.fixPeerConnectionConfig = function (pcConfig) {
+		var i, len, iceServer, hasUrls, hasUrl;
+
+		if (!Array.isArray(pcConfig.iceServers)) {
+			pcConfig.iceServers = [];
+		}
+
+		for (i = 0, len = pcConfig.iceServers.length; i < len; i += 1) {
+			iceServer = pcConfig.iceServers[i];
+			hasUrls = iceServer.hasOwnProperty('urls');
+			hasUrl = iceServer.hasOwnProperty('url');
+
+			if (typeof iceServer === 'object') {
+				// Has .urls but not .url, so add .url with a single string value.
+				if (hasUrls && !hasUrl) {
+					iceServer.url = (Array.isArray(iceServer.urls) ? iceServer.urls[0] : iceServer.urls);
+				// Has .url but not .urls, so add .urls with same value.
+				} else if (!hasUrls && hasUrl) {
+					iceServer.urls = (Array.isArray(iceServer.url) ? iceServer.url.slice() : iceServer.url);
+				}
+
+				// Ensure .url is a single string.
+				if (hasUrl && Array.isArray(iceServer.url)) {
+					iceServer.url = iceServer.url[0];
+				}
+			}
+		}
+	};
+
+	// Expose fixRTCOfferOptions.
+	Adapter.fixRTCOfferOptions = function (options) {
+		options = options || {};
+
+		// New spec.
+		if (!oldSpecRTCOfferOptions) {
+			if (options.mandatory && options.mandatory.OfferToReceiveAudio) {
+				options.offerToReceiveAudio = 1;
+			}
+			if (options.mandatory && options.mandatory.OfferToReceiveVideo) {
+				options.offerToReceiveVideo = 1;
+			}
+			delete options.mandatory;
+		// Old spec.
+		} else {
+			if (options.offerToReceiveAudio) {
+				options.mandatory = options.mandatory || {};
+				options.mandatory.OfferToReceiveAudio = true;
+			}
+			if (options.offerToReceiveVideo) {
+				options.mandatory = options.mandatory || {};
+				options.mandatory.OfferToReceiveVideo = true;
+			}
+		}
+	};
+
+	return Adapter;
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"bowser":17,"debug":9}],14:[function(require,module,exports){
+'use strict';
+
+// Expose the RTCPeerConnection class.
+module.exports = RTCPeerConnection;
+
+
+// Dependencies.
+
+var merge = require('merge'),
+	debug = require('debug')('rtcninja:RTCPeerConnection'),
+	debugerror = require('debug')('rtcninja:ERROR:RTCPeerConnection'),
+	Adapter = require('./Adapter'),
+
+	// Internal constants.
+	C = {
+		REGEXP_NORMALIZED_CANDIDATE: new RegExp(/^candidate:/i),
+		REGEXP_FIX_CANDIDATE: new RegExp(/(^a=|\r|\n)/gi),
+		REGEXP_RELAY_CANDIDATE: new RegExp(/ relay /i),
+		REGEXP_SDP_CANDIDATES: new RegExp(/^a=candidate:.*\r\n/igm),
+		REGEXP_SDP_NON_RELAY_CANDIDATES: new RegExp(/^a=candidate:(.(?!relay ))*\r\n/igm)
+	},
+
+	// Internal variables.
+	VAR = {
+		normalizeCandidate: null
+	};
+
+debugerror.log = console.warn.bind(console);
+
+
+// Constructor
+
+function RTCPeerConnection(pcConfig, pcConstraints) {
+	debug('new | pcConfig: %o', pcConfig);
+
+	// Set this.pcConfig and this.options.
+	setConfigurationAndOptions.call(this, pcConfig);
+
+	// NOTE: Deprecated pcConstraints argument.
+	this.pcConstraints = pcConstraints;
+
+	// Own version of the localDescription.
+	this.ourLocalDescription = null;
+
+	// Latest values of PC attributes to avoid events with same value.
+	this.ourSignalingState = null;
+	this.ourIceConnectionState = null;
+	this.ourIceGatheringState = null;
+
+	// Timer for options.gatheringTimeout.
+	this.timerGatheringTimeout = null;
+
+	// Timer for options.gatheringTimeoutAfterRelay.
+	this.timerGatheringTimeoutAfterRelay = null;
+
+	// Flag to ignore new gathered ICE candidates.
+	this.ignoreIceGathering = false;
+
+	// Flag set when closed.
+	this.closed = false;
+
+	// Set RTCPeerConnection.
+	setPeerConnection.call(this);
+
+	// Set properties.
+	setProperties.call(this);
+}
+
+
+// Public API.
+
+RTCPeerConnection.prototype.createOffer = function (successCallback, failureCallback, options) {
+	debug('createOffer()');
+
+	var self = this;
+
+	Adapter.fixRTCOfferOptions(options);
+
+	this.pc.createOffer(
+		function (offer) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debug('createOffer() | success');
+			if (successCallback) {
+				successCallback(offer);
+			}
+		},
+		function (error) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debugerror('createOffer() | error:', error);
+			if (failureCallback) {
+				failureCallback(error);
+			}
+		},
+		options
+	);
+};
+
+
+RTCPeerConnection.prototype.createAnswer = function (successCallback, failureCallback, options) {
+	debug('createAnswer()');
+
+	var self = this;
+
+	this.pc.createAnswer(
+		function (answer) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debug('createAnswer() | success');
+			if (successCallback) {
+				successCallback(answer);
+			}
+		},
+		function (error) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debugerror('createAnswer() | error:', error);
+			if (failureCallback) {
+				failureCallback(error);
+			}
+		},
+		options
+	);
+};
+
+
+RTCPeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
+	debug('setLocalDescription()');
+
+	var self = this;
+
+	this.pc.setLocalDescription(
+		description,
+		// success.
+		function () {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debug('setLocalDescription() | success');
+
+			// Clear gathering timers.
+			clearTimeout(self.timerGatheringTimeout);
+			delete self.timerGatheringTimeout;
+			clearTimeout(self.timerGatheringTimeoutAfterRelay);
+			delete self.timerGatheringTimeoutAfterRelay;
+
+			runTimerGatheringTimeout();
+			if (successCallback) {
+				successCallback();
+			}
+		},
+		// failure
+		function (error) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debugerror('setLocalDescription() | error:', error);
+			if (failureCallback) {
+				failureCallback(error);
+			}
+		}
+	);
+
+	// Enable (again) ICE gathering.
+	this.ignoreIceGathering = false;
+
+	// Handle gatheringTimeout.
+	function runTimerGatheringTimeout() {
+		if (typeof self.options.gatheringTimeout !== 'number') {
+			return;
+		}
+		// If setLocalDescription was already called, it may happen that
+		// ICE gathering is not needed, so don't run this timer.
+		if (self.pc.iceGatheringState === 'complete') {
+			return;
+		}
+
+		debug('setLocalDescription() | ending gathering in %d ms (gatheringTimeout option)',
+			self.options.gatheringTimeout);
+
+		self.timerGatheringTimeout = setTimeout(function () {
+			if (isClosed.call(self)) {
+				return;
+			}
+
+			debug('forced end of candidates after gatheringTimeout timeout');
+
+			// Clear gathering timers.
+			delete self.timerGatheringTimeout;
+			clearTimeout(self.timerGatheringTimeoutAfterRelay);
+			delete self.timerGatheringTimeoutAfterRelay;
+
+			// Ignore new candidates.
+			self.ignoreIceGathering = true;
+			if (self.onicecandidate) {
+				self.onicecandidate({ candidate: null }, null);
+			}
+
+		}, self.options.gatheringTimeout);
+	}
+};
+
+
+RTCPeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
+	debug('setRemoteDescription()');
+
+	var self = this;
+
+	this.pc.setRemoteDescription(
+		description,
+		function () {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debug('setRemoteDescription() | success');
+			if (successCallback) {
+				successCallback();
+			}
+		},
+		function (error) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debugerror('setRemoteDescription() | error:', error);
+			if (failureCallback) {
+				failureCallback(error);
+			}
+		}
+	);
+};
+
+
+RTCPeerConnection.prototype.updateIce = function (pcConfig) {
+	debug('updateIce() | pcConfig: %o', pcConfig);
+
+	// Update this.pcConfig and this.options.
+	setConfigurationAndOptions.call(this, pcConfig);
+
+	this.pc.updateIce(this.pcConfig);
+
+	// Enable (again) ICE gathering.
+	this.ignoreIceGathering = false;
+};
+
+
+RTCPeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
+	debug('addIceCandidate() | candidate: %o', candidate);
+
+	var self = this;
+
+	this.pc.addIceCandidate(
+		candidate,
+		function () {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debug('addIceCandidate() | success');
+			if (successCallback) {
+				successCallback();
+			}
+		},
+		function (error) {
+			if (isClosed.call(self)) {
+				return;
+			}
+			debugerror('addIceCandidate() | error:', error);
+			if (failureCallback) {
+				failureCallback(error);
+			}
+		}
+	);
+};
+
+
+RTCPeerConnection.prototype.getConfiguration = function () {
+	debug('getConfiguration()');
+
+	return this.pc.getConfiguration();
+};
+
+
+RTCPeerConnection.prototype.getLocalStreams = function () {
+	debug('getLocalStreams()');
+
+	return this.pc.getLocalStreams();
+};
+
+
+RTCPeerConnection.prototype.getRemoteStreams = function () {
+	debug('getRemoteStreams()');
+
+	return this.pc.getRemoteStreams();
+};
+
+
+RTCPeerConnection.prototype.getStreamById = function (streamId) {
+	debug('getStreamById() | streamId: %s', streamId);
+
+	return this.pc.getStreamById(streamId);
+};
+
+
+RTCPeerConnection.prototype.addStream = function (stream) {
+	debug('addStream() | stream: %s', stream);
+
+	this.pc.addStream(stream);
+};
+
+
+RTCPeerConnection.prototype.removeStream = function (stream) {
+	debug('removeStream() | stream: %o', stream);
+
+	this.pc.removeStream(stream);
+};
+
+
+RTCPeerConnection.prototype.close = function () {
+	debug('close()');
+
+	this.closed = true;
+
+	// Clear gathering timers.
+	clearTimeout(this.timerGatheringTimeout);
+	delete this.timerGatheringTimeout;
+	clearTimeout(this.timerGatheringTimeoutAfterRelay);
+	delete this.timerGatheringTimeoutAfterRelay;
+
+	this.pc.close();
+};
+
+
+RTCPeerConnection.prototype.createDataChannel = function () {
+	debug('createDataChannel()');
+
+	return this.pc.createDataChannel.apply(this.pc, arguments);
+};
+
+
+RTCPeerConnection.prototype.createDTMFSender = function (track) {
+	debug('createDTMFSender()');
+
+	return this.pc.createDTMFSender(track);
+};
+
+
+RTCPeerConnection.prototype.getStats = function () {
+	debug('getStats()');
+
+	return this.pc.getStats.apply(this.pc, arguments);
+};
+
+
+RTCPeerConnection.prototype.setIdentityProvider = function () {
+	debug('setIdentityProvider()');
+
+	return this.pc.setIdentityProvider.apply(this.pc, arguments);
+};
+
+
+RTCPeerConnection.prototype.getIdentityAssertion = function () {
+	debug('getIdentityAssertion()');
+
+	return this.pc.getIdentityAssertion();
+};
+
+
+RTCPeerConnection.prototype.reset = function (pcConfig) {
+	debug('reset() | pcConfig: %o', pcConfig);
+
+	var pc = this.pc;
+
+	// Remove events in the old PC.
+	pc.onnegotiationneeded = null;
+	pc.onicecandidate = null;
+	pc.onaddstream = null;
+	pc.onremovestream = null;
+	pc.ondatachannel = null;
+	pc.onsignalingstatechange = null;
+	pc.oniceconnectionstatechange = null;
+	pc.onicegatheringstatechange = null;
+	pc.onidentityresult = null;
+	pc.onpeeridentity = null;
+	pc.onidpassertionerror = null;
+	pc.onidpvalidationerror = null;
+
+	// Clear gathering timers.
+	clearTimeout(this.timerGatheringTimeout);
+	delete this.timerGatheringTimeout;
+	clearTimeout(this.timerGatheringTimeoutAfterRelay);
+	delete this.timerGatheringTimeoutAfterRelay;
+
+	// Silently close the old PC.
+	debug('reset() | closing current peerConnection');
+	pc.close();
+
+	// Set this.pcConfig and this.options.
+	setConfigurationAndOptions.call(this, pcConfig);
+
+	// Create a new PC.
+	setPeerConnection.call(this);
+};
+
+
+// Private Helpers.
+
+function setConfigurationAndOptions(pcConfig) {
+	// Clone pcConfig.
+	this.pcConfig = merge(true, pcConfig);
+
+	// Fix pcConfig.
+	Adapter.fixPeerConnectionConfig(this.pcConfig);
+
+	this.options = {
+		iceTransportsRelay: (this.pcConfig.iceTransports === 'relay'),
+		iceTransportsNone: (this.pcConfig.iceTransports === 'none'),
+		gatheringTimeout: this.pcConfig.gatheringTimeout,
+		gatheringTimeoutAfterRelay: this.pcConfig.gatheringTimeoutAfterRelay
+	};
+
+	// Remove custom rtcninja.RTCPeerConnection options from pcConfig.
+	delete this.pcConfig.gatheringTimeout;
+	delete this.pcConfig.gatheringTimeoutAfterRelay;
+
+	debug('setConfigurationAndOptions | processed pcConfig: %o', this.pcConfig);
+}
+
+
+function isClosed() {
+	return ((this.closed) || (this.pc && this.pc.iceConnectionState === 'closed'));
+}
+
+
+function setEvents() {
+	var self = this,
+		pc = this.pc;
+
+	pc.onnegotiationneeded = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onnegotiationneeded()');
+		if (self.onnegotiationneeded) {
+			self.onnegotiationneeded(event);
+		}
+	};
+
+	pc.onicecandidate = function (event) {
+		var candidate, isRelay, newCandidate;
+
+		if (isClosed.call(self)) {
+			return;
+		}
+		if (self.ignoreIceGathering) {
+			return;
+		}
+
+		// Ignore any candidate (event the null one) if iceTransports:'none' is set.
+		if (self.options.iceTransportsNone) {
+			return;
+		}
+
+		candidate = event.candidate;
+
+		if (candidate) {
+			isRelay = C.REGEXP_RELAY_CANDIDATE.test(candidate.candidate);
+
+			// Ignore if just relay candidates are requested.
+			if (self.options.iceTransportsRelay && !isRelay) {
+				return;
+			}
+
+			// Handle gatheringTimeoutAfterRelay.
+			if (isRelay && !self.timerGatheringTimeoutAfterRelay &&
+				(typeof self.options.gatheringTimeoutAfterRelay === 'number')) {
+				debug('onicecandidate() | first relay candidate found, ending gathering in %d ms', self.options.gatheringTimeoutAfterRelay);
+
+				self.timerGatheringTimeoutAfterRelay = setTimeout(function () {
+					if (isClosed.call(self)) {
+						return;
+					}
+
+					debug('forced end of candidates after timeout');
+
+					// Clear gathering timers.
+					delete self.timerGatheringTimeoutAfterRelay;
+					clearTimeout(self.timerGatheringTimeout);
+					delete self.timerGatheringTimeout;
+
+					// Ignore new candidates.
+					self.ignoreIceGathering = true;
+					if (self.onicecandidate) {
+						self.onicecandidate({candidate: null}, null);
+					}
+				}, self.options.gatheringTimeoutAfterRelay);
+			}
+
+			newCandidate = new Adapter.RTCIceCandidate({
+				sdpMid: candidate.sdpMid,
+				sdpMLineIndex: candidate.sdpMLineIndex,
+				candidate: candidate.candidate
+			});
+
+			// Force correct candidate syntax (just check it once).
+			if (VAR.normalizeCandidate === null) {
+				if (C.REGEXP_NORMALIZED_CANDIDATE.test(candidate.candidate)) {
+					VAR.normalizeCandidate = false;
+				} else {
+					debug('onicecandidate() | normalizing ICE candidates syntax (remove "a=" and "\\r\\n")');
+					VAR.normalizeCandidate = true;
+				}
+			}
+			if (VAR.normalizeCandidate) {
+				newCandidate.candidate = candidate.candidate.replace(C.REGEXP_FIX_CANDIDATE, '');
+			}
+
+			debug(
+				'onicecandidate() | m%d(%s) %s',
+				newCandidate.sdpMLineIndex,
+				newCandidate.sdpMid || 'no mid', newCandidate.candidate);
+			if (self.onicecandidate) {
+				self.onicecandidate(event, newCandidate);
+			}
+		// Null candidate (end of candidates).
+		} else {
+			debug('onicecandidate() | end of candidates');
+
+			// Clear gathering timers.
+			clearTimeout(self.timerGatheringTimeout);
+			delete self.timerGatheringTimeout;
+			clearTimeout(self.timerGatheringTimeoutAfterRelay);
+			delete self.timerGatheringTimeoutAfterRelay;
+			if (self.onicecandidate) {
+				self.onicecandidate(event, null);
+			}
+		}
+	};
+
+	pc.onaddstream = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onaddstream() | stream: %o', event.stream);
+		if (self.onaddstream) {
+			self.onaddstream(event, event.stream);
+		}
+	};
+
+	pc.onremovestream = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onremovestream() | stream: %o', event.stream);
+		if (self.onremovestream) {
+			self.onremovestream(event, event.stream);
+		}
+	};
+
+	pc.ondatachannel = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('ondatachannel() | datachannel: %o', event.channel);
+		if (self.ondatachannel) {
+			self.ondatachannel(event, event.channel);
+		}
+	};
+
+	pc.onsignalingstatechange = function (event) {
+		if (pc.signalingState === self.ourSignalingState) {
+			return;
+		}
+
+		debug('onsignalingstatechange() | signalingState: %s', pc.signalingState);
+		self.ourSignalingState = pc.signalingState;
+		if (self.onsignalingstatechange) {
+			self.onsignalingstatechange(event, pc.signalingState);
+		}
+	};
+
+	pc.oniceconnectionstatechange = function (event) {
+		if (pc.iceConnectionState === self.ourIceConnectionState) {
+			return;
+		}
+
+		debug('oniceconnectionstatechange() | iceConnectionState: %s', pc.iceConnectionState);
+		self.ourIceConnectionState = pc.iceConnectionState;
+		if (self.oniceconnectionstatechange) {
+			self.oniceconnectionstatechange(event, pc.iceConnectionState);
+		}
+	};
+
+	pc.onicegatheringstatechange = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		if (pc.iceGatheringState === self.ourIceGatheringState) {
+			return;
+		}
+
+		debug('onicegatheringstatechange() | iceGatheringState: %s', pc.iceGatheringState);
+		self.ourIceGatheringState = pc.iceGatheringState;
+		if (self.onicegatheringstatechange) {
+			self.onicegatheringstatechange(event, pc.iceGatheringState);
+		}
+	};
+
+	pc.onidentityresult = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onidentityresult()');
+		if (self.onidentityresult) {
+			self.onidentityresult(event);
+		}
+	};
+
+	pc.onpeeridentity = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onpeeridentity()');
+		if (self.onpeeridentity) {
+			self.onpeeridentity(event);
+		}
+	};
+
+	pc.onidpassertionerror = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onidpassertionerror()');
+		if (self.onidpassertionerror) {
+			self.onidpassertionerror(event);
+		}
+	};
+
+	pc.onidpvalidationerror = function (event) {
+		if (isClosed.call(self)) {
+			return;
+		}
+
+		debug('onidpvalidationerror()');
+		if (self.onidpvalidationerror) {
+			self.onidpvalidationerror(event);
+		}
+	};
+}
+
+
+function setPeerConnection() {
+	// Create a RTCPeerConnection.
+	if (!this.pcConstraints) {
+		this.pc = new Adapter.RTCPeerConnection(this.pcConfig);
+	} else {
+		// NOTE: Deprecated.
+		this.pc = new Adapter.RTCPeerConnection(this.pcConfig, this.pcConstraints);
+	}
+
+	// Set RTC events.
+	setEvents.call(this);
+}
+
+
+function getLocalDescription() {
+	var pc = this.pc,
+		options = this.options,
+		sdp = null;
+
+	if (!pc.localDescription) {
+		this.ourLocalDescription = null;
+		return null;
+	}
+
+	// Mangle the SDP string.
+	if (options.iceTransportsRelay) {
+		sdp = pc.localDescription.sdp.replace(C.REGEXP_SDP_NON_RELAY_CANDIDATES, '');
+	} else if (options.iceTransportsNone) {
+		sdp = pc.localDescription.sdp.replace(C.REGEXP_SDP_CANDIDATES, '');
+	}
+
+	this.ourLocalDescription = new Adapter.RTCSessionDescription({
+		type: pc.localDescription.type,
+		sdp: sdp || pc.localDescription.sdp
+	});
+
+	return this.ourLocalDescription;
+}
+
+
+function setProperties() {
+	var self = this;
+
+	Object.defineProperties(this, {
+		peerConnection: {
+			get: function () {
+				return self.pc;
+			}
+		},
+
+		signalingState: {
+			get: function () {
+				return self.pc.signalingState;
+			}
+		},
+
+		iceConnectionState: {
+			get: function () {
+				return self.pc.iceConnectionState;
+			}
+		},
+
+		iceGatheringState: {
+			get: function () {
+				return self.pc.iceGatheringState;
+			}
+		},
+
+		localDescription: {
+			get: function () {
+				return getLocalDescription.call(self);
+			}
+		},
+
+		remoteDescription: {
+			get: function () {
+				return self.pc.remoteDescription;
+			}
+		},
+
+		peerIdentity: {
+			get: function () {
+				return self.pc.peerIdentity;
+			}
+		}
+	});
+}
+
+},{"./Adapter":13,"debug":9,"merge":18}],15:[function(require,module,exports){
+'use strict';
+
+module.exports = rtcninja;
+
+
+// Dependencies.
+
+var browser = require('bowser').browser,
+	debug = require('debug')('rtcninja'),
+	debugerror = require('debug')('rtcninja:ERROR'),
+	version = require('./version'),
+	Adapter = require('./Adapter'),
+	RTCPeerConnection = require('./RTCPeerConnection'),
+
+	// Internal vars.
+	called = false;
+
+debugerror.log = console.warn.bind(console);
+debug('version %s', version);
+debug('detected browser: %s %s [mobile:%s, tablet:%s, android:%s, ios:%s]',
+		browser.name, browser.version, !!browser.mobile, !!browser.tablet,
+		!!browser.android, !!browser.ios);
+
+
+// Constructor.
+
+function rtcninja(options) {
+	// Load adapter
+	var iface = Adapter(options || {});  // jshint ignore:line
+
+	called = true;
+
+	// Expose RTCPeerConnection class.
+	rtcninja.RTCPeerConnection = RTCPeerConnection;
+
+	// Expose WebRTC API and utils.
+	rtcninja.getUserMedia = iface.getUserMedia;
+	rtcninja.RTCSessionDescription = iface.RTCSessionDescription;
+	rtcninja.RTCIceCandidate = iface.RTCIceCandidate;
+	rtcninja.MediaStreamTrack = iface.MediaStreamTrack;
+	rtcninja.getMediaDevices = iface.getMediaDevices;
+	rtcninja.attachMediaStream = iface.attachMediaStream;
+	rtcninja.closeMediaStream = iface.closeMediaStream;
+	rtcninja.canRenegotiate = iface.canRenegotiate;
+
+	// Log WebRTC support.
+	if (iface.hasWebRTC()) {
+		debug('WebRTC supported');
+		return true;
+	} else {
+		debugerror('WebRTC not supported');
+		return false;
+	}
+}
+
+
+// Public API.
+
+// If called without calling rtcninja(), call it.
+rtcninja.hasWebRTC = function () {
+	if (!called) {
+		rtcninja();
+	}
+
+	return Adapter.hasWebRTC();
+};
+
+
+// Expose version property.
+Object.defineProperty(rtcninja, 'version', {
+	get: function () {
+		return version;
+	}
+});
+
+
+// Expose called property.
+Object.defineProperty(rtcninja, 'called', {
+	get: function () {
+		return called;
+	}
+});
+
+
+// Exposing stuff.
+
+rtcninja.debug = require('debug');
+rtcninja.browser = browser;
+
+},{"./Adapter":13,"./RTCPeerConnection":14,"./version":16,"bowser":17,"debug":9}],16:[function(require,module,exports){
+'use strict';
+
+// Expose the 'version' field of package.json.
+module.exports = require('../package.json').version;
+
+
+},{"../package.json":19}],17:[function(require,module,exports){
+/*!
+  * Bowser - a browser detector
+  * https://github.com/ded/bowser
+  * MIT License | (c) Dustin Diaz 2014
+  */
+
+!function (name, definition) {
+  if (typeof module != 'undefined' && module.exports) module.exports['browser'] = definition()
+  else if (typeof define == 'function' && define.amd) define(definition)
+  else this[name] = definition()
+}('bowser', function () {
+  /**
+    * See useragents.js for examples of navigator.userAgent
+    */
+
+  var t = true
+
+  function detect(ua) {
+
+    function getFirstMatch(regex) {
+      var match = ua.match(regex);
+      return (match && match.length > 1 && match[1]) || '';
+    }
+
+    function getSecondMatch(regex) {
+      var match = ua.match(regex);
+      return (match && match.length > 1 && match[2]) || '';
+    }
+
+    var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()
+      , likeAndroid = /like android/i.test(ua)
+      , android = !likeAndroid && /android/i.test(ua)
+      , edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i)
+      , versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
+      , tablet = /tablet/i.test(ua)
+      , mobile = !tablet && /[^-]mobi/i.test(ua)
+      , result
+
+    if (/opera|opr/i.test(ua)) {
+      result = {
+        name: 'Opera'
+      , opera: t
+      , version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/windows phone/i.test(ua)) {
+      result = {
+        name: 'Windows Phone'
+      , windowsphone: t
+      }
+      if (edgeVersion) {
+        result.msedge = t
+        result.version = edgeVersion
+      }
+      else {
+        result.msie = t
+        result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/msie|trident/i.test(ua)) {
+      result = {
+        name: 'Internet Explorer'
+      , msie: t
+      , version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/chrome.+? edge/i.test(ua)) {
+      result = {
+        name: 'Microsoft Edge'
+      , msedge: t
+      , version: edgeVersion
+      }
+    }
+    else if (/chrome|crios|crmo/i.test(ua)) {
+      result = {
+        name: 'Chrome'
+      , chrome: t
+      , version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (iosdevice) {
+      result = {
+        name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
+      }
+      // WTF: version is not part of user agent in web apps
+      if (versionIdentifier) {
+        result.version = versionIdentifier
+      }
+    }
+    else if (/sailfish/i.test(ua)) {
+      result = {
+        name: 'Sailfish'
+      , sailfish: t
+      , version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/seamonkey\//i.test(ua)) {
+      result = {
+        name: 'SeaMonkey'
+      , seamonkey: t
+      , version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/firefox|iceweasel/i.test(ua)) {
+      result = {
+        name: 'Firefox'
+      , firefox: t
+      , version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
+      }
+      if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
+        result.firefoxos = t
+      }
+    }
+    else if (/silk/i.test(ua)) {
+      result =  {
+        name: 'Amazon Silk'
+      , silk: t
+      , version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (android) {
+      result = {
+        name: 'Android'
+      , version: versionIdentifier
+      }
+    }
+    else if (/phantom/i.test(ua)) {
+      result = {
+        name: 'PhantomJS'
+      , phantom: t
+      , version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) {
+      result = {
+        name: 'BlackBerry'
+      , blackberry: t
+      , version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
+      }
+    }
+    else if (/(web|hpw)os/i.test(ua)) {
+      result = {
+        name: 'WebOS'
+      , webos: t
+      , version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
+      };
+      /touchpad\//i.test(ua) && (result.touchpad = t)
+    }
+    else if (/bada/i.test(ua)) {
+      result = {
+        name: 'Bada'
+      , bada: t
+      , version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
+      };
+    }
+    else if (/tizen/i.test(ua)) {
+      result = {
+        name: 'Tizen'
+      , tizen: t
+      , version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
+      };
+    }
+    else if (/safari/i.test(ua)) {
+      result = {
+        name: 'Safari'
+      , safari: t
+      , version: versionIdentifier
+      }
+    }
+    else {
+      result = {
+        name: getFirstMatch(/^(.*)\/(.*) /),
+        version: getSecondMatch(/^(.*)\/(.*) /)
+     };
+   }
+
+    // set webkit or gecko flag for browsers based on these engines
+    if (!result.msedge && /(apple)?webkit/i.test(ua)) {
+      result.name = result.name || "Webkit"
+      result.webkit = t
+      if (!result.version && versionIdentifier) {
+        result.version = versionIdentifier
+      }
+    } else if (!result.opera && /gecko\//i.test(ua)) {
+      result.name = result.name || "Gecko"
+      result.gecko = t
+      result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i)
+    }
+
+    // set OS flags for platforms that have multiple browsers
+    if (!result.msedge && (android || result.silk)) {
+      result.android = t
+    } else if (iosdevice) {
+      result[iosdevice] = t
+      result.ios = t
+    }
+
+    // OS version extraction
+    var osVersion = '';
+    if (result.windowsphone) {
+      osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);
+    } else if (iosdevice) {
+      osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i);
+      osVersion = osVersion.replace(/[_\s]/g, '.');
+    } else if (android) {
+      osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i);
+    } else if (result.webos) {
+      osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);
+    } else if (result.blackberry) {
+      osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i);
+    } else if (result.bada) {
+      osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i);
+    } else if (result.tizen) {
+      osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i);
+    }
+    if (osVersion) {
+      result.osversion = osVersion;
+    }
+
+    // device type extraction
+    var osMajorVersion = osVersion.split('.')[0];
+    if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {
+      result.tablet = t
+    } else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {
+      result.mobile = t
+    }
+
+    // Graded Browser Support
+    // http://developer.yahoo.com/yui/articles/gbs
+    if (result.msedge ||
+        (result.msie && result.version >= 10) ||
+        (result.chrome && result.version >= 20) ||
+        (result.firefox && result.version >= 20.0) ||
+        (result.safari && result.version >= 6) ||
+        (result.opera && result.version >= 10.0) ||
+        (result.ios && result.osversion && result.osversion.split(".")[0] >= 6) ||
+        (result.blackberry && result.version >= 10.1)
+        ) {
+      result.a = t;
+    }
+    else if ((result.msie && result.version < 10) ||
+        (result.chrome && result.version < 20) ||
+        (result.firefox && result.version < 20.0) ||
+        (result.safari && result.version < 6) ||
+        (result.opera && result.version < 10.0) ||
+        (result.ios && result.osversion && result.osversion.split(".")[0] < 6)
+        ) {
+      result.c = t
+    } else result.x = t
+
+    return result
+  }
+
+  var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')
+
+  bowser.test = function (browserList) {
+    for (var i = 0; i < browserList.length; ++i) {
+      var browserItem = browserList[i];
+      if (typeof browserItem=== 'string') {
+        if (browserItem in bowser) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /*
+   * Set our detect method to the main bowser object so we can
+   * reuse it to test other user agents.
+   * This is needed to implement future tests.
+   */
+  bowser._detect = detect;
+
+  return bowser
+});
+
+},{}],18:[function(require,module,exports){
+/*!
+ * @name JavaScript/NodeJS Merge v1.2.0
+ * @author yeikos
+ * @repository https://github.com/yeikos/js.merge
+
+ * Copyright 2014 yeikos - MIT license
+ * https://raw.github.com/yeikos/js.merge/master/LICENSE
+ */
+
+;(function(isNode) {
+
+	/**
+	 * Merge one or more objects 
+	 * @param bool? clone
+	 * @param mixed,... arguments
+	 * @return object
+	 */
+
+	var Public = function(clone) {
+
+		return merge(clone === true, false, arguments);
+
+	}, publicName = 'merge';
+
+	/**
+	 * Merge two or more objects recursively 
+	 * @param bool? clone
+	 * @param mixed,... arguments
+	 * @return object
+	 */
+
+	Public.recursive = function(clone) {
+
+		return merge(clone === true, true, arguments);
+
+	};
+
+	/**
+	 * Clone the input removing any reference
+	 * @param mixed input
+	 * @return mixed
+	 */
+
+	Public.clone = function(input) {
+
+		var output = input,
+			type = typeOf(input),
+			index, size;
+
+		if (type === 'array') {
+
+			output = [];
+			size = input.length;
+
+			for (index=0;index<size;++index)
+
+				output[index] = Public.clone(input[index]);
+
+		} else if (type === 'object') {
+
+			output = {};
+
+			for (index in input)
+
+				output[index] = Public.clone(input[index]);
+
+		}
+
+		return output;
+
+	};
+
+	/**
+	 * Merge two objects recursively
+	 * @param mixed input
+	 * @param mixed extend
+	 * @return mixed
+	 */
+
+	function merge_recursive(base, extend) {
+
+		if (typeOf(base) !== 'object')
+
+			return extend;
+
+		for (var key in extend) {
+
+			if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
+
+				base[key] = merge_recursive(base[key], extend[key]);
+
+			} else {
+
+				base[key] = extend[key];
+
+			}
+
+		}
+
+		return base;
+
+	}
+
+	/**
+	 * Merge two or more objects
+	 * @param bool clone
+	 * @param bool recursive
+	 * @param array argv
+	 * @return object
+	 */
+
+	function merge(clone, recursive, argv) {
+
+		var result = argv[0],
+			size = argv.length;
+
+		if (clone || typeOf(result) !== 'object')
+
+			result = {};
+
+		for (var index=0;index<size;++index) {
+
+			var item = argv[index],
+
+				type = typeOf(item);
+
+			if (type !== 'object') continue;
+
+			for (var key in item) {
+
+				var sitem = clone ? Public.clone(item[key]) : item[key];
+
+				if (recursive) {
+
+					result[key] = merge_recursive(result[key], sitem);
+
+				} else {
+
+					result[key] = sitem;
+
+				}
+
+			}
+
+		}
+
+		return result;
+
+	}
+
+	/**
+	 * Get type of variable
+	 * @param mixed input
+	 * @return string
+	 *
+	 * @see http://jsperf.com/typeofvar
+	 */
+
+	function typeOf(input) {
+
+		return ({}).toString.call(input).slice(8, -1).toLowerCase();
+
+	}
+
+	if (isNode) {
+
+		module.exports = Public;
+
+	} else {
+
+		window[publicName] = Public;
+
+	}
+
+})(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports);
+},{}],19:[function(require,module,exports){
+module.exports={
+  "name": "rtcninja",
+  "version": "0.6.2",
+  "description": "WebRTC API wrapper to deal with different browsers",
+  "author": {
+    "name": "Iñaki Baz Castillo",
+    "email": "inaki.baz@eface2face.com",
+    "url": "http://eface2face.com"
+  },
+  "contributors": [
+    {
+      "name": "Jesús Pérez",
+      "email": "jesus.perez@eface2face.com"
+    }
+  ],
+  "license": "MIT",
+  "main": "lib/rtcninja.js",
+  "homepage": "https://github.com/eface2face/rtcninja.js",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/eface2face/rtcninja.js.git"
+  },
+  "keywords": [
+    "webrtc"
+  ],
+  "engines": {
+    "node": ">=0.10.32"
+  },
+  "dependencies": {
+    "bowser": "^0.7.3",
+    "debug": "^2.2.0",
+    "merge": "^1.2.0"
+  },
+  "devDependencies": {
+    "browserify": "^10.2.3",
+    "gulp": "git+https://github.com/gulpjs/gulp.git#4.0",
+    "gulp-expect-file": "0.0.7",
+    "gulp-filelog": "^0.4.1",
+    "gulp-header": "^1.2.2",
+    "gulp-jscs": "^1.6.0",
+    "gulp-jscs-stylish": "^1.1.0",
+    "gulp-jshint": "^1.11.0",
+    "gulp-rename": "^1.2.2",
+    "gulp-uglify": "^1.2.0",
+    "jshint-stylish": "^1.0.2",
+    "retire": "^1.1.0",
+    "shelljs": "^0.5.0",
+    "vinyl-source-stream": "^1.1.0"
+  },
+  "gitHead": "9ddf6664289d9ab9da786edcd2f8b61b0633f013",
+  "bugs": {
+    "url": "https://github.com/eface2face/rtcninja.js/issues"
+  },
+  "_id": "rtcninja@0.6.2",
+  "scripts": {},
+  "_shasum": "ac274f4184c64d2d98c1da2cca914a2725dfcf09",
+  "_from": "rtcninja@>=0.6.2 <0.7.0",
+  "_npmVersion": "2.5.1",
+  "_nodeVersion": "0.12.0",
+  "_npmUser": {
+    "name": "ibc",
+    "email": "ibc@aliax.net"
+  },
+  "dist": {
+    "shasum": "ac274f4184c64d2d98c1da2cca914a2725dfcf09",
+    "tarball": "http://registry.npmjs.org/rtcninja/-/rtcninja-0.6.2.tgz"
+  },
+  "maintainers": [
+    {
+      "name": "ibc",
+      "email": "ibc@aliax.net"
+    }
+  ],
+  "directories": {},
+  "_resolved": "https://registry.npmjs.org/rtcninja/-/rtcninja-0.6.2.tgz",
+  "readme": "ERROR: No README data found!"
+}
+
+},{}],20:[function(require,module,exports){
+var _global = (function() { return this; })();
+var nativeWebSocket = _global.WebSocket || _global.MozWebSocket;
+
+
+/**
+ * Expose a W3C WebSocket class with just one or two arguments.
+ */
+function W3CWebSocket(uri, protocols) {
+	var native_instance;
+
+	if (protocols) {
+		native_instance = new nativeWebSocket(uri, protocols);
+	}
+	else {
+		native_instance = new nativeWebSocket(uri);
+	}
+
+	/**
+	 * 'native_instance' is an instance of nativeWebSocket (the browser's WebSocket
+	 * class). Since it is an Object it will be returned as it is when creating an
+	 * instance of W3CWebSocket via 'new W3CWebSocket()'.
+	 *
+	 * ECMAScript 5: http://bclary.com/2004/11/07/#a-13.2.2
+	 */
+	return native_instance;
+}
+
+
+/**
+ * Module exports.
+ */
+module.exports = {
+    'w3cwebsocket' : nativeWebSocket ? W3CWebSocket : null,
+    'version'      : require('./version')
+};
+
+},{"./version":21}],21:[function(require,module,exports){
+module.exports = require('../package.json').version;
+
+},{"../package.json":22}],22:[function(require,module,exports){
+module.exports={
+  "name": "websocket",
+  "description": "Websocket Client & Server Library implementing the WebSocket protocol as specified in RFC 6455.",
+  "keywords": [
+    "websocket",
+    "websockets",
+    "socket",
+    "networking",
+    "comet",
+    "push",
+    "RFC-6455",
+    "realtime",
+    "server",
+    "client"
+  ],
+  "author": {
+    "name": "Brian McKelvey",
+    "email": "brian@worlize.com",
+    "url": "https://www.worlize.com/"
+  },
+  "version": "1.0.19",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/theturtle32/WebSocket-Node.git"
+  },
+  "homepage": "https://github.com/theturtle32/WebSocket-Node",
+  "engines": {
+    "node": ">=0.8.0"
+  },
+  "dependencies": {
+    "debug": "~2.1.0",
+    "nan": "1.8.x",
+    "typedarray-to-buffer": "~3.0.0"
+  },
+  "devDependencies": {
+    "buffer-equal": "0.0.1",
+    "faucet": "0.0.1",
+    "gulp": "git+https://github.com/gulpjs/gulp.git#4.0",
+    "gulp-jshint": "^1.9.0",
+    "jshint-stylish": "^1.0.0",
+    "tape": "^3.0.0"
+  },
+  "config": {
+    "verbose": false
+  },
+  "scripts": {
+    "install": "(node-gyp rebuild 2> builderror.log) || (exit 0)",
+    "test": "faucet test/unit",
+    "gulp": "gulp"
+  },
+  "main": "index",
+  "directories": {
+    "lib": "./lib"
+  },
+  "browser": "lib/browser.js",
+  "license": "Apache-2.0",
+  "gitHead": "da3bd5b04e9442c84881b2e9c13432cdbbae1f16",
+  "bugs": {
+    "url": "https://github.com/theturtle32/WebSocket-Node/issues"
+  },
+  "_id": "websocket@1.0.19",
+  "_shasum": "e62dbf1a3c5e0767425db7187cfa38f921dfb42c",
+  "_from": "websocket@>=1.0.19 <2.0.0",
+  "_npmVersion": "2.10.1",
+  "_nodeVersion": "0.12.4",
+  "_npmUser": {
+    "name": "theturtle32",
+    "email": "brian@worlize.com"
+  },
+  "maintainers": [
+    {
+      "name": "theturtle32",
+      "email": "brian@worlize.com"
+    }
+  ],
+  "dist": {
+    "shasum": "e62dbf1a3c5e0767425db7187cfa38f921dfb42c",
+    "tarball": "http://registry.npmjs.org/websocket/-/websocket-1.0.19.tgz"
+  },
+  "_resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.19.tgz",
+  "readme": "ERROR: No README data found!"
+}
+
+},{}]},{},[4])(4)
+});
+
+
+//# sourceMappingURL=sylkrtc.js.map
\ No newline at end of file
diff --git a/resources/html/webrtcgateway/js/sylkrtc.js.map b/resources/html/webrtcgateway/js/sylkrtc.js.map
new file mode 100644
index 0000000..bea008f
--- /dev/null
+++ b/resources/html/webrtcgateway/js/sylkrtc.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["node_modules/browserify/node_modules/browser-pack/_prelude.js","lib/account.js","lib/call.js","lib/connection.js","lib/sylkrtc.js","node_modules/blueimp-md5/js/md5.js","node_modules/browserify/node_modules/events/events.js","node_modules/browserify/node_modules/process/browser.js","node_modules/browserify/node_modules/timers-browserify/main.js","node_modules/debug/browser.js","node_modules/debug/debug.js","node_modules/debug/node_modules/ms/index.js","node_modules/node-uuid/uuid.js","node_modules/rtcninja/lib/Adapter.js","node_modules/rtcninja/lib/RTCPeerConnection.js","node_modules/rtcninja/lib/rtcninja.js","node_modules/rtcninja/lib/version.js","node_modules/rtcninja/node_modules/bowser/bowser.js","node_modules/rtcninja/node_modules/merge/merge.js","node_modules/rtcninja/package.json","node_modules/websocket/lib/browser.js","node_modules/websocket/lib/version.js","node_modules/websocket/package.json"],"names":[],"mappings":"AAAA;ACAA,YAAY,CAAC;;AAEb,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE;AACzC,SAAK,EAAE,IAAI;CACd,CAAC,CAAC;;AAEH,IAAI,YAAY,GAAG,CAAC,YAAY;AAAE,aAAS,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE;AAAE,aAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAAE,gBAAI,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,AAAC,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,IAAI,KAAK,CAAC,AAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,AAAC,IAAI,OAAO,IAAI,UAAU,EAAE,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,AAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;SAAE;KAAE,AAAC,OAAO,UAAU,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE;AAAE,YAAI,UAAU,EAAE,gBAAgB,CAAC,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,AAAC,IAAI,WAAW,EAAE,gBAAgB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,AAAC,OAAO,WAAW,CAAC;KAAE,CAAC;CAAE,CAAA,EAAG,CAAC;;AAEtjB,IAAI,IAAI,GAAG,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;AAAE,QAAI,MAAM,GAAG,IAAI,CAAC,AAAC,SAAS,EAAE,OAAO,MAAM,EAAE;AAAE,YAAI,MAAM,GAAG,GAAG;YAAE,QAAQ,GAAG,GAAG;YAAE,QAAQ,GAAG,GAAG,CAAC,AAAC,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,AAAC,MAAM,GAAG,KAAK,CAAC,AAAC,IAAI,MAAM,KAAK,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,AAAC,IAAI,IAAI,GAAG,MAAM,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,AAAC,IAAI,IAAI,KAAK,SAAS,EAAE;AAAE,gBAAI,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,AAAC,IAAI,MAAM,KAAK,IAAI,EAAE;AAAE,uBAAO,SAAS,CAAC;aAAE,MAAM;AAAE,mBAAG,GAAG,MAAM,CAAC,AAAC,GAAG,GAAG,QAAQ,CAAC,AAAC,GAAG,GAAG,QAAQ,CAAC,AAAC,MAAM,GAAG,IAAI,CAAC,AAAC,SAAS,SAAS,CAAC;aAAE;SAAE,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE;AAAE,mBAAO,IAAI,CAAC,KAAK,CAAC;SAAE,MAAM;AAAE,gBAAI,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,AAAC,IAAI,MAAM,KAAK,SAAS,EAAE;AAAE,uBAAO,SAAS,CAAC;aAAE,AAAC,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;SAAE;KAAE;CAAE,CAAC;;AAE9pB,SAAS,sBAAsB,CAAC,GAAG,EAAE;AAAE,WAAO,GAAG,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;CAAE;;AAEjG,SAAS,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE;AAAE,QAAI,EAAE,QAAQ,YAAY,WAAW,CAAA,AAAC,EAAE;AAAE,cAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAC;KAAE;CAAE;;AAEzJ,SAAS,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE;AAAE,QAAI,OAAO,UAAU,KAAK,UAAU,IAAI,UAAU,KAAK,IAAI,EAAE;AAAE,cAAM,IAAI,SAAS,CAAC,0DAA0D,GAAG,OAAO,UAAU,CAAC,CAAC;KAAE,AAAC,QAAQ,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,UAAU,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,AAAC,IAAI,UAAU,EAAE,QAAQ,CAAC,SAAS,GAAG,UAAU,CAAC;CAAE;;AAExa,IAAI,MAAM,GAAG,OAAO,CAdF,OAAO,CAAA,CAAA;;AAgBzB,IAAI,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;;AAE7C,IAAI,WAAW,GAAG,OAAO,CAhBL,aAAa,CAAA,CAAA;;AAkBjC,IAAI,OAAO,GAAG,OAAO,CAjBQ,QAAQ,CAAA,CAAA;;AAmBrC,IAAI,KAAK,GAAG,OAAO,CAlBE,QAAQ,CAAA,CAAA;;AAE7B,IAAM,KAAK,GAAG,CAAA,CAAA,EAAA,OAAA,CAAA,SAAA,CAAA,CAAA,CAAM,iBAAiB,CAAC,CAAC;;AAoBvC,IAjBM,OAAO,GAAA,CAAA,UAAA,aAAA,EAAA;AAkBT,aAAS,CAlBP,OAAO,EAAA,aAAA,CAAA,CAAA;;AACE,aADT,OAAO,CACG,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE;AAoBlC,uBAAe,CAAC,IAAI,EArBtB,OAAO,CAAA,CAAA;;AAEL,YAAI,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;AACxB,kBAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;SACnD;AACD,YAAA,CAAA,MAAA,CAAA,cAAA,CALF,OAAO,CAAA,SAAA,CAAA,EAAA,aAAA,EAAA,IAAA,CAAA,CAAA,IAAA,CAAA,IAAA,CAAA,CAKG;AACR,YAAM,QAAQ,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AAClD,YAAM,MAAM,GAAG,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AACjD,YAAI,CAAC,GAAG,GAAG,EAAE,CAAC;AACd,YAAI,CAAC,SAAS,GAAG,CAAA,CAAA,EAAA,WAAA,CAhBhB,GAAG,CAAA,CAgBiB,QAAQ,GAAG,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAC,CAAC;AAC/D,YAAI,CAAC,WAAW,GAAG,UAAU,CAAC;AAC9B,YAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;AAC/B,YAAI,CAAC,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;KAC3B;;AAuBD,gBAAY,CApCV,OAAO,EAAA,CAAA;AAqCL,WAAG,EAAE,UAAU;AACf,aAAK,EAXD,SAAA,QAAA,GAAG;AAYH,gBAAI,KAAK,GAAG,IAAI,CAAC;;AAXrB,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,kBAAkB;AAC3B,uBAAO,EAAE,IAAI,CAAC,GAAG;aACpB,CAAC;AACF,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAC,KAAK,EAAK;AAC9B,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;AACnC,wBAAM,QAAQ,GAAG,KAAA,CAAK,kBAAkB,CAAC;AACzC,wBAAM,QAAQ,GAAG,QAAQ,CAAC;AAC1B,wBAAI,IAAI,GAAG,EAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAC,CAAC;AACtC,yBAAA,CAAK,kBAAkB,GAAG,QAAQ,CAAC;AACnC,yBAAA,CAAK,IAAI,CAAC,0BAA0B,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;iBACnE;aACJ,CAAC,CAAC;SACN;KAcA,EAAE;AACC,WAAG,EAAE,YAAY;AACjB,aAAK,EAdC,SAAA,UAAA,GAAG;AAeL,gBAAI,MAAM,GAAG,IAAI,CAAC;;AAdtB,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,oBAAoB;AAC7B,uBAAO,EAAE,IAAI,CAAC,GAAG;aACpB,CAAC;AACF,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAC,KAAK,EAAK;AAC9B,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;iBACxC;AACD,oBAAM,QAAQ,GAAG,MAAA,CAAK,kBAAkB,CAAC;AACzC,oBAAM,QAAQ,GAAG,IAAI,CAAC;AACtB,sBAAA,CAAK,kBAAkB,GAAG,QAAQ,CAAC;AACnC,sBAAA,CAAK,IAAI,CAAC,0BAA0B,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;aACjE,CAAC,CAAC;SACN;KAiBA,EAAE;AACC,WAAG,EAAE,MAAM;AACX,aAAK,EAjBL,SAAA,IAAA,CAAC,GAAG,EAAc;AAkBd,gBAlBE,OAAO,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAC,EAAE,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AAChB,gBAAI,IAAI,GAAG,IAAA,KAAA,CAlEV,IAAI,CAkEe,IAAI,CAAC,CAAC;AAC1B,gBAAI,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACjC,gBAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;AAC/B,gBAAI,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;AAChC,mBAAO,IAAI,CAAC;SACf;KAoBA,EAAE;AACC,WAAG,EAAE,cAAc;;;;AAInB,aAAK,EArBG,SAAA,YAAA,CAAC,OAAO,EAAE;AAClB,iBAAK,CAAC,4BAA4B,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACnD,oBAAQ,OAAO,CAAC,KAAK;AACjB,qBAAK,oBAAoB;AACrB,wBAAM,QAAQ,GAAG,IAAI,CAAC,kBAAkB,CAAC;AACzC,wBAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;AACpC,wBAAI,IAAI,GAAG,EAAE,CAAC;AACd,wBAAI,CAAC,kBAAkB,GAAG,QAAQ,CAAC;AACnC,wBAAI,QAAQ,KAAK,QAAQ,EAAE;AACvB,4BAAI,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;qBACrC;AACD,wBAAI,CAAC,IAAI,CAAC,0BAA0B,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;AAChE,0BAAM;AAAA,qBACL,kBAAkB;AACnB,wBAAI,IAAI,GAAG,IAAA,KAAA,CAzFlB,IAAI,CAyFuB,IAAI,CAAC,CAAC;AAC1B,wBAAI,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/E,wBAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;AAC/B,wBAAI,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;AAChC,0BAAM;AAAA;AAEN,0BAAM;AAAA,aACb;SACJ;KAsBA,EAAE;AACC,WAAG,EAAE,cAAc;AACnB,aAAK,EAtBG,SAAA,YAAA,CAAC,GAAG,EAAE,EAAE,EAAE;AAClB,gBAAI,CAAC,WAAW,CAAC,YAAY,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;SAC1C;KAuBA,EAAE;AACC,WAAG,EAAE,IAAI;AACT,WAAG,EA1GD,SAAA,GAAA,GAAG;AACL,mBAAO,IAAI,CAAC,GAAG,CAAC;SACnB;KA2GA,EAAE;AACC,WAAG,EAAE,UAAU;AACf,WAAG,EA3GK,SAAA,GAAA,GAAG;AACX,mBAAO,IAAI,CAAC,SAAS,CAAC;SACzB;KA4GA,EAAE;AACC,WAAG,EAAE,mBAAmB;AACxB,WAAG,EA5Gc,SAAA,GAAA,GAAG;AACpB,mBAAO,IAAI,CAAC,kBAAkB,CAAC;SAClC;KA6GA,CAAC,CAAC,CAAC;;AAEJ,WAxIE,OAAO,CAAA;CAyIZ,CAAA,CAAE,OAAO,CA/ID,YAAY,CAAA,CAAA;;AAiJrB,OAAO,CAtCE,OAAO,GAAP,OAAO,CAAA;;;AChHhB,YAAY,CAAC;;AAEb,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE;AACzC,SAAK,EAAE,IAAI;CACd,CAAC,CAAC;;AAEH,IAAI,YAAY,GAAG,CAAC,YAAY;AAAE,aAAS,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE;AAAE,aAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAAE,gBAAI,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,AAAC,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,IAAI,KAAK,CAAC,AAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,AAAC,IAAI,OAAO,IAAI,UAAU,EAAE,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,AAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;SAAE;KAAE,AAAC,OAAO,UAAU,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE;AAAE,YAAI,UAAU,EAAE,gBAAgB,CAAC,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,AAAC,IAAI,WAAW,EAAE,gBAAgB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,AAAC,OAAO,WAAW,CAAC;KAAE,CAAC;CAAE,CAAA,EAAG,CAAC;;AAEtjB,IAAI,IAAI,GAAG,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;AAAE,QAAI,MAAM,GAAG,IAAI,CAAC,AAAC,SAAS,EAAE,OAAO,MAAM,EAAE;AAAE,YAAI,MAAM,GAAG,GAAG;YAAE,QAAQ,GAAG,GAAG;YAAE,QAAQ,GAAG,GAAG,CAAC,AAAC,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,AAAC,MAAM,GAAG,KAAK,CAAC,AAAC,IAAI,MAAM,KAAK,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,AAAC,IAAI,IAAI,GAAG,MAAM,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,AAAC,IAAI,IAAI,KAAK,SAAS,EAAE;AAAE,gBAAI,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,AAAC,IAAI,MAAM,KAAK,IAAI,EAAE;AAAE,uBAAO,SAAS,CAAC;aAAE,MAAM;AAAE,mBAAG,GAAG,MAAM,CAAC,AAAC,GAAG,GAAG,QAAQ,CAAC,AAAC,GAAG,GAAG,QAAQ,CAAC,AAAC,MAAM,GAAG,IAAI,CAAC,AAAC,SAAS,SAAS,CAAC;aAAE;SAAE,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE;AAAE,mBAAO,IAAI,CAAC,KAAK,CAAC;SAAE,MAAM;AAAE,gBAAI,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,AAAC,IAAI,MAAM,KAAK,SAAS,EAAE;AAAE,uBAAO,SAAS,CAAC;aAAE,AAAC,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;SAAE;KAAE;CAAE,CAAC;;AAE9pB,SAAS,sBAAsB,CAAC,GAAG,EAAE;AAAE,WAAO,GAAG,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;CAAE;;AAEjG,SAAS,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE;AAAE,QAAI,EAAE,QAAQ,YAAY,WAAW,CAAA,AAAC,EAAE;AAAE,cAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAC;KAAE;CAAE;;AAEzJ,SAAS,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE;AAAE,QAAI,OAAO,UAAU,KAAK,UAAU,IAAI,UAAU,KAAK,IAAI,EAAE;AAAE,cAAM,IAAI,SAAS,CAAC,0DAA0D,GAAG,OAAO,UAAU,CAAC,CAAC;KAAE,AAAC,QAAQ,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,UAAU,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,AAAC,IAAI,UAAU,EAAE,QAAQ,CAAC,SAAS,GAAG,UAAU,CAAC;CAAE;;AAExa,IAAI,MAAM,GAAG,OAAO,CAdF,OAAO,CAAA,CAAA;;AAgBzB,IAAI,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;;AAE7C,IAAI,SAAS,GAAG,OAAO,CAjBN,WAAW,CAAA,CAAA;;AAmB5B,IAAI,UAAU,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;;AAEnD,IAAI,SAAS,GAAG,OAAO,CApBF,UAAU,CAAA,CAAA;;AAsB/B,IAAI,UAAU,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;;AAEnD,IAAI,OAAO,GAAG,OAAO,CAtBQ,QAAQ,CAAA,CAAA;;AAErC,IAAM,KAAK,GAAG,CAAA,CAAA,EAAA,OAAA,CAAA,SAAA,CAAA,CAAA,CAAM,cAAc,CAAC,CAAC;;AAwBpC,IArBM,IAAI,GAAA,CAAA,UAAA,aAAA,EAAA;AAsBN,aAAS,CAtBP,IAAI,EAAA,aAAA,CAAA,CAAA;;AACK,aADT,IAAI,CACM,OAAO,EAAE;AAwBjB,uBAAe,CAAC,IAAI,EAzBtB,IAAI,CAAA,CAAA;;AAEF,YAAA,CAAA,MAAA,CAAA,cAAA,CAFF,IAAI,CAAA,SAAA,CAAA,EAAA,aAAA,EAAA,IAAA,CAAA,CAAA,IAAA,CAAA,IAAA,CAAA,CAEM;AACR,YAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;AACxB,YAAI,CAAC,GAAG,GAAG,IAAI,CAAC;AAChB,YAAI,CAAC,UAAU,GAAG,IAAI,CAAC;AACvB,YAAI,CAAC,GAAG,GAAG,IAAI,CAAC;AAChB,YAAI,CAAC,MAAM,GAAG,IAAI,CAAC;AACnB,YAAI,CAAC,WAAW,GAAG,KAAK,CAAC;AACzB,YAAI,CAAC,YAAY,GAAG,IAAI,CAAC;AACzB,YAAI,CAAC,eAAe,GAAG,IAAI,CAAC;KAC/B;;AA2BD,gBAAY,CAtCV,IAAI,EAAA,CAAA;AAuCF,WAAG,EAAE,iBAAiB;AACtB,aAAK,EAHM,SAAA,eAAA,GAAG;AACd,gBAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE;AACnB,uBAAO,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC;aACrC,MAAM;AACH,uBAAO,EAAE,CAAC;aACb;SACJ;KAIA,EAAE;AACC,WAAG,EAAE,kBAAkB;AACvB,aAAK,EAJO,SAAA,gBAAA,GAAG;AACf,gBAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE;AACnB,uBAAO,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;aACtC,MAAM;AACH,uBAAO,EAAE,CAAC;aACb;SACJ;KAKA,EAAE;AACC,WAAG,EAAE,QAAQ;AACb,aAAK,EALH,SAAA,MAAA,GAAe;AAMb,gBAND,OAAO,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAG,EAAE,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AACf,gBAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE;AAC5B,sBAAM,IAAI,KAAK,CAAC,qCAAqC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;aACxE;;AAED,gBAAM,IAAI,GAAG,IAAI,CAAC;AAClB,gBAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAC,UAAU,EAAC,EAAE,EAAC,CAAC;AACrD,gBAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,EAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAC,CAAC;AAChF,gBAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;;;AAG5C,gBAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC;;;AAGtC,sBAAA,CAAA,SAAA,CAAA,CAAS,YAAY,CACjB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,CAClB,CAAC;;AAEF,qBAAS,kBAAkB,CAAC,MAAM,EAAE;;AAEhC,oBAAI,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAC3B,oBAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;;AAEtC,oBAAI,CAAC,GAAG,CAAC,oBAAoB,CACzB,IAAI,UAAA,CAAA,SAAA,CAAA,CAAS,qBAAqB,CAAC,EAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,YAAY,EAAC,CAAC;;AAE3E,4BAAW;AACP,wBAAI,CAAC,eAAe,CAChB,QAAQ,EACR,aAAa;;AAEb,8BAAS,GAAG,EAAE;AACV,6BAAK,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;AAC5B,4BAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;qBACzB;;AAED,8BAAS,KAAK,EAAE;AACZ,6BAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;AAC7C,4BAAI,CAAC,SAAS,EAAE,CAAC;qBACpB,CACJ,CAAC;iBACL;;AAED,0BAAS,KAAK,EAAE;AACZ,yBAAK,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAC;AACrD,wBAAI,CAAC,SAAS,EAAE,CAAC;iBACpB,CACJ,CAAC;aACL;;AAED,qBAAS,eAAe,CAAC,KAAK,EAAE;AAC5B,qBAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;AAC7C,oBAAI,CAAC,SAAS,EAAE,CAAC;aACpB;SACJ;KADA,EAAE;AACC,WAAG,EAAE,WAAW;AAChB,aAAK,EACA,SAAA,SAAA,GAAG;AACR,gBAAI,IAAI,CAAC,WAAW,EAAE;AAClB,uBAAO;aACV;;AAED,gBAAI,CAAC,cAAc,EAAE,CAAC;SACzB;KAAA,EAAE;AACC,WAAG,EAAE,eAAe;;;;AAIpB,aAAK,EADI,SAAA,aAAA,CAAC,GAAG,EAAc;AAEvB,gBAFW,OAAO,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAC,EAAE,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AACzB,gBAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE;AACzB,sBAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC;aAClC;;AAED,gBAAI,CAAC,GAAG,GAAG,UAAA,CAAA,SAAA,CAAA,CAAK,EAAE,EAAE,CAAC;AACrB,gBAAI,CAAC,UAAU,GAAG,UAAU,CAAC;;AAE7B,gBAAM,IAAI,GAAG,IAAI,CAAC;AAClB,gBAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAC,UAAU,EAAC,EAAE,EAAC,CAAC;AACrD,gBAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,EAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAC,CAAC;AAChF,gBAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;;;AAG1C,gBAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC;;;AAGtC,sBAAA,CAAA,SAAA,CAAA,CAAS,YAAY,CACjB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,CAClB,CAAC;;AAEF,qBAAS,kBAAkB,CAAC,MAAM,EAAE;;AAEhC,oBAAI,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAC3B,oBAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;;AAEtC,oBAAI,CAAC,eAAe,CAChB,OAAO,EACP,YAAY;;AAEZ,0BAAS,GAAG,EAAE;AACV,yBAAK,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;AAC5B,wBAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;iBAC5B;;AAED,0BAAS,KAAK,EAAE;AACZ,yBAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;AAC7C,wBAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;iBAC/B,CACJ,CAAC;aACL;;AAED,qBAAS,eAAe,CAAC,KAAK,EAAE;AAC5B,qBAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;AAC7C,oBAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;aAC/B;SACJ;KAHA,EAAE;AACC,WAAG,EAAE,eAAe;AACpB,aAAK,EAGI,SAAA,aAAA,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE;AAC3B,gBAAI,CAAC,GAAG,GAAG,EAAE,CAAC;AACd,gBAAI,CAAC,eAAe,GAAG,MAAM,CAAC;AAC9B,gBAAI,CAAC,YAAY,GAAG,GAAG,CAAC;AACxB,gBAAI,CAAC,UAAU,GAAG,UAAU,CAAC;AAC7B,gBAAI,CAAC,MAAM,GAAG,UAAU,CAAC;SAC5B;KAFA,EAAE;AACC,WAAG,EAAE,cAAc;AACnB,aAAK,EAEG,SAAA,YAAA,CAAC,OAAO,EAAE;AADd,gBAAI,KAAK,GAAG,IAAI,CAAC;;AAErB,iBAAK,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;AACjC,oBAAQ,OAAO,CAAC,KAAK;AACjB,qBAAK,OAAO;AACR,wBAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;AAC7B,wBAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;AACpC,wBAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;AACvB,wBAAI,IAAI,GAAG,EAAE,CAAC;;AAEd,wBAAI,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,UAAU,KAAK,UAAU,EAAE;AACvD,yBAAC,YAAY;AAAjB,gCAAM,IAAI,GAAA,KAAO,CAAC;AAClB,iCAAA,CAAK,GAAG,CAAC,oBAAoB,CACzB,IAAI,UAAA,CAAA,SAAA,CAAA,CAAS,qBAAqB,CAAC,EAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAC,CAAC;;AAE3E,wCAAW;AACP,qCAAK,CAAC,eAAe,CAAC,CAAC;AACvB,oCAAI,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;6BACvD;;AAED,sCAAS,KAAK,EAAE;AACZ,qCAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;AACzC,oCAAI,CAAC,SAAS,EAAE,CAAC;6BACpB,CACJ,CAAC;yBAAG,CAAA,EAAG,CAAC;qBACZ,MAAM;AACH,4BAAI,QAAQ,KAAK,YAAY,EAAE;AAC3B,gCAAI,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;AAClC,gCAAI,CAAC,WAAW,GAAG,IAAI,CAAC;yBAC3B;AACD,4BAAI,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;AACpD,4BAAI,QAAQ,KAAK,YAAY,EAAE;AAC3B,gCAAI,CAAC,uBAAuB,EAAE,CAAC;yBAClC;qBACJ;AACD,0BAAM;AAAA;AAEN,0BAAM;AAAA,aACb;SACJ;KACA,EAAE;AACC,WAAG,EAAE,wBAAwB;AAC7B,aAAK,EADa,SAAA,sBAAA,CAAC,QAAQ,EAAE;AAC7B,gBAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE;AACnB,sBAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;aAC5D;;AAED,gBAAM,IAAI,GAAG,IAAI,CAAC;AAClB,gBAAI,CAAC,GAAG,GAAG,IAAI,UAAA,CAAA,SAAA,CAAA,CAAS,iBAAiB,CAAC,QAAQ,CAAC,CAAC;AACpD,gBAAI,CAAC,GAAG,CAAC,WAAW,GAAG,UAAS,KAAK,EAAE,MAAM,EAAE;AAC3C,qBAAK,CAAC,cAAc,CAAC,CAAC;AACtB,oBAAI,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;aACpC,CAAC;AACF,gBAAI,CAAC,GAAG,CAAC,cAAc,GAAG,UAAS,KAAK,EAAE;AACtC,oBAAI,SAAS,GAAG,IAAI,CAAC;AACrB,oBAAI,KAAK,CAAC,SAAS,KAAK,IAAI,EAAE;AAC1B,6BAAS,GAAG;AACR,mCAAW,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS;AACtC,gCAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM;AAChC,uCAAe,EAAE,KAAK,CAAC,SAAS,CAAC,aAAa;qBACjD,CAAC;AACF,yBAAK,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC;iBAC5C;AACD,oBAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;aAChC,CAAC;SACL;KAEA,EAAE;AACC,WAAG,EAAE,iBAAiB;AACtB,aAAK,EAFM,SAAA,eAAA,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE;AACjD,gBAAM,IAAI,GAAG,IAAI,CAAC;;AAElB,gBAAI,IAAI,KAAK,OAAO,EAAE;AAClB,oBAAI,CAAC,GAAG,CAAC,WAAW;;AAEhB,+BAAe;;AAEf,uBAAO;;AAEP,uBAAO,CACV,CAAC;aACL,MAAM,IAAI,IAAI,KAAK,QAAQ,EAAE;AAC1B,oBAAI,CAAC,GAAG,CAAC,YAAY;;AAEjB,+BAAe;;AAEf,uBAAO;;AAEP,uBAAO,CACV,CAAC;aACL,MAAM;AACH,sBAAM,IAAI,KAAK,CAAC,yCAAyC,GAAE,IAAI,GAAE,aAAa,CAAC,CAAC;aACnF;;AAED,qBAAS,eAAe,CAAC,IAAI,EAAE;AAC3B,oBAAI,CAAC,GAAG,CAAC,mBAAmB,CACxB,IAAI;;AAEJ,4BAAW;AACP,6BAAS,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;iBAC5C;;AAED,uBAAO,CACV,CAAC;aACL;;AAED,qBAAS,OAAO,CAAC,KAAK,EAAE;AACpB,yBAAS,CAAC,KAAK,CAAC,CAAC;aACpB;SACJ;KADA,EAAE;AACC,WAAG,EAAE,cAAc;AACnB,aAAK,EACG,SAAA,YAAA,CAAC,GAAG,EAAE,EAAE,EAAE;AAClB,gBAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;SACvC;KAAA,EAAE;AACC,WAAG,EAAE,WAAW;AAChB,aAAK,EAAA,SAAA,SAAA,CAAC,GAAG,EAAE,GAAG,EAAE;AAChB,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,gBAAgB;AACzB,uBAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE;AACxB,uBAAO,EAAE,IAAI,CAAC,EAAE;AAChB,mBAAG,EAAE,GAAG;AACR,mBAAG,EAAE,GAAG;aACX,CAAC;AACF,gBAAM,IAAI,GAAG,IAAI,CAAC;AAClB,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAS,KAAK,EAAE;AACnC,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;AAC/B,wBAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;iBAC/B;aACJ,CAAC,CAAC;SACN;KACA,EAAE;AACC,WAAG,EAAE,gBAAgB;AACrB,aAAK,EADK,SAAA,cAAA,GAAG;AACb,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,mBAAmB;AAC5B,uBAAO,EAAE,IAAI,CAAC,EAAE;aACnB,CAAC;AACF,gBAAM,IAAI,GAAG,IAAI,CAAC;AAClB,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAS,KAAK,EAAE;AACnC,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;AAC3C,wBAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;iBAC/B;AACD,oBAAI,CAAC,WAAW,GAAG,IAAI,CAAC;aAC3B,CAAC,CAAC;SACN;KAEA,EAAE;AACC,WAAG,EAAE,cAAc;AACnB,aAAK,EAFG,SAAA,YAAA,CAAC,SAAS,EAAE;AACpB,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,iBAAiB;AAC1B,uBAAO,EAAE,IAAI,CAAC,EAAE;AAChB,0BAAU,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;aACpD,CAAC;AACF,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;SAChC;KAGA,EAAE;AACC,WAAG,EAAE,aAAa;AAClB,aAAK,EAHE,SAAA,WAAA,CAAC,GAAG,EAAE;AACb,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,gBAAgB;AACzB,uBAAO,EAAE,IAAI,CAAC,EAAE;AAChB,mBAAG,EAAE,GAAG;aACX,CAAC;AACF,gBAAM,IAAI,GAAG,IAAI,CAAC;AAClB,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAS,KAAK,EAAE;AACnC,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;AACjC,wBAAI,CAAC,SAAS,EAAE,CAAC;iBACpB;aACJ,CAAC,CAAC;SACN;KAIA,EAAE;AACC,WAAG,EAAE,yBAAyB;AAC9B,aAAK,EAJc,SAAA,uBAAA,GAAG;AACtB,iBAAK,CAAC,2BAA2B,CAAC,CAAC;AACnC,gBAAI,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE;AAKf,oBAAI,yBAAyB,GAAG,IAAI,CAAC;AACrC,oBAAI,iBAAiB,GAAG,KAAK,CAAC;AAC9B,oBAAI,cAAc,GAAG,SAAS,CAAC;;AAE/B,oBAAI;AARR,yBAAA,IAAA,SAAA,GAAmB,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAA,MAAA,CAAA,QAAA,CAAA,EAAA,EAAA,KAAA,EAAA,EAAA,yBAAA,GAAA,CAAA,KAAA,GAAA,SAAA,CAAA,IAAA,EAAA,CAAA,CAAA,IAAA,CAAA,EAAA,yBAAA,GAAA,IAAA,EAAE;AAUnC,4BAVH,MAAM,GAAA,KAAA,CAAA,KAAA,CAAA;;AACX,kCAAA,CAAA,SAAA,CAAA,CAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC;qBACrC;iBAYI,CAAC,OAAO,GAAG,EAAE;AACV,qCAAiB,GAAG,IAAI,CAAC;AACzB,kCAAc,GAAG,GAAG,CAAC;iBACxB,SAAS;AACN,wBAAI;AACA,4BAAI,CAAC,yBAAyB,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE;AACnD,qCAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;yBACzB;qBACJ,SAAS;AACN,4BAAI,iBAAiB,EAAE;AACnB,kCAAM,cAAc,CAAC;yBACxB;qBACJ;iBACJ;;AAED,oBAAI,0BAA0B,GAAG,IAAI,CAAC;AACtC,oBAAI,kBAAkB,GAAG,KAAK,CAAC;AAC/B,oBAAI,eAAe,GAAG,SAAS,CAAC;;AAEhC,oBAAI;AA9BR,yBAAA,IAAA,UAAA,GAAmB,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAA,MAAA,CAAA,QAAA,CAAA,EAAA,EAAA,MAAA,EAAA,EAAA,0BAAA,GAAA,CAAA,MAAA,GAAA,UAAA,CAAA,IAAA,EAAA,CAAA,CAAA,IAAA,CAAA,EAAA,0BAAA,GAAA,IAAA,EAAE;AAgCpC,4BAhCH,MAAM,GAAA,MAAA,CAAA,KAAA,CAAA;;AACX,kCAAA,CAAA,SAAA,CAAA,CAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC;qBACrC;iBAkCI,CAAC,OAAO,GAAG,EAAE;AACV,sCAAkB,GAAG,IAAI,CAAC;AAC1B,mCAAe,GAAG,GAAG,CAAC;iBACzB,SAAS;AACN,wBAAI;AACA,4BAAI,CAAC,0BAA0B,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE;AACrD,sCAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;yBAC1B;qBACJ,SAAS;AACN,4BAAI,kBAAkB,EAAE;AACpB,kCAAM,eAAe,CAAC;yBACzB;qBACJ;iBACJ;;AA9CL,oBAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;AACjB,oBAAI,CAAC,GAAG,GAAG,IAAI,CAAC;aACnB;SACJ;KAiDA,EAAE;AACC,WAAG,EAAE,iBAAiB;AACtB,aAAK,EAjDM,SAAA,eAAA,CAAC,KAAK,EAAE;AACnB,gBAAI,IAAI,CAAC,WAAW,EAAE;AAClB,uBAAO;aACV;AACD,gBAAI,CAAC,QAAQ,CAAC,MAAM,CAAA,QAAA,CAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACrC,gBAAI,CAAC,WAAW,GAAG,IAAI,CAAC;AACxB,gBAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;AAC7B,gBAAM,QAAQ,GAAG,YAAY,CAAC;AAC9B,gBAAI,IAAI,GAAG;AACP,sBAAM,EAAE,KAAK,CAAC,QAAQ,EAAE;aAC3B,CAAC;AACF,gBAAI,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;AACpD,gBAAI,CAAC,uBAAuB,EAAE,CAAC;SAClC;KAkDA,EAAE;AACC,WAAG,EAAE,SAAS;AACd,WAAG,EA5ZI,SAAA,GAAA,GAAG;AACV,mBAAO,IAAI,CAAC,QAAQ,CAAC;SACxB;KA6ZA,EAAE;AACC,WAAG,EAAE,IAAI;AACT,WAAG,EA7ZD,SAAA,GAAA,GAAG;AACL,mBAAO,IAAI,CAAC,GAAG,CAAC;SACnB;KA8ZA,EAAE;AACC,WAAG,EAAE,WAAW;AAChB,WAAG,EA9ZM,SAAA,GAAA,GAAG;AACZ,mBAAO,IAAI,CAAC,UAAU,CAAC;SAC1B;KA+ZA,EAAE;AACC,WAAG,EAAE,OAAO;AACZ,WAAG,EA/ZE,SAAA,GAAA,GAAG;AACR,mBAAO,IAAI,CAAC,MAAM,CAAC;SACtB;KAgaA,EAAE;AACC,WAAG,EAAE,eAAe;AACpB,WAAG,EAhaU,SAAA,GAAA,GAAG;AAChB,mBAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;SAC3B;KAiaA,EAAE;AACC,WAAG,EAAE,gBAAgB;AACrB,WAAG,EAjaW,SAAA,GAAA,GAAG;AACjB,mBAAO,IAAI,CAAC,eAAe,CAAC;SAC/B;KAkaA,CAAC,CAAC,CAAC;;AAEJ,WAvcE,IAAI,CAAA;CAwcT,CAAA,CAAE,OAAO,CA7cD,YAAY,CAAA,CAAA;;AA+crB,OAAO,CAjFE,IAAI,GAAJ,IAAI,CAAA;;;;ACpYb,YAAY,CAAC;;AAEb,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE;AACzC,SAAK,EAAE,IAAI;CACd,CAAC,CAAC;;AAEH,IAAI,YAAY,GAAG,CAAC,YAAY;AAAE,aAAS,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE;AAAE,aAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAAE,gBAAI,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,AAAC,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,IAAI,KAAK,CAAC,AAAC,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,AAAC,IAAI,OAAO,IAAI,UAAU,EAAE,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,AAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;SAAE;KAAE,AAAC,OAAO,UAAU,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE;AAAE,YAAI,UAAU,EAAE,gBAAgB,CAAC,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,AAAC,IAAI,WAAW,EAAE,gBAAgB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,AAAC,OAAO,WAAW,CAAC;KAAE,CAAC;CAAE,CAAA,EAAG,CAAC;;AAEtjB,IAAI,IAAI,GAAG,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;AAAE,QAAI,MAAM,GAAG,IAAI,CAAC,AAAC,SAAS,EAAE,OAAO,MAAM,EAAE;AAAE,YAAI,MAAM,GAAG,GAAG;YAAE,QAAQ,GAAG,GAAG;YAAE,QAAQ,GAAG,GAAG,CAAC,AAAC,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,AAAC,MAAM,GAAG,KAAK,CAAC,AAAC,IAAI,MAAM,KAAK,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,AAAC,IAAI,IAAI,GAAG,MAAM,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,AAAC,IAAI,IAAI,KAAK,SAAS,EAAE;AAAE,gBAAI,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,AAAC,IAAI,MAAM,KAAK,IAAI,EAAE;AAAE,uBAAO,SAAS,CAAC;aAAE,MAAM;AAAE,mBAAG,GAAG,MAAM,CAAC,AAAC,GAAG,GAAG,QAAQ,CAAC,AAAC,GAAG,GAAG,QAAQ,CAAC,AAAC,MAAM,GAAG,IAAI,CAAC,AAAC,SAAS,SAAS,CAAC;aAAE;SAAE,MAAM,IAAI,OAAO,IAAI,IAAI,EAAE;AAAE,mBAAO,IAAI,CAAC,KAAK,CAAC;SAAE,MAAM;AAAE,gBAAI,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,AAAC,IAAI,MAAM,KAAK,SAAS,EAAE;AAAE,uBAAO,SAAS,CAAC;aAAE,AAAC,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;SAAE;KAAE;CAAE,CAAC;;AAE9pB,SAAS,sBAAsB,CAAC,GAAG,EAAE;AAAE,WAAO,GAAG,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;CAAE;;AAEjG,SAAS,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE;AAAE,QAAI,EAAE,QAAQ,YAAY,WAAW,CAAA,AAAC,EAAE;AAAE,cAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAC;KAAE;CAAE;;AAEzJ,SAAS,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE;AAAE,QAAI,OAAO,UAAU,KAAK,UAAU,IAAI,UAAU,KAAK,IAAI,EAAE;AAAE,cAAM,IAAI,SAAS,CAAC,0DAA0D,GAAG,OAAO,UAAU,CAAC,CAAC;KAAE,AAAC,QAAQ,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,UAAU,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,AAAC,IAAI,UAAU,EAAE,QAAQ,CAAC,SAAS,GAAG,UAAU,CAAC;CAAE;;AAExa,IAAI,MAAM,GAAG,OAAO,CAdF,OAAO,CAAA,CAAA;;AAgBzB,IAAI,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;;AAE7C,IAAI,SAAS,GAAG,OAAO,CAjBN,WAAW,CAAA,CAAA;;AAmB5B,IAAI,UAAU,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;;AAEnD,IAAI,OAAO,GAAG,OAAO,CAnBQ,QAAQ,CAAA,CAAA;;AAqBrC,IAAI,OAAO,GAAG,OAAO,CApBQ,QAAQ,CAAA,CAAA;;AAsBrC,IAAI,UAAU,GAAG,OAAO,CArBqB,WAAW,CAAA,CAAA;;AAuBxD,IAAI,QAAQ,GAAG,OAAO,CAtBE,WAAW,CAAA,CAAA;;AAEnC,IAAM,aAAa,GAAG,WAAW,CAAC;AAClC,IAAM,KAAK,GAAG,CAAA,CAAA,EAAA,OAAA,CAAA,SAAA,CAAA,CAAA,CAAM,oBAAoB,CAAC,CAAC;AAC1C,IAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC;;AAwBjC,IArBM,UAAU,GAAA,CAAA,UAAA,aAAA,EAAA;AAsBZ,aAAS,CAtBP,UAAU,EAAA,aAAA,CAAA,CAAA;;AACD,aADT,UAAU,GACc;AAwBtB,YAxBQ,OAAO,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAG,EAAE,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AA0BpB,uBAAe,CAAC,IAAI,EA3BtB,UAAU,CAAA,CAAA;;AAER,YAAI,CAAC,OAAO,CAAC,MAAM,EAAE;AACjB,kBAAM,IAAI,KAAK,CAAC,4BAA8B,CAAC,CAAC;SACnD;AACD,YAAA,CAAA,MAAA,CAAA,cAAA,CALF,UAAU,CAAA,SAAA,CAAA,EAAA,aAAA,EAAA,IAAA,CAAA,CAAA,IAAA,CAAA,IAAA,CAAA,CAKA;AACR,YAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AAC7B,YAAI,CAAC,KAAK,GAAG,IAAI,CAAC;AAClB,YAAI,CAAC,MAAM,GAAG,IAAI,CAAC;AACnB,YAAI,CAAC,OAAO,GAAG,KAAK,CAAC;AACrB,YAAI,CAAC,MAAM,GAAG,IAAI,CAAC;AACnB,YAAI,CAAC,MAAM,GAAG,aAAa,CAAC;AAC5B,YAAI,CAAC,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;AAC3B,YAAI,CAAC,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;KAC9B;;AA6BD,gBAAY,CA3CV,UAAU,EAAA,CAAA;AA4CR,WAAG,EAAE,OAAO;AACZ,aAAK,EAzBJ,SAAA,KAAA,GAAG;AA0BA,gBAAI,KAAK,GAAG,IAAI,CAAC;;AAzBrB,gBAAI,IAAI,CAAC,OAAO,EAAE;AACd,uBAAO;aACV;AACD,gBAAI,CAAC,OAAO,GAAG,IAAI,CAAC;AACpB,gBAAI,IAAI,CAAC,MAAM,EAAE;AACb,4BAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1B,oBAAI,CAAC,MAAM,GAAG,IAAI,CAAC;aACtB;AACD,gBAAI,IAAI,CAAC,KAAK,EAAE;AACZ,oBAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;AACnB,oBAAI,CAAC,KAAK,GAAG,IAAI,CAAC;aACrB,MAAM;AACH,iBAAA,CAAA,EAAA,OAAA,CA1CH,YAAY,CAAA,CA0CI,YAAM;AACf,yBAAA,CAAK,SAAS,CAAC,QAAQ,CAAC,CAAC;iBAC5B,CAAC,CAAC;aACN;SACJ;KA4BA,EAAE;AACC,WAAG,EAAE,YAAY;AACjB,aAAK,EA5BC,SAAA,UAAA,GAA0B;AA6B5B,gBAAI,MAAM,GAAG,IAAI,CAAC;;AAElB,gBA/BG,OAAO,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAG,EAAE,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;AAgCf,gBAhCiB,EAAE,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAG,IAAI,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AAC9B,gBAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE;AAC7E,sBAAM,IAAI,KAAK,CAAC,4DAAgE,CAAC,CAAC;aACrF;AACD,gBAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;AACrC,sBAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;aAC5C;;AAED,gBAAI,GAAG,GAAG,IAAA,QAAA,CAtDT,OAAO,CAsDc,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;;AAE/D,gBAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;;AAEhC,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,aAAa;AACtB,uBAAO,EAAE,GAAG,CAAC,EAAE;AACf,wBAAQ,EAAE,GAAG,CAAC,QAAQ;aACzB,CAAC;AACF,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAC,KAAK,EAAK;AAC9B,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,uBAAuB,EAAE,KAAK,CAAC,CAAC;AACtC,0BAAA,CAAK,SAAS,CAAA,QAAA,CAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC9B,uBAAG,GAAG,IAAI,CAAC;iBACd;AACD,oBAAI,EAAE,EAAE;AACJ,sBAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;iBAClB;aACJ,CAAC,CAAC;SAEN;KAiCA,EAAE;AACC,WAAG,EAAE,eAAe;AACpB,aAAK,EAjCI,SAAA,aAAA,CAAC,OAAO,EAAW;AAkCxB,gBAlCe,EAAE,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAC,IAAI,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AAC1B,gBAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAC3C,gBAAI,OAAO,KAAK,GAAG,EAAE;AACjB,sBAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;aACtC;;;AAGD,gBAAI,CAAC,SAAS,CAAA,QAAA,CAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;;AAElC,gBAAI,GAAG,GAAG;AACN,uBAAO,EAAE,gBAAgB;AACzB,uBAAO,EAAE,GAAG,CAAC,EAAE;aAClB,CAAC;AACF,gBAAI,CAAC,YAAY,CAAC,GAAG,EAAE,UAAC,KAAK,EAAK;AAC9B,oBAAI,KAAK,EAAE;AACP,yBAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;iBAC5C;AACD,oBAAI,EAAE,EAAE;AACJ,sBAAE,EAAE,CAAC;iBACR;aACJ,CAAC,CAAC;SAEN;KAmCA,EAAE;AACC,WAAG,EAAE,aAAa;;;;AAIlB,aAAK,EApCE,SAAA,WAAA,GAAG;AAqCN,gBAAI,MAAM,GAAG,IAAI,CAAC;;AApCtB,gBAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE;AACrB,sBAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;aACpD;AACD,gBAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE;AACtB,sBAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;aAChD;;AAED,iBAAK,CAAC,cAAc,CAAC,CAAC;;AAEtB,gBAAI,OAAO,CAAC,OAAO,EAAE;AACjB,sBAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,YAAM;AAC1C,wBAAI,MAAA,CAAK,KAAK,KAAK,IAAI,EAAE;AACrB,4BAAI,IAAI,GAAG,SAAP,IAAI,GAAc,EAAE,CAAC;AACzB,8BAAA,CAAK,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;AAC1B,8BAAA,CAAK,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC;AAC5B,8BAAA,CAAK,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;AAC1B,8BAAA,CAAK,KAAK,CAAC,KAAK,EAAE,CAAC;qBACtB;iBACJ,CAAC,CAAC;aACN;;AAED,gBAAI,CAAC,MAAM,GAAG,UAAU,CAAC,YAAM;AAC3B,sBAAA,CAAK,QAAQ,EAAE,CAAC;aACnB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;SACnB;KAuCA,EAAE;AACC,WAAG,EAAE,UAAU;AACf,aAAK,EAvCD,SAAA,QAAA,GAAG;AAwCH,gBAAI,MAAM,GAAG,IAAI,CAAC;;AAvCtB,iBAAK,CAAC,sBAAsB,CAAC,CAAC;AAC9B,gBAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;;AAE7B,gBAAI,CAAC,KAAK,GAAG,IAAA,UAAA,CAtIZ,YAAY,CAsIiB,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAC1D,gBAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAM;AACtB,qBAAK,CAAC,2BAA2B,CAAC,CAAC;AACnC,sBAAA,CAAK,OAAO,EAAE,CAAC;aAClB,CAAC;AACF,gBAAI,CAAC,KAAK,CAAC,OAAO,GAAG,YAAM;AACvB,qBAAK,CAAC,gCAAgC,CAAC,CAAC;aAC3C,CAAC;AACF,gBAAI,CAAC,KAAK,CAAC,OAAO,GAAG,UAAC,KAAK,EAAK;AAC5B,qBAAK,CAAC,0DAA4D,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC9G,sBAAA,CAAK,QAAQ,EAAE,CAAC;aACnB,CAAC;AACF,gBAAI,CAAC,KAAK,CAAC,SAAS,GAAG,UAAC,KAAK,EAAK;AAC9B,qBAAK,CAAC,gCAAgC,EAAE,KAAK,CAAC,CAAC;AAC/C,sBAAA,CAAK,UAAU,CAAC,KAAK,CAAC,CAAC;aAC1B,CAAC;SACL;KA0CA,EAAE;AACC,WAAG,EAAE,cAAc;AACnB,aAAK,EA1CG,SAAA,YAAA,CAAC,GAAG,EAAE,EAAE,EAAE;AAClB,gBAAM,WAAW,GAAG,UAAA,CAAA,SAAA,CAAA,CAAK,EAAE,EAAE,CAAC;AAC9B,eAAG,CAAC,WAAW,GAAG,WAAW,CAAC;AAC9B,gBAAI,IAAI,CAAC,MAAM,KAAK,OAAO,EAAE;AACzB,iBAAA,CAAA,EAAA,OAAA,CA7JH,YAAY,CAAA,CA6JI,YAAM;AACf,sBAAE,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;iBAC5C,CAAC,CAAC;AACH,uBAAO;aACV;AACD,gBAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE,EAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAC,CAAC,CAAC;AACpD,gBAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;SACxC;KA2CA,EAAE;AACC,WAAG,EAAE,WAAW;AAChB,aAAK,EA3CA,SAAA,SAAA,CAAC,QAAQ,EAAE;AAChB,iBAAK,CAAC,qBAAqB,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACpD,gBAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;AAC7B,gBAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;AACvB,gBAAI,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;SACjD;KA4CA,EAAE;AACC,WAAG,EAAE,SAAS;;;;AAId,aAAK,EA7CF,SAAA,OAAA,GAAG;AACN,wBAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1B,gBAAI,CAAC,MAAM,GAAG,IAAI,CAAC;AACnB,gBAAI,CAAC,MAAM,GAAG,aAAa,CAAC;AAC5B,gBAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;SAC/B;KA8CA,EAAE;AACC,WAAG,EAAE,UAAU;AACf,aAAK,EA9CD,SAAA,QAAA,GAAG;AA+CH,gBAAI,MAAM,GAAG,IAAI,CAAC;;AA9CtB,gBAAI,CAAC,KAAK,GAAG,IAAI,CAAC;AAClB,gBAAI,IAAI,CAAC,MAAM,EAAE;AACb,4BAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1B,oBAAI,CAAC,MAAM,GAAG,IAAI,CAAC;aACtB;;;AAGD,gBAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;;AAEvB,gBAAI,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAC/B,gBAAI,CAAC,IAAI,CAAC,OAAO,EAAE;AACf,oBAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9B,oBAAI,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE;AAChC,wBAAI,CAAC,MAAM,GAAG,aAAa,CAAC;iBAC/B;AACD,qBAAK,CAAC,mCAAmC,EAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAC/D,oBAAI,CAAC,MAAM,GAAG,UAAU,CAAC,YAAM;AAC3B,0BAAA,CAAK,QAAQ,EAAE,CAAC;iBACnB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;aACnB,MAAM;AACH,oBAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;aAC5B;SACJ;KAiDA,EAAE;AACC,WAAG,EAAE,YAAY;AACjB,aAAK,EAjDC,SAAA,UAAA,CAAC,KAAK,EAAE;AACd,gBAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACrC,gBAAI,OAAO,OAAO,CAAC,OAAO,KAAK,WAAW,EAAE;AACxC,qBAAK,CAAC,+BAA+B,CAAC,CAAC;AACvC,uBAAO;aACV;;AAED,iBAAK,CAAC,2BAA6B,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;;AAE/D,gBAAI,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE;AAC7B,qBAAK,CAAC,sBAAwB,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AAC/C,wBAAQ,OAAO,CAAC,KAAK;AACjB,yBAAK,OAAO;AACR,4BAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AACxB,8BAAM;AAAA;AAEN,8BAAM;AAAA,iBACb;aACJ,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,eAAe,EAAE;AAC5C,oBAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AAC9C,oBAAI,CAAC,GAAG,EAAE;AACN,yBAAK,CAAC,sBAAsB,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;AAC/C,2BAAO;iBACV;AACD,mBAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;aAC7B,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,eAAe,EAAE;AAC5C,oBAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;AAkD9B,oBAAI,yBAAyB,GAAG,IAAI,CAAC;AACrC,oBAAI,iBAAiB,GAAG,KAAK,CAAC;AAC9B,oBAAI,cAAc,GAAG,SAAS,CAAC;;AAE/B,oBAAI;AArDR,yBAAA,IAAA,SAAA,GAAgB,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAA,MAAA,CAAA,QAAA,CAAA,EAAA,EAAA,KAAA,EAAA,EAAA,yBAAA,GAAA,CAAA,KAAA,GAAA,SAAA,CAAA,IAAA,EAAA,CAAA,CAAA,IAAA,CAAA,EAAA,yBAAA,GAAA,IAAA,EAAE;AAuD7B,4BAvDH,GAAG,GAAA,KAAA,CAAA,KAAA,CAAA;;AACR,4BAAI,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AACrC,4BAAI,IAAI,EAAE;AACN,gCAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAC3B,kCAAM;yBACT;qBACJ;iBAyDI,CAAC,OAAO,GAAG,EAAE;AACV,qCAAiB,GAAG,IAAI,CAAC;AACzB,kCAAc,GAAG,GAAG,CAAC;iBACxB,SAAS;AACN,wBAAI;AACA,4BAAI,CAAC,yBAAyB,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE;AACnD,qCAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;yBACzB;qBACJ,SAAS;AACN,4BAAI,iBAAiB,EAAE;AACnB,kCAAM,cAAc,CAAC;yBACxB;qBACJ;iBACJ;aArER,MAAM,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,IAAI,OAAO,CAAC,OAAO,KAAK,OAAO,EAAE;AACjE,oBAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AACxC,oBAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AAC7C,oBAAI,CAAC,IAAI,EAAE;AACP,yBAAK,CAAC,+BAA+B,EAAE,WAAW,CAAC,CAAC;AACpD,2BAAO;iBACV;AACD,oBAAI,CAAC,SAAS,CAAA,QAAA,CAAO,CAAC,WAAW,CAAC,CAAC;AACnC,qBAAK,CAAC,+BAAiC,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;AACpE,oBAAI,IAAI,CAAC,EAAE,EAAE;AACT,wBAAI,OAAO,CAAC,OAAO,KAAK,KAAK,EAAE;AAC3B,4BAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;qBACjB,MAAM;AACH,4BAAI,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;qBACrC;iBACJ;aACJ;SACJ;KAuEA,EAAE;AACC,WAAG,EAAE,OAAO;AACZ,WAAG,EAlTE,SAAA,GAAA,GAAG;AACR,mBAAO,IAAI,CAAC,MAAM,CAAC;SACtB;KAmTA,CAAC,CAAC,CAAC;;AAEJ,WAvUE,UAAU,CAAA;CAwUf,CAAA,CAAE,OAAO,CAlVD,YAAY,CAAA,CAAA;;AAoVrB,OAAO,CA5EE,UAAU,GAAV,UAAU,CAAA;;;;;AC7QnB,YAAY,CAAC;;AAEb,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE;AACzC,SAAK,EAAE,IAAI;CACd,CAAC,CAAC;;AAEH,SAAS,sBAAsB,CAAC,GAAG,EAAE;AAAE,WAAO,GAAG,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;CAAE;;AAEjG,IAAI,MAAM,GAAG,OAAO,CANF,OAAO,CAAA,CAAA;;AAQzB,IAAI,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;;AAE7C,IAAI,SAAS,GAAG,OAAO,CATF,UAAU,CAAA,CAAA;;AAW/B,IAAI,UAAU,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;;AAEnD,IAAI,WAAW,GAAG,OAAO,CAZE,cAAc,CAAA,CAAA;;;;AAKzC,SAAS,gBAAgB,GAAe;AAYpC,QAZsB,OAAO,GAAA,SAAA,CAAA,MAAA,IAAA,CAAA,IAAA,SAAA,CAAA,CAAA,CAAA,KAAA,SAAA,GAAG,EAAE,GAAA,SAAA,CAAA,CAAA,CAAA,CAAA;;AAClC,QAAI,CAAC,UAAA,CAAA,SAAA,CAAA,CAAS,SAAS,EAAE,EAAE;AACvB,cAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;KAClD;;AAED,QAAI,IAAI,GAAG,IAAA,WAAA,CAVN,UAAU,CAUW,OAAO,CAAC,CAAC;AACnC,QAAI,CAAC,WAAW,EAAE,CAAC;AACnB,WAAO,IAAI,CAAC;CACf;;;;AAKD,SAAS,iBAAiB,GAAG;AACzB,WAAO,UAAA,CAAA,SAAA,CAAA,CAAS,SAAS,EAAE,CAAC;CAC/B;;AAED,SAAS,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE;AACxC,cAAA,CAAA,SAAA,CAAA,CAAS,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;CAC/C;;AAED,SAAS,gBAAgB,CAAC,MAAM,EAAE;AAC9B,cAAA,CAAA,SAAA,CAAA,CAAS,gBAAgB,CAAC,MAAM,CAAC,CAAC;CACrC;;AAcD,OAAO,CAVH,gBAAgB,GAAhB,gBAAgB,CAAA;AAWpB,OAAO,CAVH,KAAK,GAAA,OAAA,CAAA,SAAA,CAAA,CAAA;AAWT,OAAO,CAVH,iBAAiB,GAAjB,iBAAiB,CAAA;AAWrB,OAAO,CAXgB,gBAAgB,GAAhB,gBAAgB,CAAA;AAYvC,OAAO,CAZkC,iBAAiB,GAAjB,iBAAiB,CAAA;;;ACtC1D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AClRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC7SA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC1FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC3EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC7HA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACvPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5SA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC7uBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxFA;AACA;AACA;AACA;AACA;AACA;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpRA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9KA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC7EA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACnCA;AACA;;ACDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"sylkrtc.js","sourceRoot":"/source/","sourcesContent":["(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})","'use strict';\n\nimport debug from 'debug';\n\nimport { md5 } from 'blueimp-md5';\nimport { EventEmitter } from 'events';\nimport { Call } from './call';\n\nconst DEBUG = debug('sylkrtc:Account');\n\n\nclass Account extends EventEmitter {\n    constructor(id, password, connection) {\n        if (id.indexOf('@') === -1) {\n            throw new Error('Invalid account id specified');\n        }\n        super();\n        const username = id.substring(0, id.indexOf('@'));\n        const domain = id.substring(id.indexOf('@') + 1);\n        this._id = id;\n        this._password = md5(username + ':' + domain + ':' + password);\n        this._connection = connection;\n        this._registrationState = null;\n        this._calls = new Map();\n    }\n\n    get id() {\n        return this._id;\n    }\n\n    get password() {\n        return this._password;\n    }\n\n    get registrationState() {\n        return this._registrationState;\n    }\n\n    register() {\n        let req = {\n            sylkrtc: 'account-register',\n            account: this._id\n        };\n        this._sendRequest(req, (error) => {\n            if (error) {\n                DEBUG('Register error: %s', error);\n                const oldState = this._registrationState;\n                const newState = 'failed';\n                let data = {reason: error.toString()};\n                this._registrationState = newState;\n                this.emit('registrationStateChanged', oldState, newState, data);\n            }\n        });\n    }\n\n    unregister() {\n        let req = {\n            sylkrtc: 'account-unregister',\n            account: this._id,\n        };\n        this._sendRequest(req, (error) => {\n            if (error) {\n                DEBUG('Unregister error: %s', error);\n            }\n            const oldState = this._registrationState;\n            const newState = null;\n            this._registrationState = newState;\n            this.emit('registrationStateChanged', oldState, newState, {});\n        });\n    }\n\n    call(uri, options={}) {\n        let call = new Call(this);\n        call._initOutgoing(uri, options);\n        this._calls.set(call.id, call);\n        this.emit('outgoingCall', call);\n        return call;\n    }\n\n    // Private API\n\n    _handleEvent(message) {\n        DEBUG('Received account event: %s', message.event);\n        switch (message.event) {\n            case 'registration_state':\n                const oldState = this._registrationState;\n                const newState = message.data.state;\n                let data = {};\n                this._registrationState = newState;\n                if (newState === 'failed') {\n                    data.reason = message.data.reason;\n                }\n                this.emit('registrationStateChanged', oldState, newState, data);\n                break;\n            case 'incoming_session':\n                let call = new Call(this);\n                call._initIncoming(message.session, message.data.originator, message.data.sdp);\n                this._calls.set(call.id, call);\n                this.emit('incomingCall', call);\n                break;\n            default:\n                break;\n        }\n    }\n\n    _sendRequest(req, cb) {\n        this._connection._sendRequest(req, cb);\n    }\n\n}\n\n\nexport { Account };\n","'use strict';\n\nimport debug from 'debug';\nimport uuid from 'node-uuid';\nimport rtcninja from 'rtcninja';\n\nimport { EventEmitter } from 'events';\n\nconst DEBUG = debug('sylkrtc:Call');\n\n\nclass Call extends EventEmitter {\n    constructor(account) {\n        super();\n        this._account = account;\n        this._id = null;\n        this._direction = null;\n        this._pc = null;\n        this._state = null;\n        this._terminated = false;\n        this._incomingSdp = null;\n        this._remoteIdentity = null;\n    }\n\n    get account() {\n        return this._account;\n    }\n\n    get id() {\n        return this._id;\n    }\n\n    get direction() {\n        return this._direction;\n    }\n\n    get state() {\n        return this._state;\n    }\n\n    get localIdentity() {\n        return this._account.id;\n    }\n\n    get remoteIdentity() {\n        return this._remoteIdentity;\n    }\n\n    getLocalStreams() {\n        if (this._pc !== null) {\n            return this._pc.getLocalStreams();\n        } else {\n            return [];\n        }\n    }\n\n    getRemoteStreams() {\n        if (this._pc !== null) {\n            return this._pc.getRemoteStreams();\n        } else {\n            return [];\n        }\n    }\n\n    answer(options = {}) {\n        if (this._state !== 'incoming') {\n            throw new Error('Call is not in the incoming state: ' + this._state);\n        }\n\n        const self = this;\n        const pcConfig = options.pcConfig || {iceServers:[]};\n        const mediaConstraints = options.mediaConstraints || {audio: true, video: true};\n        const answerOptions = options.answerOptions;\n\n        // Create the RTCPeerConnection\n        this._initRTCPeerConnection(pcConfig);\n\n        // Get the user media\n        rtcninja.getUserMedia(\n            mediaConstraints,\n            userMediaSucceeded,\n            userMediaFailed\n        );\n\n        function userMediaSucceeded(stream) {\n            // adding a local stream doesn't trigger the 'onaddstream' callback\n            self._pc.addStream(stream);\n            self.emit('localStreamAdded', stream);\n\n            self._pc.setRemoteDescription(\n                new rtcninja.RTCSessionDescription({type: 'offer', sdp: self._incomingSdp}),\n                // success\n                function() {\n                    self._createLocalSDP(\n                        'answer',\n                        answerOptions,\n                        // success\n                        function(sdp) {\n                            DEBUG('Local SDP: %s', sdp);\n                            self._sendAnswer(sdp);\n                        },\n                        // failure\n                        function(error) {\n                            DEBUG('Error creating local SDP: %s', error);\n                            self.terminate();\n                        }\n                    );\n                },\n                // failure\n                function(error) {\n                    DEBUG('Error setting remote description: %s', error);\n                    self.terminate();\n                }\n            );\n        }\n\n        function userMediaFailed(error) {\n            DEBUG('Error getting user media: %s', error);\n            self.terminate();\n        }\n    }\n\n    terminate() {\n        if (this._terminated) {\n            return;\n        }\n\n        this._sendTerminate();\n    }\n\n    // Private API\n\n    _initOutgoing(uri, options={}) {\n        if (uri.indexOf('@') === -1) {\n            throw new Error('Invalid URI');\n        }\n\n        this._id = uuid.v4();\n        this._direction = 'outgoing';\n\n        const self = this;\n        const pcConfig = options.pcConfig || {iceServers:[]};\n        const mediaConstraints = options.mediaConstraints || {audio: true, video: true};\n        const offerOptions = options.offerOptions;\n\n        // Create the RTCPeerConnection\n        this._initRTCPeerConnection(pcConfig);\n\n        // Get the user media\n        rtcninja.getUserMedia(\n            mediaConstraints,\n            userMediaSucceeded,\n            userMediaFailed\n        );\n\n        function userMediaSucceeded(stream) {\n            // adding a local stream doesn't trigger the 'onaddstream' callback\n            self._pc.addStream(stream);\n            self.emit('localStreamAdded', stream);\n\n            self._createLocalSDP(\n                'offer',\n                offerOptions,\n                // success\n                function(sdp) {\n                    DEBUG('Local SDP: %s', sdp);\n                    self._sendCall(uri, sdp);\n                },\n                // failure\n                function(error) {\n                    DEBUG('Error creating local SDP: %s', error);\n                    self._localTerminate(error);\n                }\n            );\n        }\n\n        function userMediaFailed(error) {\n            DEBUG('Error getting user media: %s', error);\n            self._localTerminate(error);\n        }\n    }\n\n    _initIncoming(id, caller, sdp) {\n        this._id = id;\n        this._remoteIdentity = caller;\n        this._incomingSdp = sdp;\n        this._direction = 'incoming';\n        this._state = 'incoming';\n    }\n\n    _handleEvent(message) {\n        DEBUG('Call event: %o', message);\n        switch (message.event) {\n            case 'state':\n                const oldState = this._state;\n                const newState = message.data.state;\n                this._state = newState;\n                let data = {};\n\n                if (newState === 'accepted' && this._direction === 'outgoing') {\n                    const self = this;\n                    this._pc.setRemoteDescription(\n                        new rtcninja.RTCSessionDescription({type: 'answer', sdp: message.data.sdp}),\n                        // success\n                        function() {\n                            DEBUG('Call accepted');\n                            self.emit('stateChanged', oldState, newState, data);\n                        },\n                        // failure\n                        function(error) {\n                            DEBUG('Error accepting call: %s', error);\n                            self.terminate();\n                        }\n                    );\n                } else {\n                    if (newState === 'terminated') {\n                        data.reason = message.data.reason;\n                        this._terminated = true;\n                    }\n                    this.emit('stateChanged', oldState, newState, data);\n                    if (newState === 'terminated') {\n                        this._closeRTCPeerConnection();\n                    }\n                }\n                break;\n            default:\n                break;\n        }\n    }\n\n    _initRTCPeerConnection(pcConfig) {\n        if (this._pc !== null) {\n            throw new Error('RTCPeerConnection already initialized');\n        }\n\n        const self = this;\n        this._pc = new rtcninja.RTCPeerConnection(pcConfig);\n        this._pc.onaddstream = function(event, stream) {\n            DEBUG('Stream added');\n            self.emit('streamAdded', stream);\n        };\n        this._pc.onicecandidate = function(event) {\n            let candidate = null;\n            if (event.candidate !== null) {\n                candidate = {\n                    'candidate': event.candidate.candidate,\n                    'sdpMid': event.candidate.sdpMid,\n                    'sdpMLineIndex': event.candidate.sdpMLineIndex\n                };\n                DEBUG('New ICE candidate %o', candidate);\n            }\n            self._sendTrickle(candidate);\n        };\n    }\n\n    _createLocalSDP(type, options, onSuccess, onFailure) {\n        const self = this;\n\n        if (type === 'offer') {\n            this._pc.createOffer(\n                // success\n                createSucceeded,\n                // failure\n                failure,\n                // options\n                options\n            );\n        } else if (type === 'answer') {\n            this._pc.createAnswer(\n                // success\n                createSucceeded,\n                // failure\n                failure,\n                // options\n                options\n            );\n        } else {\n            throw new Error('type must be \"offer\" or \"answer\", but \"' +type+ '\" was given');\n        }\n\n        function createSucceeded(desc) {\n            self._pc.setLocalDescription(\n                desc,\n                // success\n                function() {\n                    onSuccess(self._pc.localDescription.sdp);\n                },\n                // failure\n                failure\n            );\n        }\n\n        function failure(error) {\n            onFailure(error);\n        }\n    }\n\n    _sendRequest(req, cb) {\n        this._account._sendRequest(req, cb);\n    }\n\n    _sendCall(uri, sdp) {\n        let req = {\n            sylkrtc: 'session-create',\n            account: this.account.id,\n            session: this.id,\n            uri: uri,\n            sdp: sdp\n        };\n        const self = this;\n        this._sendRequest(req, function(error) {\n            if (error) {\n                DEBUG('Call error: %s', error);\n                self._localTerminate(error);\n            }\n        });\n    }\n\n    _sendTerminate() {\n        let req = {\n            sylkrtc: 'session-terminate',\n            session: this.id\n        };\n        const self = this;\n        this._sendRequest(req, function(error) {\n            if (error) {\n                DEBUG('Error terminating call: %s', error);\n                self._localTerminate(error);\n            }\n            self._terminated = true;\n        });\n    }\n\n    _sendTrickle(candidate) {\n        let req = {\n            sylkrtc: 'session-trickle',\n            session: this.id,\n            candidates: candidate !== null ? [candidate] : [],\n        };\n        this._sendRequest(req, null);\n    }\n\n    _sendAnswer(sdp) {\n        let req = {\n            sylkrtc: 'session-answer',\n            session: this.id,\n            sdp: sdp\n        };\n        const self = this;\n        this._sendRequest(req, function(error) {\n            if (error) {\n                DEBUG('Answer error: %s', error);\n                self.terminate();\n            }\n        });\n    }\n\n    _closeRTCPeerConnection() {\n        DEBUG('Closing RTCPeerConnection');\n        if (this._pc !== null) {\n            for (let stream of this._pc.getLocalStreams()) {\n                rtcninja.closeMediaStream(stream);\n            }\n            for (let stream of this._pc.getRemoteStreams()) {\n                rtcninja.closeMediaStream(stream);\n            }\n            this._pc.close();\n            this._pc = null;\n        }\n    }\n\n    _localTerminate(error) {\n        if (this._terminated) {\n            return;\n        }\n        this._account._calls.delete(this.id);\n        this._terminated = true;\n        const oldState = this._state;\n        const newState = 'terminated';\n        let data = {\n            reason: error.toString()\n        };\n        this.emit('stateChanged', oldState, newState, data);\n        this._closeRTCPeerConnection();\n    }\n}\n\n\nexport { Call };\n","'use strict';\n\nimport debug from 'debug';\nimport uuid from 'node-uuid';\n\nimport { EventEmitter } from 'events';\nimport { setImmediate } from 'timers';\nimport { w3cwebsocket as W3CWebSocket } from 'websocket';\nimport { Account } from './account';\n\nconst SYLKRTC_PROTO = 'sylkRTC-1';\nconst DEBUG = debug('sylkrtc:Connection');\nconst INITIAL_DELAY = 0.5 * 1000;\n\n\nclass Connection extends EventEmitter {\n    constructor(options = {}) {\n        if (!options.server) {\n            throw new Error('\\\"server\\\" must be specified');\n        }\n        super();\n        this._wsUri = options.server;\n        this._sock = null;\n        this._state = null;\n        this._closed = false;\n        this._timer = null;\n        this._delay = INITIAL_DELAY;\n        this._accounts = new Map();\n        this._requests = new Map();\n    }\n\n    get state() {\n        return this._state;\n    }\n\n    close() {\n        if (this._closed) {\n            return;\n        }\n        this._closed = true;\n        if (this._timer) {\n            clearTimeout(this._timer);\n            this._timer = null;\n        }\n        if (this._sock) {\n            this._sock.close();\n            this._sock = null;\n        } else {\n            setImmediate(() => {\n                this._setState('closed');\n            });\n        }\n    }\n\n    addAccount(options = {}, cb = null) {\n        if (typeof options.account !== 'string' || typeof options.password !== 'string') {\n            throw new Error('Invalid options, \\\"account\\\" and \\\"password\\\" must be supplied');\n        }\n        if (this._accounts.has(options.account)) {\n            throw new Error('Account already added');\n        }\n\n        let acc = new Account(options.account, options.password, this);\n        // add it early to the set so we don't add it more than once, ever\n        this._accounts.set(acc.id, acc);\n\n        let req = {\n            sylkrtc: 'account-add',\n            account: acc.id,\n            password: acc.password\n        };\n        this._sendRequest(req, (error) => {\n            if (error) {\n                DEBUG('add_account error: %s', error);\n                this._accounts.delete(acc.id);\n                acc = null;\n            }\n            if (cb) {\n                cb(error, acc);\n            }\n        });\n\n    }\n\n    removeAccount(account, cb=null) {\n        const acc = this._accounts.get(account.id);\n        if (account !== acc) {\n            throw new Error('Unknown account');\n        }\n\n        // delete the account from the mapping, regardless of the result\n        this._accounts.delete(account.id);\n\n        let req = {\n            sylkrtc: 'account-remove',\n            account: acc.id\n        };\n        this._sendRequest(req, (error) => {\n            if (error) {\n                DEBUG('remove_account error: %s', error);\n            }\n            if (cb) {\n                cb();\n            }\n        });\n\n    }\n\n    // Private API\n\n    _initialize() {\n        if (this._sock !== null) {\n            throw new Error('WebSocket already initialized');\n        }\n        if (this._timer !== null) {\n            throw new Error('Initialize is in progress');\n        }\n\n        DEBUG('Initializing');\n\n        if (process.browser) {\n            window.addEventListener('beforeunload', () => {\n                if (this._sock !== null) {\n                    let noop = function() {};\n                    this._sock.onerror = noop;\n                    this._sock.onmessage = noop;\n                    this._sock.onclose = noop;\n                    this._sock.close();\n                }\n            });\n        }\n\n        this._timer = setTimeout(() => {\n            this._connect();\n        }, this._delay);\n    }\n\n    _connect() {\n        DEBUG('WebSocket connecting');\n        this._setState('connecting');\n\n        this._sock = new W3CWebSocket(this._wsUri, SYLKRTC_PROTO);\n        this._sock.onopen = () => {\n            DEBUG('WebSocket connection open');\n            this._onOpen();\n        };\n        this._sock.onerror = () => {\n            DEBUG('WebSocket connection got error');\n        };\n        this._sock.onclose = (event) => {\n            DEBUG('WebSocket connection closed: %d: (reason=\\\"%s\\\", clean=%s)', event.code, event.reason, event.wasClean);\n            this._onClose();\n        };\n        this._sock.onmessage = (event) => {\n            DEBUG('WebSocket received message: %o', event);\n            this._onMessage(event);\n        };\n    }\n\n    _sendRequest(req, cb) {\n        const transaction = uuid.v4();\n        req.transaction = transaction;\n        if (this._state !== 'ready') {\n            setImmediate(() => {\n                cb(new Error('Connection is not ready'));\n            });\n            return;\n        }\n        this._requests.set(transaction, {req: req, cb: cb});\n        this._sock.send(JSON.stringify(req));\n    }\n\n    _setState(newState) {\n        DEBUG('Set state: %s -> %s', this._state, newState);\n        const oldState = this._state;\n        this._state = newState;\n        this.emit('stateChanged', oldState, newState);\n    }\n\n    // WebSocket callbacks\n\n    _onOpen() {\n        clearTimeout(this._timer);\n        this._timer = null;\n        this._delay = INITIAL_DELAY;\n        this._setState('connected');\n    }\n\n    _onClose() {\n        this._sock = null;\n        if (this._timer) {\n            clearTimeout(this._timer);\n            this._timer = null;\n        }\n\n        // remove all accounts, the server no longer has them anyway\n        this._accounts.clear();\n\n        this._setState('disconnected');\n        if (!this._closed) {\n            this._delay = this._delay * 2;\n            if (this._delay > Number.MAX_VALUE) {\n                this._delay = INITIAL_DELAY;\n            }\n            DEBUG('Retrying connection in %s seconds', this._delay / 1000);\n            this._timer = setTimeout(() => {\n                this._connect();\n            }, this._delay);\n        } else {\n            this._setState('closed');\n        }\n    }\n\n    _onMessage(event) {\n        let message = JSON.parse(event.data);\n        if (typeof message.sylkrtc === 'undefined') {\n            DEBUG('Unrecognized message received');\n            return;\n        }\n\n        DEBUG('Received \\\"%s\\\" message: %o', message.sylkrtc, message);\n\n        if (message.sylkrtc === 'event') {\n            DEBUG('Received event: \\\"%s\\\"', message.event);\n            switch (message.event) {\n                case 'ready':\n                    this._setState('ready');\n                    break;\n                default:\n                    break;\n            }\n        } else if (message.sylkrtc === 'account_event') {\n            let acc = this._accounts.get(message.account);\n            if (!acc) {\n                DEBUG('Account %s not found', message.account);\n                return;\n            }\n            acc._handleEvent(message);\n        } else if (message.sylkrtc === 'session_event') {\n            const sessionId = message.session;\n            for (let acc of this._accounts.values()) {\n                let call = acc._calls.get(sessionId);\n                if (call) {\n                    call._handleEvent(message);\n                    break;\n                }\n            }\n        } else if (message.sylkrtc === 'ack' || message.sylkrtc === 'error') {\n            const transaction = message.transaction;\n            const data = this._requests.get(transaction);\n            if (!data) {\n                DEBUG('Could not find transaction %s', transaction);\n                return;\n            }\n            this._requests.delete(transaction);\n            DEBUG('Received \\\"%s\\\" for request: %o', message.sylkrtc, data.req);\n            if (data.cb) {\n                if (message.sylkrtc === 'ack') {\n                    data.cb(null);\n                } else {\n                    data.cb(new Error(message.error));\n                }\n            }\n        }\n    }\n\n}\n\n\nexport { Connection };\n","'use strict';\n\nimport debug from 'debug';\nimport rtcninja from 'rtcninja';\nimport { Connection } from './connection';\n\n\n// Public API\n\nfunction createConnection(options = {}) {\n    if (!rtcninja.hasWebRTC()) {\n        throw new Error('WebRTC support not detected');\n    }\n\n    let conn = new Connection(options);\n    conn._initialize();\n    return conn;\n}\n\n\n// Some proxied functions from rtcninja\n\nfunction isWebRTCSupported() {\n    return rtcninja.hasWebRTC();\n}\n\nfunction attachMediaStream(element, stream) {\n    rtcninja.attachMediaStream(element, stream);\n}\n\nfunction closeMediaStream(stream) {\n    rtcninja.closeMediaStream(stream);\n}\n\n\nexport {\n    createConnection,\n    debug,\n    attachMediaStream, closeMediaStream, isWebRTCSupported\n};\n","/*\n * JavaScript MD5 1.0.1\n * https://github.com/blueimp/JavaScript-MD5\n *\n * Copyright 2011, Sebastian Tschan\n * https://blueimp.net\n *\n * Licensed under the MIT license:\n * http://www.opensource.org/licenses/MIT\n * \n * Based on\n * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message\n * Digest Algorithm, as defined in RFC 1321.\n * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009\n * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet\n * Distributed under the BSD License\n * See http://pajhome.org.uk/crypt/md5 for more info.\n */\n\n/*jslint bitwise: true */\n/*global unescape, define */\n\n(function ($) {\n    'use strict';\n\n    /*\n    * Add integers, wrapping at 2^32. This uses 16-bit operations internally\n    * to work around bugs in some JS interpreters.\n    */\n    function safe_add(x, y) {\n        var lsw = (x & 0xFFFF) + (y & 0xFFFF),\n            msw = (x >> 16) + (y >> 16) + (lsw >> 16);\n        return (msw << 16) | (lsw & 0xFFFF);\n    }\n\n    /*\n    * Bitwise rotate a 32-bit number to the left.\n    */\n    function bit_rol(num, cnt) {\n        return (num << cnt) | (num >>> (32 - cnt));\n    }\n\n    /*\n    * These functions implement the four basic operations the algorithm uses.\n    */\n    function md5_cmn(q, a, b, x, s, t) {\n        return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);\n    }\n    function md5_ff(a, b, c, d, x, s, t) {\n        return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);\n    }\n    function md5_gg(a, b, c, d, x, s, t) {\n        return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);\n    }\n    function md5_hh(a, b, c, d, x, s, t) {\n        return md5_cmn(b ^ c ^ d, a, b, x, s, t);\n    }\n    function md5_ii(a, b, c, d, x, s, t) {\n        return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);\n    }\n\n    /*\n    * Calculate the MD5 of an array of little-endian words, and a bit length.\n    */\n    function binl_md5(x, len) {\n        /* append padding */\n        x[len >> 5] |= 0x80 << (len % 32);\n        x[(((len + 64) >>> 9) << 4) + 14] = len;\n\n        var i, olda, oldb, oldc, oldd,\n            a =  1732584193,\n            b = -271733879,\n            c = -1732584194,\n            d =  271733878;\n\n        for (i = 0; i < x.length; i += 16) {\n            olda = a;\n            oldb = b;\n            oldc = c;\n            oldd = d;\n\n            a = md5_ff(a, b, c, d, x[i],       7, -680876936);\n            d = md5_ff(d, a, b, c, x[i +  1], 12, -389564586);\n            c = md5_ff(c, d, a, b, x[i +  2], 17,  606105819);\n            b = md5_ff(b, c, d, a, x[i +  3], 22, -1044525330);\n            a = md5_ff(a, b, c, d, x[i +  4],  7, -176418897);\n            d = md5_ff(d, a, b, c, x[i +  5], 12,  1200080426);\n            c = md5_ff(c, d, a, b, x[i +  6], 17, -1473231341);\n            b = md5_ff(b, c, d, a, x[i +  7], 22, -45705983);\n            a = md5_ff(a, b, c, d, x[i +  8],  7,  1770035416);\n            d = md5_ff(d, a, b, c, x[i +  9], 12, -1958414417);\n            c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);\n            b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);\n            a = md5_ff(a, b, c, d, x[i + 12],  7,  1804603682);\n            d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);\n            c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);\n            b = md5_ff(b, c, d, a, x[i + 15], 22,  1236535329);\n\n            a = md5_gg(a, b, c, d, x[i +  1],  5, -165796510);\n            d = md5_gg(d, a, b, c, x[i +  6],  9, -1069501632);\n            c = md5_gg(c, d, a, b, x[i + 11], 14,  643717713);\n            b = md5_gg(b, c, d, a, x[i],      20, -373897302);\n            a = md5_gg(a, b, c, d, x[i +  5],  5, -701558691);\n            d = md5_gg(d, a, b, c, x[i + 10],  9,  38016083);\n            c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);\n            b = md5_gg(b, c, d, a, x[i +  4], 20, -405537848);\n            a = md5_gg(a, b, c, d, x[i +  9],  5,  568446438);\n            d = md5_gg(d, a, b, c, x[i + 14],  9, -1019803690);\n            c = md5_gg(c, d, a, b, x[i +  3], 14, -187363961);\n            b = md5_gg(b, c, d, a, x[i +  8], 20,  1163531501);\n            a = md5_gg(a, b, c, d, x[i + 13],  5, -1444681467);\n            d = md5_gg(d, a, b, c, x[i +  2],  9, -51403784);\n            c = md5_gg(c, d, a, b, x[i +  7], 14,  1735328473);\n            b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);\n\n            a = md5_hh(a, b, c, d, x[i +  5],  4, -378558);\n            d = md5_hh(d, a, b, c, x[i +  8], 11, -2022574463);\n            c = md5_hh(c, d, a, b, x[i + 11], 16,  1839030562);\n            b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);\n            a = md5_hh(a, b, c, d, x[i +  1],  4, -1530992060);\n            d = md5_hh(d, a, b, c, x[i +  4], 11,  1272893353);\n            c = md5_hh(c, d, a, b, x[i +  7], 16, -155497632);\n            b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);\n            a = md5_hh(a, b, c, d, x[i + 13],  4,  681279174);\n            d = md5_hh(d, a, b, c, x[i],      11, -358537222);\n            c = md5_hh(c, d, a, b, x[i +  3], 16, -722521979);\n            b = md5_hh(b, c, d, a, x[i +  6], 23,  76029189);\n            a = md5_hh(a, b, c, d, x[i +  9],  4, -640364487);\n            d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);\n            c = md5_hh(c, d, a, b, x[i + 15], 16,  530742520);\n            b = md5_hh(b, c, d, a, x[i +  2], 23, -995338651);\n\n            a = md5_ii(a, b, c, d, x[i],       6, -198630844);\n            d = md5_ii(d, a, b, c, x[i +  7], 10,  1126891415);\n            c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);\n            b = md5_ii(b, c, d, a, x[i +  5], 21, -57434055);\n            a = md5_ii(a, b, c, d, x[i + 12],  6,  1700485571);\n            d = md5_ii(d, a, b, c, x[i +  3], 10, -1894986606);\n            c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);\n            b = md5_ii(b, c, d, a, x[i +  1], 21, -2054922799);\n            a = md5_ii(a, b, c, d, x[i +  8],  6,  1873313359);\n            d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);\n            c = md5_ii(c, d, a, b, x[i +  6], 15, -1560198380);\n            b = md5_ii(b, c, d, a, x[i + 13], 21,  1309151649);\n            a = md5_ii(a, b, c, d, x[i +  4],  6, -145523070);\n            d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);\n            c = md5_ii(c, d, a, b, x[i +  2], 15,  718787259);\n            b = md5_ii(b, c, d, a, x[i +  9], 21, -343485551);\n\n            a = safe_add(a, olda);\n            b = safe_add(b, oldb);\n            c = safe_add(c, oldc);\n            d = safe_add(d, oldd);\n        }\n        return [a, b, c, d];\n    }\n\n    /*\n    * Convert an array of little-endian words to a string\n    */\n    function binl2rstr(input) {\n        var i,\n            output = '';\n        for (i = 0; i < input.length * 32; i += 8) {\n            output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);\n        }\n        return output;\n    }\n\n    /*\n    * Convert a raw string to an array of little-endian words\n    * Characters >255 have their high-byte silently ignored.\n    */\n    function rstr2binl(input) {\n        var i,\n            output = [];\n        output[(input.length >> 2) - 1] = undefined;\n        for (i = 0; i < output.length; i += 1) {\n            output[i] = 0;\n        }\n        for (i = 0; i < input.length * 8; i += 8) {\n            output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32);\n        }\n        return output;\n    }\n\n    /*\n    * Calculate the MD5 of a raw string\n    */\n    function rstr_md5(s) {\n        return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));\n    }\n\n    /*\n    * Calculate the HMAC-MD5, of a key and some data (raw strings)\n    */\n    function rstr_hmac_md5(key, data) {\n        var i,\n            bkey = rstr2binl(key),\n            ipad = [],\n            opad = [],\n            hash;\n        ipad[15] = opad[15] = undefined;\n        if (bkey.length > 16) {\n            bkey = binl_md5(bkey, key.length * 8);\n        }\n        for (i = 0; i < 16; i += 1) {\n            ipad[i] = bkey[i] ^ 0x36363636;\n            opad[i] = bkey[i] ^ 0x5C5C5C5C;\n        }\n        hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);\n        return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));\n    }\n\n    /*\n    * Convert a raw string to a hex string\n    */\n    function rstr2hex(input) {\n        var hex_tab = '0123456789abcdef',\n            output = '',\n            x,\n            i;\n        for (i = 0; i < input.length; i += 1) {\n            x = input.charCodeAt(i);\n            output += hex_tab.charAt((x >>> 4) & 0x0F) +\n                hex_tab.charAt(x & 0x0F);\n        }\n        return output;\n    }\n\n    /*\n    * Encode a string as utf-8\n    */\n    function str2rstr_utf8(input) {\n        return unescape(encodeURIComponent(input));\n    }\n\n    /*\n    * Take string arguments and return either raw or hex encoded strings\n    */\n    function raw_md5(s) {\n        return rstr_md5(str2rstr_utf8(s));\n    }\n    function hex_md5(s) {\n        return rstr2hex(raw_md5(s));\n    }\n    function raw_hmac_md5(k, d) {\n        return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d));\n    }\n    function hex_hmac_md5(k, d) {\n        return rstr2hex(raw_hmac_md5(k, d));\n    }\n\n    function md5(string, key, raw) {\n        if (!key) {\n            if (!raw) {\n                return hex_md5(string);\n            }\n            return raw_md5(string);\n        }\n        if (!raw) {\n            return hex_hmac_md5(key, string);\n        }\n        return raw_hmac_md5(key, string);\n    }\n\n    if (typeof define === 'function' && define.amd) {\n        define(function () {\n            return md5;\n        });\n    } else {\n        $.md5 = md5;\n    }\n}(this));\n","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\nfunction EventEmitter() {\n  this._events = this._events || {};\n  this._maxListeners = this._maxListeners || undefined;\n}\nmodule.exports = EventEmitter;\n\n// Backwards-compat with node 0.10.x\nEventEmitter.EventEmitter = EventEmitter;\n\nEventEmitter.prototype._events = undefined;\nEventEmitter.prototype._maxListeners = undefined;\n\n// By default EventEmitters will print a warning if more than 10 listeners are\n// added to it. This is a useful default which helps finding memory leaks.\nEventEmitter.defaultMaxListeners = 10;\n\n// Obviously not all Emitters should be limited to 10. This function allows\n// that to be increased. Set to zero for unlimited.\nEventEmitter.prototype.setMaxListeners = function(n) {\n  if (!isNumber(n) || n < 0 || isNaN(n))\n    throw TypeError('n must be a positive number');\n  this._maxListeners = n;\n  return this;\n};\n\nEventEmitter.prototype.emit = function(type) {\n  var er, handler, len, args, i, listeners;\n\n  if (!this._events)\n    this._events = {};\n\n  // If there is no 'error' event listener then throw.\n  if (type === 'error') {\n    if (!this._events.error ||\n        (isObject(this._events.error) && !this._events.error.length)) {\n      er = arguments[1];\n      if (er instanceof Error) {\n        throw er; // Unhandled 'error' event\n      }\n      throw TypeError('Uncaught, unspecified \"error\" event.');\n    }\n  }\n\n  handler = this._events[type];\n\n  if (isUndefined(handler))\n    return false;\n\n  if (isFunction(handler)) {\n    switch (arguments.length) {\n      // fast cases\n      case 1:\n        handler.call(this);\n        break;\n      case 2:\n        handler.call(this, arguments[1]);\n        break;\n      case 3:\n        handler.call(this, arguments[1], arguments[2]);\n        break;\n      // slower\n      default:\n        len = arguments.length;\n        args = new Array(len - 1);\n        for (i = 1; i < len; i++)\n          args[i - 1] = arguments[i];\n        handler.apply(this, args);\n    }\n  } else if (isObject(handler)) {\n    len = arguments.length;\n    args = new Array(len - 1);\n    for (i = 1; i < len; i++)\n      args[i - 1] = arguments[i];\n\n    listeners = handler.slice();\n    len = listeners.length;\n    for (i = 0; i < len; i++)\n      listeners[i].apply(this, args);\n  }\n\n  return true;\n};\n\nEventEmitter.prototype.addListener = function(type, listener) {\n  var m;\n\n  if (!isFunction(listener))\n    throw TypeError('listener must be a function');\n\n  if (!this._events)\n    this._events = {};\n\n  // To avoid recursion in the case that type === \"newListener\"! Before\n  // adding it to the listeners, first emit \"newListener\".\n  if (this._events.newListener)\n    this.emit('newListener', type,\n              isFunction(listener.listener) ?\n              listener.listener : listener);\n\n  if (!this._events[type])\n    // Optimize the case of one listener. Don't need the extra array object.\n    this._events[type] = listener;\n  else if (isObject(this._events[type]))\n    // If we've already got an array, just append.\n    this._events[type].push(listener);\n  else\n    // Adding the second element, need to change to array.\n    this._events[type] = [this._events[type], listener];\n\n  // Check for listener leak\n  if (isObject(this._events[type]) && !this._events[type].warned) {\n    var m;\n    if (!isUndefined(this._maxListeners)) {\n      m = this._maxListeners;\n    } else {\n      m = EventEmitter.defaultMaxListeners;\n    }\n\n    if (m && m > 0 && this._events[type].length > m) {\n      this._events[type].warned = true;\n      console.error('(node) warning: possible EventEmitter memory ' +\n                    'leak detected. %d listeners added. ' +\n                    'Use emitter.setMaxListeners() to increase limit.',\n                    this._events[type].length);\n      if (typeof console.trace === 'function') {\n        // not supported in IE 10\n        console.trace();\n      }\n    }\n  }\n\n  return this;\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.once = function(type, listener) {\n  if (!isFunction(listener))\n    throw TypeError('listener must be a function');\n\n  var fired = false;\n\n  function g() {\n    this.removeListener(type, g);\n\n    if (!fired) {\n      fired = true;\n      listener.apply(this, arguments);\n    }\n  }\n\n  g.listener = listener;\n  this.on(type, g);\n\n  return this;\n};\n\n// emits a 'removeListener' event iff the listener was removed\nEventEmitter.prototype.removeListener = function(type, listener) {\n  var list, position, length, i;\n\n  if (!isFunction(listener))\n    throw TypeError('listener must be a function');\n\n  if (!this._events || !this._events[type])\n    return this;\n\n  list = this._events[type];\n  length = list.length;\n  position = -1;\n\n  if (list === listener ||\n      (isFunction(list.listener) && list.listener === listener)) {\n    delete this._events[type];\n    if (this._events.removeListener)\n      this.emit('removeListener', type, listener);\n\n  } else if (isObject(list)) {\n    for (i = length; i-- > 0;) {\n      if (list[i] === listener ||\n          (list[i].listener && list[i].listener === listener)) {\n        position = i;\n        break;\n      }\n    }\n\n    if (position < 0)\n      return this;\n\n    if (list.length === 1) {\n      list.length = 0;\n      delete this._events[type];\n    } else {\n      list.splice(position, 1);\n    }\n\n    if (this._events.removeListener)\n      this.emit('removeListener', type, listener);\n  }\n\n  return this;\n};\n\nEventEmitter.prototype.removeAllListeners = function(type) {\n  var key, listeners;\n\n  if (!this._events)\n    return this;\n\n  // not listening for removeListener, no need to emit\n  if (!this._events.removeListener) {\n    if (arguments.length === 0)\n      this._events = {};\n    else if (this._events[type])\n      delete this._events[type];\n    return this;\n  }\n\n  // emit removeListener for all listeners on all events\n  if (arguments.length === 0) {\n    for (key in this._events) {\n      if (key === 'removeListener') continue;\n      this.removeAllListeners(key);\n    }\n    this.removeAllListeners('removeListener');\n    this._events = {};\n    return this;\n  }\n\n  listeners = this._events[type];\n\n  if (isFunction(listeners)) {\n    this.removeListener(type, listeners);\n  } else {\n    // LIFO order\n    while (listeners.length)\n      this.removeListener(type, listeners[listeners.length - 1]);\n  }\n  delete this._events[type];\n\n  return this;\n};\n\nEventEmitter.prototype.listeners = function(type) {\n  var ret;\n  if (!this._events || !this._events[type])\n    ret = [];\n  else if (isFunction(this._events[type]))\n    ret = [this._events[type]];\n  else\n    ret = this._events[type].slice();\n  return ret;\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n  var ret;\n  if (!emitter._events || !emitter._events[type])\n    ret = 0;\n  else if (isFunction(emitter._events[type]))\n    ret = 1;\n  else\n    ret = emitter._events[type].length;\n  return ret;\n};\n\nfunction isFunction(arg) {\n  return typeof arg === 'function';\n}\n\nfunction isNumber(arg) {\n  return typeof arg === 'number';\n}\n\nfunction isObject(arg) {\n  return typeof arg === 'object' && arg !== null;\n}\n\nfunction isUndefined(arg) {\n  return arg === void 0;\n}\n","// shim for using process in browser\n\nvar process = module.exports = {};\nvar queue = [];\nvar draining = false;\nvar currentQueue;\nvar queueIndex = -1;\n\nfunction cleanUpNextTick() {\n    draining = false;\n    if (currentQueue.length) {\n        queue = currentQueue.concat(queue);\n    } else {\n        queueIndex = -1;\n    }\n    if (queue.length) {\n        drainQueue();\n    }\n}\n\nfunction drainQueue() {\n    if (draining) {\n        return;\n    }\n    var timeout = setTimeout(cleanUpNextTick);\n    draining = true;\n\n    var len = queue.length;\n    while(len) {\n        currentQueue = queue;\n        queue = [];\n        while (++queueIndex < len) {\n            currentQueue[queueIndex].run();\n        }\n        queueIndex = -1;\n        len = queue.length;\n    }\n    currentQueue = null;\n    draining = false;\n    clearTimeout(timeout);\n}\n\nprocess.nextTick = function (fun) {\n    var args = new Array(arguments.length - 1);\n    if (arguments.length > 1) {\n        for (var i = 1; i < arguments.length; i++) {\n            args[i - 1] = arguments[i];\n        }\n    }\n    queue.push(new Item(fun, args));\n    if (queue.length === 1 && !draining) {\n        setTimeout(drainQueue, 0);\n    }\n};\n\n// v8 likes predictible objects\nfunction Item(fun, array) {\n    this.fun = fun;\n    this.array = array;\n}\nItem.prototype.run = function () {\n    this.fun.apply(null, this.array);\n};\nprocess.title = 'browser';\nprocess.browser = true;\nprocess.env = {};\nprocess.argv = [];\nprocess.version = ''; // empty string to avoid regexp issues\nprocess.versions = {};\n\nfunction noop() {}\n\nprocess.on = noop;\nprocess.addListener = noop;\nprocess.once = noop;\nprocess.off = noop;\nprocess.removeListener = noop;\nprocess.removeAllListeners = noop;\nprocess.emit = noop;\n\nprocess.binding = function (name) {\n    throw new Error('process.binding is not supported');\n};\n\n// TODO(shtylman)\nprocess.cwd = function () { return '/' };\nprocess.chdir = function (dir) {\n    throw new Error('process.chdir is not supported');\n};\nprocess.umask = function() { return 0; };\n","var nextTick = require('process/browser.js').nextTick;\nvar apply = Function.prototype.apply;\nvar slice = Array.prototype.slice;\nvar immediateIds = {};\nvar nextImmediateId = 0;\n\n// DOM APIs, for completeness\n\nexports.setTimeout = function() {\n  return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout);\n};\nexports.setInterval = function() {\n  return new Timeout(apply.call(setInterval, window, arguments), clearInterval);\n};\nexports.clearTimeout =\nexports.clearInterval = function(timeout) { timeout.close(); };\n\nfunction Timeout(id, clearFn) {\n  this._id = id;\n  this._clearFn = clearFn;\n}\nTimeout.prototype.unref = Timeout.prototype.ref = function() {};\nTimeout.prototype.close = function() {\n  this._clearFn.call(window, this._id);\n};\n\n// Does not start the time, just sets up the members needed.\nexports.enroll = function(item, msecs) {\n  clearTimeout(item._idleTimeoutId);\n  item._idleTimeout = msecs;\n};\n\nexports.unenroll = function(item) {\n  clearTimeout(item._idleTimeoutId);\n  item._idleTimeout = -1;\n};\n\nexports._unrefActive = exports.active = function(item) {\n  clearTimeout(item._idleTimeoutId);\n\n  var msecs = item._idleTimeout;\n  if (msecs >= 0) {\n    item._idleTimeoutId = setTimeout(function onTimeout() {\n      if (item._onTimeout)\n        item._onTimeout();\n    }, msecs);\n  }\n};\n\n// That's not how node.js implements it but the exposed api is the same.\nexports.setImmediate = typeof setImmediate === \"function\" ? setImmediate : function(fn) {\n  var id = nextImmediateId++;\n  var args = arguments.length < 2 ? false : slice.call(arguments, 1);\n\n  immediateIds[id] = true;\n\n  nextTick(function onNextTick() {\n    if (immediateIds[id]) {\n      // fn.call() is faster so we optimize for the common use-case\n      // @see http://jsperf.com/call-apply-segu\n      if (args) {\n        fn.apply(null, args);\n      } else {\n        fn.call(null);\n      }\n      // Prevent ids from leaking\n      exports.clearImmediate(id);\n    }\n  });\n\n  return id;\n};\n\nexports.clearImmediate = typeof clearImmediate === \"function\" ? clearImmediate : function(id) {\n  delete immediateIds[id];\n};","\n/**\n * This is the web browser implementation of `debug()`.\n *\n * Expose `debug()` as the module.\n */\n\nexports = module.exports = require('./debug');\nexports.log = log;\nexports.formatArgs = formatArgs;\nexports.save = save;\nexports.load = load;\nexports.useColors = useColors;\nexports.storage = 'undefined' != typeof chrome\n               && 'undefined' != typeof chrome.storage\n                  ? chrome.storage.local\n                  : localstorage();\n\n/**\n * Colors.\n */\n\nexports.colors = [\n  'lightseagreen',\n  'forestgreen',\n  'goldenrod',\n  'dodgerblue',\n  'darkorchid',\n  'crimson'\n];\n\n/**\n * Currently only WebKit-based Web Inspectors, Firefox >= v31,\n * and the Firebug extension (any Firefox version) are known\n * to support \"%c\" CSS customizations.\n *\n * TODO: add a `localStorage` variable to explicitly enable/disable colors\n */\n\nfunction useColors() {\n  // is webkit? http://stackoverflow.com/a/16459606/376773\n  return ('WebkitAppearance' in document.documentElement.style) ||\n    // is firebug? http://stackoverflow.com/a/398120/376773\n    (window.console && (console.firebug || (console.exception && console.table))) ||\n    // is firefox >= v31?\n    // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages\n    (navigator.userAgent.toLowerCase().match(/firefox\\/(\\d+)/) && parseInt(RegExp.$1, 10) >= 31);\n}\n\n/**\n * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.\n */\n\nexports.formatters.j = function(v) {\n  return JSON.stringify(v);\n};\n\n\n/**\n * Colorize log arguments if enabled.\n *\n * @api public\n */\n\nfunction formatArgs() {\n  var args = arguments;\n  var useColors = this.useColors;\n\n  args[0] = (useColors ? '%c' : '')\n    + this.namespace\n    + (useColors ? ' %c' : ' ')\n    + args[0]\n    + (useColors ? '%c ' : ' ')\n    + '+' + exports.humanize(this.diff);\n\n  if (!useColors) return args;\n\n  var c = 'color: ' + this.color;\n  args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1));\n\n  // the final \"%c\" is somewhat tricky, because there could be other\n  // arguments passed either before or after the %c, so we need to\n  // figure out the correct index to insert the CSS into\n  var index = 0;\n  var lastC = 0;\n  args[0].replace(/%[a-z%]/g, function(match) {\n    if ('%%' === match) return;\n    index++;\n    if ('%c' === match) {\n      // we only are interested in the *last* %c\n      // (the user may have provided their own)\n      lastC = index;\n    }\n  });\n\n  args.splice(lastC, 0, c);\n  return args;\n}\n\n/**\n * Invokes `console.log()` when available.\n * No-op when `console.log` is not a \"function\".\n *\n * @api public\n */\n\nfunction log() {\n  // this hackery is required for IE8/9, where\n  // the `console.log` function doesn't have 'apply'\n  return 'object' === typeof console\n    && console.log\n    && Function.prototype.apply.call(console.log, console, arguments);\n}\n\n/**\n * Save `namespaces`.\n *\n * @param {String} namespaces\n * @api private\n */\n\nfunction save(namespaces) {\n  try {\n    if (null == namespaces) {\n      exports.storage.removeItem('debug');\n    } else {\n      exports.storage.debug = namespaces;\n    }\n  } catch(e) {}\n}\n\n/**\n * Load `namespaces`.\n *\n * @return {String} returns the previously persisted debug modes\n * @api private\n */\n\nfunction load() {\n  var r;\n  try {\n    r = exports.storage.debug;\n  } catch(e) {}\n  return r;\n}\n\n/**\n * Enable namespaces listed in `localStorage.debug` initially.\n */\n\nexports.enable(load());\n\n/**\n * Localstorage attempts to return the localstorage.\n *\n * This is necessary because safari throws\n * when a user disables cookies/localstorage\n * and you attempt to access it.\n *\n * @return {LocalStorage}\n * @api private\n */\n\nfunction localstorage(){\n  try {\n    return window.localStorage;\n  } catch (e) {}\n}\n","\n/**\n * This is the common logic for both the Node.js and web browser\n * implementations of `debug()`.\n *\n * Expose `debug()` as the module.\n */\n\nexports = module.exports = debug;\nexports.coerce = coerce;\nexports.disable = disable;\nexports.enable = enable;\nexports.enabled = enabled;\nexports.humanize = require('ms');\n\n/**\n * The currently active debug mode names, and names to skip.\n */\n\nexports.names = [];\nexports.skips = [];\n\n/**\n * Map of special \"%n\" handling functions, for the debug \"format\" argument.\n *\n * Valid key names are a single, lowercased letter, i.e. \"n\".\n */\n\nexports.formatters = {};\n\n/**\n * Previously assigned color.\n */\n\nvar prevColor = 0;\n\n/**\n * Previous log timestamp.\n */\n\nvar prevTime;\n\n/**\n * Select a color.\n *\n * @return {Number}\n * @api private\n */\n\nfunction selectColor() {\n  return exports.colors[prevColor++ % exports.colors.length];\n}\n\n/**\n * Create a debugger with the given `namespace`.\n *\n * @param {String} namespace\n * @return {Function}\n * @api public\n */\n\nfunction debug(namespace) {\n\n  // define the `disabled` version\n  function disabled() {\n  }\n  disabled.enabled = false;\n\n  // define the `enabled` version\n  function enabled() {\n\n    var self = enabled;\n\n    // set `diff` timestamp\n    var curr = +new Date();\n    var ms = curr - (prevTime || curr);\n    self.diff = ms;\n    self.prev = prevTime;\n    self.curr = curr;\n    prevTime = curr;\n\n    // add the `color` if not set\n    if (null == self.useColors) self.useColors = exports.useColors();\n    if (null == self.color && self.useColors) self.color = selectColor();\n\n    var args = Array.prototype.slice.call(arguments);\n\n    args[0] = exports.coerce(args[0]);\n\n    if ('string' !== typeof args[0]) {\n      // anything else let's inspect with %o\n      args = ['%o'].concat(args);\n    }\n\n    // apply any `formatters` transformations\n    var index = 0;\n    args[0] = args[0].replace(/%([a-z%])/g, function(match, format) {\n      // if we encounter an escaped % then don't increase the array index\n      if (match === '%%') return match;\n      index++;\n      var formatter = exports.formatters[format];\n      if ('function' === typeof formatter) {\n        var val = args[index];\n        match = formatter.call(self, val);\n\n        // now we need to remove `args[index]` since it's inlined in the `format`\n        args.splice(index, 1);\n        index--;\n      }\n      return match;\n    });\n\n    if ('function' === typeof exports.formatArgs) {\n      args = exports.formatArgs.apply(self, args);\n    }\n    var logFn = enabled.log || exports.log || console.log.bind(console);\n    logFn.apply(self, args);\n  }\n  enabled.enabled = true;\n\n  var fn = exports.enabled(namespace) ? enabled : disabled;\n\n  fn.namespace = namespace;\n\n  return fn;\n}\n\n/**\n * Enables a debug mode by namespaces. This can include modes\n * separated by a colon and wildcards.\n *\n * @param {String} namespaces\n * @api public\n */\n\nfunction enable(namespaces) {\n  exports.save(namespaces);\n\n  var split = (namespaces || '').split(/[\\s,]+/);\n  var len = split.length;\n\n  for (var i = 0; i < len; i++) {\n    if (!split[i]) continue; // ignore empty strings\n    namespaces = split[i].replace(/\\*/g, '.*?');\n    if (namespaces[0] === '-') {\n      exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));\n    } else {\n      exports.names.push(new RegExp('^' + namespaces + '$'));\n    }\n  }\n}\n\n/**\n * Disable debug output.\n *\n * @api public\n */\n\nfunction disable() {\n  exports.enable('');\n}\n\n/**\n * Returns true if the given mode name is enabled, false otherwise.\n *\n * @param {String} name\n * @return {Boolean}\n * @api public\n */\n\nfunction enabled(name) {\n  var i, len;\n  for (i = 0, len = exports.skips.length; i < len; i++) {\n    if (exports.skips[i].test(name)) {\n      return false;\n    }\n  }\n  for (i = 0, len = exports.names.length; i < len; i++) {\n    if (exports.names[i].test(name)) {\n      return true;\n    }\n  }\n  return false;\n}\n\n/**\n * Coerce `val`.\n *\n * @param {Mixed} val\n * @return {Mixed}\n * @api private\n */\n\nfunction coerce(val) {\n  if (val instanceof Error) return val.stack || val.message;\n  return val;\n}\n","/**\n * Helpers.\n */\n\nvar s = 1000;\nvar m = s * 60;\nvar h = m * 60;\nvar d = h * 24;\nvar y = d * 365.25;\n\n/**\n * Parse or format the given `val`.\n *\n * Options:\n *\n *  - `long` verbose formatting [false]\n *\n * @param {String|Number} val\n * @param {Object} options\n * @return {String|Number}\n * @api public\n */\n\nmodule.exports = function(val, options){\n  options = options || {};\n  if ('string' == typeof val) return parse(val);\n  return options.long\n    ? long(val)\n    : short(val);\n};\n\n/**\n * Parse the given `str` and return milliseconds.\n *\n * @param {String} str\n * @return {Number}\n * @api private\n */\n\nfunction parse(str) {\n  str = '' + str;\n  if (str.length > 10000) return;\n  var match = /^((?:\\d+)?\\.?\\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(str);\n  if (!match) return;\n  var n = parseFloat(match[1]);\n  var type = (match[2] || 'ms').toLowerCase();\n  switch (type) {\n    case 'years':\n    case 'year':\n    case 'yrs':\n    case 'yr':\n    case 'y':\n      return n * y;\n    case 'days':\n    case 'day':\n    case 'd':\n      return n * d;\n    case 'hours':\n    case 'hour':\n    case 'hrs':\n    case 'hr':\n    case 'h':\n      return n * h;\n    case 'minutes':\n    case 'minute':\n    case 'mins':\n    case 'min':\n    case 'm':\n      return n * m;\n    case 'seconds':\n    case 'second':\n    case 'secs':\n    case 'sec':\n    case 's':\n      return n * s;\n    case 'milliseconds':\n    case 'millisecond':\n    case 'msecs':\n    case 'msec':\n    case 'ms':\n      return n;\n  }\n}\n\n/**\n * Short format for `ms`.\n *\n * @param {Number} ms\n * @return {String}\n * @api private\n */\n\nfunction short(ms) {\n  if (ms >= d) return Math.round(ms / d) + 'd';\n  if (ms >= h) return Math.round(ms / h) + 'h';\n  if (ms >= m) return Math.round(ms / m) + 'm';\n  if (ms >= s) return Math.round(ms / s) + 's';\n  return ms + 'ms';\n}\n\n/**\n * Long format for `ms`.\n *\n * @param {Number} ms\n * @return {String}\n * @api private\n */\n\nfunction long(ms) {\n  return plural(ms, d, 'day')\n    || plural(ms, h, 'hour')\n    || plural(ms, m, 'minute')\n    || plural(ms, s, 'second')\n    || ms + ' ms';\n}\n\n/**\n * Pluralization helper.\n */\n\nfunction plural(ms, n, name) {\n  if (ms < n) return;\n  if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;\n  return Math.ceil(ms / n) + ' ' + name + 's';\n}\n","//     uuid.js\n//\n//     Copyright (c) 2010-2012 Robert Kieffer\n//     MIT License - http://opensource.org/licenses/mit-license.php\n\n(function() {\n  var _global = this;\n\n  // Unique ID creation requires a high quality random # generator.  We feature\n  // detect to determine the best RNG source, normalizing to a function that\n  // returns 128-bits of randomness, since that's what's usually required\n  var _rng;\n\n  // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html\n  //\n  // Moderately fast, high quality\n  if (typeof(_global.require) == 'function') {\n    try {\n      var _rb = _global.require('crypto').randomBytes;\n      _rng = _rb && function() {return _rb(16);};\n    } catch(e) {}\n  }\n\n  if (!_rng && _global.crypto && crypto.getRandomValues) {\n    // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto\n    //\n    // Moderately fast, high quality\n    var _rnds8 = new Uint8Array(16);\n    _rng = function whatwgRNG() {\n      crypto.getRandomValues(_rnds8);\n      return _rnds8;\n    };\n  }\n\n  if (!_rng) {\n    // Math.random()-based (RNG)\n    //\n    // If all else fails, use Math.random().  It's fast, but is of unspecified\n    // quality.\n    var  _rnds = new Array(16);\n    _rng = function() {\n      for (var i = 0, r; i < 16; i++) {\n        if ((i & 0x03) === 0) r = Math.random() * 0x100000000;\n        _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;\n      }\n\n      return _rnds;\n    };\n  }\n\n  // Buffer class to use\n  var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array;\n\n  // Maps for number <-> hex string conversion\n  var _byteToHex = [];\n  var _hexToByte = {};\n  for (var i = 0; i < 256; i++) {\n    _byteToHex[i] = (i + 0x100).toString(16).substr(1);\n    _hexToByte[_byteToHex[i]] = i;\n  }\n\n  // **`parse()` - Parse a UUID into it's component bytes**\n  function parse(s, buf, offset) {\n    var i = (buf && offset) || 0, ii = 0;\n\n    buf = buf || [];\n    s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {\n      if (ii < 16) { // Don't overflow!\n        buf[i + ii++] = _hexToByte[oct];\n      }\n    });\n\n    // Zero out remaining bytes if string was short\n    while (ii < 16) {\n      buf[i + ii++] = 0;\n    }\n\n    return buf;\n  }\n\n  // **`unparse()` - Convert UUID byte array (ala parse()) into a string**\n  function unparse(buf, offset) {\n    var i = offset || 0, bth = _byteToHex;\n    return  bth[buf[i++]] + bth[buf[i++]] +\n            bth[buf[i++]] + bth[buf[i++]] + '-' +\n            bth[buf[i++]] + bth[buf[i++]] + '-' +\n            bth[buf[i++]] + bth[buf[i++]] + '-' +\n            bth[buf[i++]] + bth[buf[i++]] + '-' +\n            bth[buf[i++]] + bth[buf[i++]] +\n            bth[buf[i++]] + bth[buf[i++]] +\n            bth[buf[i++]] + bth[buf[i++]];\n  }\n\n  // **`v1()` - Generate time-based UUID**\n  //\n  // Inspired by https://github.com/LiosK/UUID.js\n  // and http://docs.python.org/library/uuid.html\n\n  // random #'s we need to init node and clockseq\n  var _seedBytes = _rng();\n\n  // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)\n  var _nodeId = [\n    _seedBytes[0] | 0x01,\n    _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]\n  ];\n\n  // Per 4.2.2, randomize (14 bit) clockseq\n  var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;\n\n  // Previous uuid creation time\n  var _lastMSecs = 0, _lastNSecs = 0;\n\n  // See https://github.com/broofa/node-uuid for API details\n  function v1(options, buf, offset) {\n    var i = buf && offset || 0;\n    var b = buf || [];\n\n    options = options || {};\n\n    var clockseq = options.clockseq != null ? options.clockseq : _clockseq;\n\n    // UUID timestamps are 100 nano-second units since the Gregorian epoch,\n    // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so\n    // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'\n    // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.\n    var msecs = options.msecs != null ? options.msecs : new Date().getTime();\n\n    // Per 4.2.1.2, use count of uuid's generated during the current clock\n    // cycle to simulate higher resolution clock\n    var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1;\n\n    // Time since last uuid creation (in msecs)\n    var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;\n\n    // Per 4.2.1.2, Bump clockseq on clock regression\n    if (dt < 0 && options.clockseq == null) {\n      clockseq = clockseq + 1 & 0x3fff;\n    }\n\n    // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new\n    // time interval\n    if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {\n      nsecs = 0;\n    }\n\n    // Per 4.2.1.2 Throw error if too many uuids are requested\n    if (nsecs >= 10000) {\n      throw new Error('uuid.v1(): Can\\'t create more than 10M uuids/sec');\n    }\n\n    _lastMSecs = msecs;\n    _lastNSecs = nsecs;\n    _clockseq = clockseq;\n\n    // Per 4.1.4 - Convert from unix epoch to Gregorian epoch\n    msecs += 12219292800000;\n\n    // `time_low`\n    var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;\n    b[i++] = tl >>> 24 & 0xff;\n    b[i++] = tl >>> 16 & 0xff;\n    b[i++] = tl >>> 8 & 0xff;\n    b[i++] = tl & 0xff;\n\n    // `time_mid`\n    var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;\n    b[i++] = tmh >>> 8 & 0xff;\n    b[i++] = tmh & 0xff;\n\n    // `time_high_and_version`\n    b[i++] = tmh >>> 24 & 0xf | 0x10; // include version\n    b[i++] = tmh >>> 16 & 0xff;\n\n    // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)\n    b[i++] = clockseq >>> 8 | 0x80;\n\n    // `clock_seq_low`\n    b[i++] = clockseq & 0xff;\n\n    // `node`\n    var node = options.node || _nodeId;\n    for (var n = 0; n < 6; n++) {\n      b[i + n] = node[n];\n    }\n\n    return buf ? buf : unparse(b);\n  }\n\n  // **`v4()` - Generate random UUID**\n\n  // See https://github.com/broofa/node-uuid for API details\n  function v4(options, buf, offset) {\n    // Deprecated - 'format' argument, as supported in v1.2\n    var i = buf && offset || 0;\n\n    if (typeof(options) == 'string') {\n      buf = options == 'binary' ? new BufferClass(16) : null;\n      options = null;\n    }\n    options = options || {};\n\n    var rnds = options.random || (options.rng || _rng)();\n\n    // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`\n    rnds[6] = (rnds[6] & 0x0f) | 0x40;\n    rnds[8] = (rnds[8] & 0x3f) | 0x80;\n\n    // Copy bytes to buffer, if provided\n    if (buf) {\n      for (var ii = 0; ii < 16; ii++) {\n        buf[i + ii] = rnds[ii];\n      }\n    }\n\n    return buf || unparse(rnds);\n  }\n\n  // Export public API\n  var uuid = v4;\n  uuid.v1 = v1;\n  uuid.v4 = v4;\n  uuid.parse = parse;\n  uuid.unparse = unparse;\n  uuid.BufferClass = BufferClass;\n\n  if (typeof(module) != 'undefined' && module.exports) {\n    // Publish as node.js module\n    module.exports = uuid;\n  } else  if (typeof define === 'function' && define.amd) {\n    // Publish as AMD module\n    define(function() {return uuid;});\n \n\n  } else {\n    // Publish as global (in browsers)\n    var _previousRoot = _global.uuid;\n\n    // **`noConflict()` - (browser only) to reset global 'uuid' var**\n    uuid.noConflict = function() {\n      _global.uuid = _previousRoot;\n      return uuid;\n    };\n\n    _global.uuid = uuid;\n  }\n}).call(this);\n","'use strict';\n\n// Expose the Adapter function/object.\nmodule.exports = Adapter;\n\n\n// Dependencies\n\nvar browser = require('bowser').browser,\n\tdebug = require('debug')('rtcninja:Adapter'),\n\tdebugerror = require('debug')('rtcninja:ERROR:Adapter'),\n\n\t// Internal vars\n\tgetUserMedia = null,\n\tRTCPeerConnection = null,\n\tRTCSessionDescription = null,\n\tRTCIceCandidate = null,\n\tMediaStreamTrack = null,\n\tgetMediaDevices = null,\n\tattachMediaStream = null,\n\tcanRenegotiate = false,\n\toldSpecRTCOfferOptions = false,\n\tbrowserVersion = Number(browser.version) || 0,\n\tisDesktop = !!(!browser.mobile || !browser.tablet),\n\thasWebRTC = false,\n\tvirtGlobal, virtNavigator;\n\ndebugerror.log = console.warn.bind(console);\n\n// Dirty trick to get this library working in a Node-webkit env with browserified libs\nvirtGlobal = global.window || global;\n// Don't fail in Node\nvirtNavigator = virtGlobal.navigator || {};\n\n\n// Constructor.\n\nfunction Adapter(options) {\n\t// Chrome desktop, Chrome Android, Opera desktop, Opera Android, Android native browser\n\t// or generic Webkit browser.\n\tif (\n\t\t(isDesktop && browser.chrome && browserVersion >= 32) ||\n\t\t(browser.android && browser.chrome && browserVersion >= 39) ||\n\t\t(isDesktop && browser.opera && browserVersion >= 27) ||\n\t\t(browser.android && browser.opera && browserVersion >= 24) ||\n\t\t(browser.android && browser.webkit && !browser.chrome && browserVersion >= 37) ||\n\t\t(virtNavigator.webkitGetUserMedia && virtGlobal.webkitRTCPeerConnection)\n\t) {\n\t\thasWebRTC = true;\n\t\tgetUserMedia = virtNavigator.webkitGetUserMedia.bind(virtNavigator);\n\t\tRTCPeerConnection = virtGlobal.webkitRTCPeerConnection;\n\t\tRTCSessionDescription = virtGlobal.RTCSessionDescription;\n\t\tRTCIceCandidate = virtGlobal.RTCIceCandidate;\n\t\tMediaStreamTrack = virtGlobal.MediaStreamTrack;\n\t\tif (MediaStreamTrack && MediaStreamTrack.getSources) {\n\t\t\tgetMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack);\n\t\t} else if (virtNavigator.getMediaDevices) {\n\t\t\tgetMediaDevices = virtNavigator.getMediaDevices.bind(virtNavigator);\n\t\t}\n\t\tattachMediaStream = function (element, stream) {\n\t\t\telement.src = URL.createObjectURL(stream);\n\t\t\treturn element;\n\t\t};\n\t\tcanRenegotiate = true;\n\t\toldSpecRTCOfferOptions = false;\n\t// Firefox desktop, Firefox Android.\n\t} else if (\n\t\t(isDesktop && browser.firefox && browserVersion >= 22) ||\n\t\t(browser.android && browser.firefox && browserVersion >= 33) ||\n\t\t(virtNavigator.mozGetUserMedia && virtGlobal.mozRTCPeerConnection)\n\t) {\n\t\thasWebRTC = true;\n\t\tgetUserMedia = virtNavigator.mozGetUserMedia.bind(virtNavigator);\n\t\tRTCPeerConnection = virtGlobal.mozRTCPeerConnection;\n\t\tRTCSessionDescription = virtGlobal.mozRTCSessionDescription;\n\t\tRTCIceCandidate = virtGlobal.mozRTCIceCandidate;\n\t\tMediaStreamTrack = virtGlobal.MediaStreamTrack;\n\t\tattachMediaStream = function (element, stream) {\n\t\t\telement.src = URL.createObjectURL(stream);\n\t\t\treturn element;\n\t\t};\n\t\tcanRenegotiate = false;\n\t\toldSpecRTCOfferOptions = false;\n\t\t// WebRTC plugin required. For example IE or Safari with the Temasys plugin.\n\t} else if (\n\t\toptions.plugin &&\n\t\ttypeof options.plugin.isRequired === 'function' &&\n\t\toptions.plugin.isRequired() &&\n\t\ttypeof options.plugin.isInstalled === 'function' &&\n\t\toptions.plugin.isInstalled()\n\t) {\n\t\tvar pluginiface = options.plugin.interface;\n\n\t\thasWebRTC = true;\n\t\tgetUserMedia = pluginiface.getUserMedia;\n\t\tRTCPeerConnection = pluginiface.RTCPeerConnection;\n\t\tRTCSessionDescription = pluginiface.RTCSessionDescription;\n\t\tRTCIceCandidate = pluginiface.RTCIceCandidate;\n\t\tMediaStreamTrack = pluginiface.MediaStreamTrack;\n\t\tif (MediaStreamTrack && MediaStreamTrack.getSources) {\n\t\t\tgetMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack);\n\t\t} else if (virtNavigator.getMediaDevices) {\n\t\t\tgetMediaDevices = virtNavigator.getMediaDevices.bind(virtNavigator);\n\t\t}\n\t\tattachMediaStream = pluginiface.attachMediaStream;\n\t\tcanRenegotiate = pluginiface.canRenegotiate;\n\t\toldSpecRTCOfferOptions = true;  // TODO: Update when fixed in the plugin.\n\t// Best effort (may be adater.js is loaded).\n\t} else if (virtNavigator.getUserMedia && virtGlobal.RTCPeerConnection) {\n\t\thasWebRTC = true;\n\t\tgetUserMedia = virtNavigator.getUserMedia.bind(virtNavigator);\n\t\tRTCPeerConnection = virtGlobal.RTCPeerConnection;\n\t\tRTCSessionDescription = virtGlobal.RTCSessionDescription;\n\t\tRTCIceCandidate = virtGlobal.RTCIceCandidate;\n\t\tMediaStreamTrack = virtGlobal.MediaStreamTrack;\n\t\tif (MediaStreamTrack && MediaStreamTrack.getSources) {\n\t\t\tgetMediaDevices = MediaStreamTrack.getSources.bind(MediaStreamTrack);\n\t\t} else if (virtNavigator.getMediaDevices) {\n\t\t\tgetMediaDevices = virtNavigator.getMediaDevices.bind(virtNavigator);\n\t\t}\n\t\tattachMediaStream = virtGlobal.attachMediaStream || function (element, stream) {\n\t\t\telement.src = URL.createObjectURL(stream);\n\t\t\treturn element;\n\t\t};\n\t\tcanRenegotiate = false;\n\t\toldSpecRTCOfferOptions = false;\n\t}\n\n\n\tfunction throwNonSupported(item) {\n\t\treturn function () {\n\t\t\tthrow new Error('rtcninja: WebRTC not supported, missing ' + item +\n\t\t\t' [browser: ' + browser.name + ' ' + browser.version + ']');\n\t\t};\n\t}\n\n\n\t// Public API.\n\n\t// Expose a WebRTC checker.\n\tAdapter.hasWebRTC = function () {\n\t\treturn hasWebRTC;\n\t};\n\n\t// Expose getUserMedia.\n\tif (getUserMedia) {\n\t\tAdapter.getUserMedia = function (constraints, successCallback, errorCallback) {\n\t\t\tdebug('getUserMedia() | constraints: %o', constraints);\n\n\t\t\ttry {\n\t\t\t\tgetUserMedia(constraints,\n\t\t\t\t\tfunction (stream) {\n\t\t\t\t\t\tdebug('getUserMedia() | success');\n\t\t\t\t\t\tif (successCallback) {\n\t\t\t\t\t\t\tsuccessCallback(stream);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tfunction (error) {\n\t\t\t\t\t\tdebug('getUserMedia() | error:', error);\n\t\t\t\t\t\tif (errorCallback) {\n\t\t\t\t\t\t\terrorCallback(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t\tcatch (error) {\n\t\t\t\tdebugerror('getUserMedia() | error:', error);\n\t\t\t\tif (errorCallback) {\n\t\t\t\t\terrorCallback(error);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t} else {\n\t\tAdapter.getUserMedia = function (constraints, successCallback, errorCallback) {\n\t\t\tdebugerror('getUserMedia() | WebRTC not supported');\n\t\t\tif (errorCallback) {\n\t\t\t\terrorCallback(new Error('rtcninja: WebRTC not supported, missing ' +\n\t\t\t\t'getUserMedia [browser: ' + browser.name + ' ' + browser.version + ']'));\n\t\t\t} else {\n\t\t\t\tthrowNonSupported('getUserMedia');\n\t\t\t}\n\t\t};\n\t}\n\n\t// Expose RTCPeerConnection.\n\tAdapter.RTCPeerConnection = RTCPeerConnection || throwNonSupported('RTCPeerConnection');\n\n\t// Expose RTCSessionDescription.\n\tAdapter.RTCSessionDescription = RTCSessionDescription || throwNonSupported('RTCSessionDescription');\n\n\t// Expose RTCIceCandidate.\n\tAdapter.RTCIceCandidate = RTCIceCandidate || throwNonSupported('RTCIceCandidate');\n\n\t// Expose MediaStreamTrack.\n\tAdapter.MediaStreamTrack = MediaStreamTrack || throwNonSupported('MediaStreamTrack');\n\n\t// Expose getMediaDevices.\n\tAdapter.getMediaDevices = getMediaDevices;\n\n\t// Expose MediaStreamTrack.\n\tAdapter.attachMediaStream = attachMediaStream || throwNonSupported('attachMediaStream');\n\n\t// Expose canRenegotiate attribute.\n\tAdapter.canRenegotiate = canRenegotiate;\n\n\t// Expose closeMediaStream.\n\tAdapter.closeMediaStream = function (stream) {\n\t\tif (!stream) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Latest spec states that MediaStream has no stop() method and instead must\n\t\t// call stop() on every MediaStreamTrack.\n\t\tif (MediaStreamTrack && MediaStreamTrack.prototype && MediaStreamTrack.prototype.stop) {\n\t\t\tdebug('closeMediaStream() | calling stop() on all the MediaStreamTrack');\n\n\t\t\tvar tracks, i, len;\n\n\t\t\tif (stream.getTracks) {\n\t\t\t\ttracks = stream.getTracks();\n\t\t\t\tfor (i = 0, len = tracks.length; i < len; i += 1) {\n\t\t\t\t\ttracks[i].stop();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttracks = stream.getAudioTracks();\n\t\t\t\tfor (i = 0, len = tracks.length; i < len; i += 1) {\n\t\t\t\t\ttracks[i].stop();\n\t\t\t\t}\n\n\t\t\t\ttracks = stream.getVideoTracks();\n\t\t\t\tfor (i = 0, len = tracks.length; i < len; i += 1) {\n\t\t\t\t\ttracks[i].stop();\n\t\t\t\t}\n\t\t\t}\n\t\t// Deprecated by the spec, but still in use.\n\t\t} else if (typeof stream.stop === 'function') {\n\t\t\tdebug('closeMediaStream() | calling stop() on the MediaStream');\n\n\t\t\tstream.stop();\n\t\t}\n\t};\n\n\t// Expose fixPeerConnectionConfig.\n\tAdapter.fixPeerConnectionConfig = function (pcConfig) {\n\t\tvar i, len, iceServer, hasUrls, hasUrl;\n\n\t\tif (!Array.isArray(pcConfig.iceServers)) {\n\t\t\tpcConfig.iceServers = [];\n\t\t}\n\n\t\tfor (i = 0, len = pcConfig.iceServers.length; i < len; i += 1) {\n\t\t\ticeServer = pcConfig.iceServers[i];\n\t\t\thasUrls = iceServer.hasOwnProperty('urls');\n\t\t\thasUrl = iceServer.hasOwnProperty('url');\n\n\t\t\tif (typeof iceServer === 'object') {\n\t\t\t\t// Has .urls but not .url, so add .url with a single string value.\n\t\t\t\tif (hasUrls && !hasUrl) {\n\t\t\t\t\ticeServer.url = (Array.isArray(iceServer.urls) ? iceServer.urls[0] : iceServer.urls);\n\t\t\t\t// Has .url but not .urls, so add .urls with same value.\n\t\t\t\t} else if (!hasUrls && hasUrl) {\n\t\t\t\t\ticeServer.urls = (Array.isArray(iceServer.url) ? iceServer.url.slice() : iceServer.url);\n\t\t\t\t}\n\n\t\t\t\t// Ensure .url is a single string.\n\t\t\t\tif (hasUrl && Array.isArray(iceServer.url)) {\n\t\t\t\t\ticeServer.url = iceServer.url[0];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t// Expose fixRTCOfferOptions.\n\tAdapter.fixRTCOfferOptions = function (options) {\n\t\toptions = options || {};\n\n\t\t// New spec.\n\t\tif (!oldSpecRTCOfferOptions) {\n\t\t\tif (options.mandatory && options.mandatory.OfferToReceiveAudio) {\n\t\t\t\toptions.offerToReceiveAudio = 1;\n\t\t\t}\n\t\t\tif (options.mandatory && options.mandatory.OfferToReceiveVideo) {\n\t\t\t\toptions.offerToReceiveVideo = 1;\n\t\t\t}\n\t\t\tdelete options.mandatory;\n\t\t// Old spec.\n\t\t} else {\n\t\t\tif (options.offerToReceiveAudio) {\n\t\t\t\toptions.mandatory = options.mandatory || {};\n\t\t\t\toptions.mandatory.OfferToReceiveAudio = true;\n\t\t\t}\n\t\t\tif (options.offerToReceiveVideo) {\n\t\t\t\toptions.mandatory = options.mandatory || {};\n\t\t\t\toptions.mandatory.OfferToReceiveVideo = true;\n\t\t\t}\n\t\t}\n\t};\n\n\treturn Adapter;\n}\n","'use strict';\n\n// Expose the RTCPeerConnection class.\nmodule.exports = RTCPeerConnection;\n\n\n// Dependencies.\n\nvar merge = require('merge'),\n\tdebug = require('debug')('rtcninja:RTCPeerConnection'),\n\tdebugerror = require('debug')('rtcninja:ERROR:RTCPeerConnection'),\n\tAdapter = require('./Adapter'),\n\n\t// Internal constants.\n\tC = {\n\t\tREGEXP_NORMALIZED_CANDIDATE: new RegExp(/^candidate:/i),\n\t\tREGEXP_FIX_CANDIDATE: new RegExp(/(^a=|\\r|\\n)/gi),\n\t\tREGEXP_RELAY_CANDIDATE: new RegExp(/ relay /i),\n\t\tREGEXP_SDP_CANDIDATES: new RegExp(/^a=candidate:.*\\r\\n/igm),\n\t\tREGEXP_SDP_NON_RELAY_CANDIDATES: new RegExp(/^a=candidate:(.(?!relay ))*\\r\\n/igm)\n\t},\n\n\t// Internal variables.\n\tVAR = {\n\t\tnormalizeCandidate: null\n\t};\n\ndebugerror.log = console.warn.bind(console);\n\n\n// Constructor\n\nfunction RTCPeerConnection(pcConfig, pcConstraints) {\n\tdebug('new | pcConfig: %o', pcConfig);\n\n\t// Set this.pcConfig and this.options.\n\tsetConfigurationAndOptions.call(this, pcConfig);\n\n\t// NOTE: Deprecated pcConstraints argument.\n\tthis.pcConstraints = pcConstraints;\n\n\t// Own version of the localDescription.\n\tthis.ourLocalDescription = null;\n\n\t// Latest values of PC attributes to avoid events with same value.\n\tthis.ourSignalingState = null;\n\tthis.ourIceConnectionState = null;\n\tthis.ourIceGatheringState = null;\n\n\t// Timer for options.gatheringTimeout.\n\tthis.timerGatheringTimeout = null;\n\n\t// Timer for options.gatheringTimeoutAfterRelay.\n\tthis.timerGatheringTimeoutAfterRelay = null;\n\n\t// Flag to ignore new gathered ICE candidates.\n\tthis.ignoreIceGathering = false;\n\n\t// Flag set when closed.\n\tthis.closed = false;\n\n\t// Set RTCPeerConnection.\n\tsetPeerConnection.call(this);\n\n\t// Set properties.\n\tsetProperties.call(this);\n}\n\n\n// Public API.\n\nRTCPeerConnection.prototype.createOffer = function (successCallback, failureCallback, options) {\n\tdebug('createOffer()');\n\n\tvar self = this;\n\n\tAdapter.fixRTCOfferOptions(options);\n\n\tthis.pc.createOffer(\n\t\tfunction (offer) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebug('createOffer() | success');\n\t\t\tif (successCallback) {\n\t\t\t\tsuccessCallback(offer);\n\t\t\t}\n\t\t},\n\t\tfunction (error) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebugerror('createOffer() | error:', error);\n\t\t\tif (failureCallback) {\n\t\t\t\tfailureCallback(error);\n\t\t\t}\n\t\t},\n\t\toptions\n\t);\n};\n\n\nRTCPeerConnection.prototype.createAnswer = function (successCallback, failureCallback, options) {\n\tdebug('createAnswer()');\n\n\tvar self = this;\n\n\tthis.pc.createAnswer(\n\t\tfunction (answer) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebug('createAnswer() | success');\n\t\t\tif (successCallback) {\n\t\t\t\tsuccessCallback(answer);\n\t\t\t}\n\t\t},\n\t\tfunction (error) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebugerror('createAnswer() | error:', error);\n\t\t\tif (failureCallback) {\n\t\t\t\tfailureCallback(error);\n\t\t\t}\n\t\t},\n\t\toptions\n\t);\n};\n\n\nRTCPeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {\n\tdebug('setLocalDescription()');\n\n\tvar self = this;\n\n\tthis.pc.setLocalDescription(\n\t\tdescription,\n\t\t// success.\n\t\tfunction () {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebug('setLocalDescription() | success');\n\n\t\t\t// Clear gathering timers.\n\t\t\tclearTimeout(self.timerGatheringTimeout);\n\t\t\tdelete self.timerGatheringTimeout;\n\t\t\tclearTimeout(self.timerGatheringTimeoutAfterRelay);\n\t\t\tdelete self.timerGatheringTimeoutAfterRelay;\n\n\t\t\trunTimerGatheringTimeout();\n\t\t\tif (successCallback) {\n\t\t\t\tsuccessCallback();\n\t\t\t}\n\t\t},\n\t\t// failure\n\t\tfunction (error) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebugerror('setLocalDescription() | error:', error);\n\t\t\tif (failureCallback) {\n\t\t\t\tfailureCallback(error);\n\t\t\t}\n\t\t}\n\t);\n\n\t// Enable (again) ICE gathering.\n\tthis.ignoreIceGathering = false;\n\n\t// Handle gatheringTimeout.\n\tfunction runTimerGatheringTimeout() {\n\t\tif (typeof self.options.gatheringTimeout !== 'number') {\n\t\t\treturn;\n\t\t}\n\t\t// If setLocalDescription was already called, it may happen that\n\t\t// ICE gathering is not needed, so don't run this timer.\n\t\tif (self.pc.iceGatheringState === 'complete') {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('setLocalDescription() | ending gathering in %d ms (gatheringTimeout option)',\n\t\t\tself.options.gatheringTimeout);\n\n\t\tself.timerGatheringTimeout = setTimeout(function () {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tdebug('forced end of candidates after gatheringTimeout timeout');\n\n\t\t\t// Clear gathering timers.\n\t\t\tdelete self.timerGatheringTimeout;\n\t\t\tclearTimeout(self.timerGatheringTimeoutAfterRelay);\n\t\t\tdelete self.timerGatheringTimeoutAfterRelay;\n\n\t\t\t// Ignore new candidates.\n\t\t\tself.ignoreIceGathering = true;\n\t\t\tif (self.onicecandidate) {\n\t\t\t\tself.onicecandidate({ candidate: null }, null);\n\t\t\t}\n\n\t\t}, self.options.gatheringTimeout);\n\t}\n};\n\n\nRTCPeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {\n\tdebug('setRemoteDescription()');\n\n\tvar self = this;\n\n\tthis.pc.setRemoteDescription(\n\t\tdescription,\n\t\tfunction () {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebug('setRemoteDescription() | success');\n\t\t\tif (successCallback) {\n\t\t\t\tsuccessCallback();\n\t\t\t}\n\t\t},\n\t\tfunction (error) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebugerror('setRemoteDescription() | error:', error);\n\t\t\tif (failureCallback) {\n\t\t\t\tfailureCallback(error);\n\t\t\t}\n\t\t}\n\t);\n};\n\n\nRTCPeerConnection.prototype.updateIce = function (pcConfig) {\n\tdebug('updateIce() | pcConfig: %o', pcConfig);\n\n\t// Update this.pcConfig and this.options.\n\tsetConfigurationAndOptions.call(this, pcConfig);\n\n\tthis.pc.updateIce(this.pcConfig);\n\n\t// Enable (again) ICE gathering.\n\tthis.ignoreIceGathering = false;\n};\n\n\nRTCPeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {\n\tdebug('addIceCandidate() | candidate: %o', candidate);\n\n\tvar self = this;\n\n\tthis.pc.addIceCandidate(\n\t\tcandidate,\n\t\tfunction () {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebug('addIceCandidate() | success');\n\t\t\tif (successCallback) {\n\t\t\t\tsuccessCallback();\n\t\t\t}\n\t\t},\n\t\tfunction (error) {\n\t\t\tif (isClosed.call(self)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdebugerror('addIceCandidate() | error:', error);\n\t\t\tif (failureCallback) {\n\t\t\t\tfailureCallback(error);\n\t\t\t}\n\t\t}\n\t);\n};\n\n\nRTCPeerConnection.prototype.getConfiguration = function () {\n\tdebug('getConfiguration()');\n\n\treturn this.pc.getConfiguration();\n};\n\n\nRTCPeerConnection.prototype.getLocalStreams = function () {\n\tdebug('getLocalStreams()');\n\n\treturn this.pc.getLocalStreams();\n};\n\n\nRTCPeerConnection.prototype.getRemoteStreams = function () {\n\tdebug('getRemoteStreams()');\n\n\treturn this.pc.getRemoteStreams();\n};\n\n\nRTCPeerConnection.prototype.getStreamById = function (streamId) {\n\tdebug('getStreamById() | streamId: %s', streamId);\n\n\treturn this.pc.getStreamById(streamId);\n};\n\n\nRTCPeerConnection.prototype.addStream = function (stream) {\n\tdebug('addStream() | stream: %s', stream);\n\n\tthis.pc.addStream(stream);\n};\n\n\nRTCPeerConnection.prototype.removeStream = function (stream) {\n\tdebug('removeStream() | stream: %o', stream);\n\n\tthis.pc.removeStream(stream);\n};\n\n\nRTCPeerConnection.prototype.close = function () {\n\tdebug('close()');\n\n\tthis.closed = true;\n\n\t// Clear gathering timers.\n\tclearTimeout(this.timerGatheringTimeout);\n\tdelete this.timerGatheringTimeout;\n\tclearTimeout(this.timerGatheringTimeoutAfterRelay);\n\tdelete this.timerGatheringTimeoutAfterRelay;\n\n\tthis.pc.close();\n};\n\n\nRTCPeerConnection.prototype.createDataChannel = function () {\n\tdebug('createDataChannel()');\n\n\treturn this.pc.createDataChannel.apply(this.pc, arguments);\n};\n\n\nRTCPeerConnection.prototype.createDTMFSender = function (track) {\n\tdebug('createDTMFSender()');\n\n\treturn this.pc.createDTMFSender(track);\n};\n\n\nRTCPeerConnection.prototype.getStats = function () {\n\tdebug('getStats()');\n\n\treturn this.pc.getStats.apply(this.pc, arguments);\n};\n\n\nRTCPeerConnection.prototype.setIdentityProvider = function () {\n\tdebug('setIdentityProvider()');\n\n\treturn this.pc.setIdentityProvider.apply(this.pc, arguments);\n};\n\n\nRTCPeerConnection.prototype.getIdentityAssertion = function () {\n\tdebug('getIdentityAssertion()');\n\n\treturn this.pc.getIdentityAssertion();\n};\n\n\nRTCPeerConnection.prototype.reset = function (pcConfig) {\n\tdebug('reset() | pcConfig: %o', pcConfig);\n\n\tvar pc = this.pc;\n\n\t// Remove events in the old PC.\n\tpc.onnegotiationneeded = null;\n\tpc.onicecandidate = null;\n\tpc.onaddstream = null;\n\tpc.onremovestream = null;\n\tpc.ondatachannel = null;\n\tpc.onsignalingstatechange = null;\n\tpc.oniceconnectionstatechange = null;\n\tpc.onicegatheringstatechange = null;\n\tpc.onidentityresult = null;\n\tpc.onpeeridentity = null;\n\tpc.onidpassertionerror = null;\n\tpc.onidpvalidationerror = null;\n\n\t// Clear gathering timers.\n\tclearTimeout(this.timerGatheringTimeout);\n\tdelete this.timerGatheringTimeout;\n\tclearTimeout(this.timerGatheringTimeoutAfterRelay);\n\tdelete this.timerGatheringTimeoutAfterRelay;\n\n\t// Silently close the old PC.\n\tdebug('reset() | closing current peerConnection');\n\tpc.close();\n\n\t// Set this.pcConfig and this.options.\n\tsetConfigurationAndOptions.call(this, pcConfig);\n\n\t// Create a new PC.\n\tsetPeerConnection.call(this);\n};\n\n\n// Private Helpers.\n\nfunction setConfigurationAndOptions(pcConfig) {\n\t// Clone pcConfig.\n\tthis.pcConfig = merge(true, pcConfig);\n\n\t// Fix pcConfig.\n\tAdapter.fixPeerConnectionConfig(this.pcConfig);\n\n\tthis.options = {\n\t\ticeTransportsRelay: (this.pcConfig.iceTransports === 'relay'),\n\t\ticeTransportsNone: (this.pcConfig.iceTransports === 'none'),\n\t\tgatheringTimeout: this.pcConfig.gatheringTimeout,\n\t\tgatheringTimeoutAfterRelay: this.pcConfig.gatheringTimeoutAfterRelay\n\t};\n\n\t// Remove custom rtcninja.RTCPeerConnection options from pcConfig.\n\tdelete this.pcConfig.gatheringTimeout;\n\tdelete this.pcConfig.gatheringTimeoutAfterRelay;\n\n\tdebug('setConfigurationAndOptions | processed pcConfig: %o', this.pcConfig);\n}\n\n\nfunction isClosed() {\n\treturn ((this.closed) || (this.pc && this.pc.iceConnectionState === 'closed'));\n}\n\n\nfunction setEvents() {\n\tvar self = this,\n\t\tpc = this.pc;\n\n\tpc.onnegotiationneeded = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onnegotiationneeded()');\n\t\tif (self.onnegotiationneeded) {\n\t\t\tself.onnegotiationneeded(event);\n\t\t}\n\t};\n\n\tpc.onicecandidate = function (event) {\n\t\tvar candidate, isRelay, newCandidate;\n\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\t\tif (self.ignoreIceGathering) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Ignore any candidate (event the null one) if iceTransports:'none' is set.\n\t\tif (self.options.iceTransportsNone) {\n\t\t\treturn;\n\t\t}\n\n\t\tcandidate = event.candidate;\n\n\t\tif (candidate) {\n\t\t\tisRelay = C.REGEXP_RELAY_CANDIDATE.test(candidate.candidate);\n\n\t\t\t// Ignore if just relay candidates are requested.\n\t\t\tif (self.options.iceTransportsRelay && !isRelay) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Handle gatheringTimeoutAfterRelay.\n\t\t\tif (isRelay && !self.timerGatheringTimeoutAfterRelay &&\n\t\t\t\t(typeof self.options.gatheringTimeoutAfterRelay === 'number')) {\n\t\t\t\tdebug('onicecandidate() | first relay candidate found, ending gathering in %d ms', self.options.gatheringTimeoutAfterRelay);\n\n\t\t\t\tself.timerGatheringTimeoutAfterRelay = setTimeout(function () {\n\t\t\t\t\tif (isClosed.call(self)) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tdebug('forced end of candidates after timeout');\n\n\t\t\t\t\t// Clear gathering timers.\n\t\t\t\t\tdelete self.timerGatheringTimeoutAfterRelay;\n\t\t\t\t\tclearTimeout(self.timerGatheringTimeout);\n\t\t\t\t\tdelete self.timerGatheringTimeout;\n\n\t\t\t\t\t// Ignore new candidates.\n\t\t\t\t\tself.ignoreIceGathering = true;\n\t\t\t\t\tif (self.onicecandidate) {\n\t\t\t\t\t\tself.onicecandidate({candidate: null}, null);\n\t\t\t\t\t}\n\t\t\t\t}, self.options.gatheringTimeoutAfterRelay);\n\t\t\t}\n\n\t\t\tnewCandidate = new Adapter.RTCIceCandidate({\n\t\t\t\tsdpMid: candidate.sdpMid,\n\t\t\t\tsdpMLineIndex: candidate.sdpMLineIndex,\n\t\t\t\tcandidate: candidate.candidate\n\t\t\t});\n\n\t\t\t// Force correct candidate syntax (just check it once).\n\t\t\tif (VAR.normalizeCandidate === null) {\n\t\t\t\tif (C.REGEXP_NORMALIZED_CANDIDATE.test(candidate.candidate)) {\n\t\t\t\t\tVAR.normalizeCandidate = false;\n\t\t\t\t} else {\n\t\t\t\t\tdebug('onicecandidate() | normalizing ICE candidates syntax (remove \"a=\" and \"\\\\r\\\\n\")');\n\t\t\t\t\tVAR.normalizeCandidate = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (VAR.normalizeCandidate) {\n\t\t\t\tnewCandidate.candidate = candidate.candidate.replace(C.REGEXP_FIX_CANDIDATE, '');\n\t\t\t}\n\n\t\t\tdebug(\n\t\t\t\t'onicecandidate() | m%d(%s) %s',\n\t\t\t\tnewCandidate.sdpMLineIndex,\n\t\t\t\tnewCandidate.sdpMid || 'no mid', newCandidate.candidate);\n\t\t\tif (self.onicecandidate) {\n\t\t\t\tself.onicecandidate(event, newCandidate);\n\t\t\t}\n\t\t// Null candidate (end of candidates).\n\t\t} else {\n\t\t\tdebug('onicecandidate() | end of candidates');\n\n\t\t\t// Clear gathering timers.\n\t\t\tclearTimeout(self.timerGatheringTimeout);\n\t\t\tdelete self.timerGatheringTimeout;\n\t\t\tclearTimeout(self.timerGatheringTimeoutAfterRelay);\n\t\t\tdelete self.timerGatheringTimeoutAfterRelay;\n\t\t\tif (self.onicecandidate) {\n\t\t\t\tself.onicecandidate(event, null);\n\t\t\t}\n\t\t}\n\t};\n\n\tpc.onaddstream = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onaddstream() | stream: %o', event.stream);\n\t\tif (self.onaddstream) {\n\t\t\tself.onaddstream(event, event.stream);\n\t\t}\n\t};\n\n\tpc.onremovestream = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onremovestream() | stream: %o', event.stream);\n\t\tif (self.onremovestream) {\n\t\t\tself.onremovestream(event, event.stream);\n\t\t}\n\t};\n\n\tpc.ondatachannel = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('ondatachannel() | datachannel: %o', event.channel);\n\t\tif (self.ondatachannel) {\n\t\t\tself.ondatachannel(event, event.channel);\n\t\t}\n\t};\n\n\tpc.onsignalingstatechange = function (event) {\n\t\tif (pc.signalingState === self.ourSignalingState) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onsignalingstatechange() | signalingState: %s', pc.signalingState);\n\t\tself.ourSignalingState = pc.signalingState;\n\t\tif (self.onsignalingstatechange) {\n\t\t\tself.onsignalingstatechange(event, pc.signalingState);\n\t\t}\n\t};\n\n\tpc.oniceconnectionstatechange = function (event) {\n\t\tif (pc.iceConnectionState === self.ourIceConnectionState) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('oniceconnectionstatechange() | iceConnectionState: %s', pc.iceConnectionState);\n\t\tself.ourIceConnectionState = pc.iceConnectionState;\n\t\tif (self.oniceconnectionstatechange) {\n\t\t\tself.oniceconnectionstatechange(event, pc.iceConnectionState);\n\t\t}\n\t};\n\n\tpc.onicegatheringstatechange = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (pc.iceGatheringState === self.ourIceGatheringState) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onicegatheringstatechange() | iceGatheringState: %s', pc.iceGatheringState);\n\t\tself.ourIceGatheringState = pc.iceGatheringState;\n\t\tif (self.onicegatheringstatechange) {\n\t\t\tself.onicegatheringstatechange(event, pc.iceGatheringState);\n\t\t}\n\t};\n\n\tpc.onidentityresult = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onidentityresult()');\n\t\tif (self.onidentityresult) {\n\t\t\tself.onidentityresult(event);\n\t\t}\n\t};\n\n\tpc.onpeeridentity = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onpeeridentity()');\n\t\tif (self.onpeeridentity) {\n\t\t\tself.onpeeridentity(event);\n\t\t}\n\t};\n\n\tpc.onidpassertionerror = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onidpassertionerror()');\n\t\tif (self.onidpassertionerror) {\n\t\t\tself.onidpassertionerror(event);\n\t\t}\n\t};\n\n\tpc.onidpvalidationerror = function (event) {\n\t\tif (isClosed.call(self)) {\n\t\t\treturn;\n\t\t}\n\n\t\tdebug('onidpvalidationerror()');\n\t\tif (self.onidpvalidationerror) {\n\t\t\tself.onidpvalidationerror(event);\n\t\t}\n\t};\n}\n\n\nfunction setPeerConnection() {\n\t// Create a RTCPeerConnection.\n\tif (!this.pcConstraints) {\n\t\tthis.pc = new Adapter.RTCPeerConnection(this.pcConfig);\n\t} else {\n\t\t// NOTE: Deprecated.\n\t\tthis.pc = new Adapter.RTCPeerConnection(this.pcConfig, this.pcConstraints);\n\t}\n\n\t// Set RTC events.\n\tsetEvents.call(this);\n}\n\n\nfunction getLocalDescription() {\n\tvar pc = this.pc,\n\t\toptions = this.options,\n\t\tsdp = null;\n\n\tif (!pc.localDescription) {\n\t\tthis.ourLocalDescription = null;\n\t\treturn null;\n\t}\n\n\t// Mangle the SDP string.\n\tif (options.iceTransportsRelay) {\n\t\tsdp = pc.localDescription.sdp.replace(C.REGEXP_SDP_NON_RELAY_CANDIDATES, '');\n\t} else if (options.iceTransportsNone) {\n\t\tsdp = pc.localDescription.sdp.replace(C.REGEXP_SDP_CANDIDATES, '');\n\t}\n\n\tthis.ourLocalDescription = new Adapter.RTCSessionDescription({\n\t\ttype: pc.localDescription.type,\n\t\tsdp: sdp || pc.localDescription.sdp\n\t});\n\n\treturn this.ourLocalDescription;\n}\n\n\nfunction setProperties() {\n\tvar self = this;\n\n\tObject.defineProperties(this, {\n\t\tpeerConnection: {\n\t\t\tget: function () {\n\t\t\t\treturn self.pc;\n\t\t\t}\n\t\t},\n\n\t\tsignalingState: {\n\t\t\tget: function () {\n\t\t\t\treturn self.pc.signalingState;\n\t\t\t}\n\t\t},\n\n\t\ticeConnectionState: {\n\t\t\tget: function () {\n\t\t\t\treturn self.pc.iceConnectionState;\n\t\t\t}\n\t\t},\n\n\t\ticeGatheringState: {\n\t\t\tget: function () {\n\t\t\t\treturn self.pc.iceGatheringState;\n\t\t\t}\n\t\t},\n\n\t\tlocalDescription: {\n\t\t\tget: function () {\n\t\t\t\treturn getLocalDescription.call(self);\n\t\t\t}\n\t\t},\n\n\t\tremoteDescription: {\n\t\t\tget: function () {\n\t\t\t\treturn self.pc.remoteDescription;\n\t\t\t}\n\t\t},\n\n\t\tpeerIdentity: {\n\t\t\tget: function () {\n\t\t\t\treturn self.pc.peerIdentity;\n\t\t\t}\n\t\t}\n\t});\n}\n","'use strict';\n\nmodule.exports = rtcninja;\n\n\n// Dependencies.\n\nvar browser = require('bowser').browser,\n\tdebug = require('debug')('rtcninja'),\n\tdebugerror = require('debug')('rtcninja:ERROR'),\n\tversion = require('./version'),\n\tAdapter = require('./Adapter'),\n\tRTCPeerConnection = require('./RTCPeerConnection'),\n\n\t// Internal vars.\n\tcalled = false;\n\ndebugerror.log = console.warn.bind(console);\ndebug('version %s', version);\ndebug('detected browser: %s %s [mobile:%s, tablet:%s, android:%s, ios:%s]',\n\t\tbrowser.name, browser.version, !!browser.mobile, !!browser.tablet,\n\t\t!!browser.android, !!browser.ios);\n\n\n// Constructor.\n\nfunction rtcninja(options) {\n\t// Load adapter\n\tvar iface = Adapter(options || {});  // jshint ignore:line\n\n\tcalled = true;\n\n\t// Expose RTCPeerConnection class.\n\trtcninja.RTCPeerConnection = RTCPeerConnection;\n\n\t// Expose WebRTC API and utils.\n\trtcninja.getUserMedia = iface.getUserMedia;\n\trtcninja.RTCSessionDescription = iface.RTCSessionDescription;\n\trtcninja.RTCIceCandidate = iface.RTCIceCandidate;\n\trtcninja.MediaStreamTrack = iface.MediaStreamTrack;\n\trtcninja.getMediaDevices = iface.getMediaDevices;\n\trtcninja.attachMediaStream = iface.attachMediaStream;\n\trtcninja.closeMediaStream = iface.closeMediaStream;\n\trtcninja.canRenegotiate = iface.canRenegotiate;\n\n\t// Log WebRTC support.\n\tif (iface.hasWebRTC()) {\n\t\tdebug('WebRTC supported');\n\t\treturn true;\n\t} else {\n\t\tdebugerror('WebRTC not supported');\n\t\treturn false;\n\t}\n}\n\n\n// Public API.\n\n// If called without calling rtcninja(), call it.\nrtcninja.hasWebRTC = function () {\n\tif (!called) {\n\t\trtcninja();\n\t}\n\n\treturn Adapter.hasWebRTC();\n};\n\n\n// Expose version property.\nObject.defineProperty(rtcninja, 'version', {\n\tget: function () {\n\t\treturn version;\n\t}\n});\n\n\n// Expose called property.\nObject.defineProperty(rtcninja, 'called', {\n\tget: function () {\n\t\treturn called;\n\t}\n});\n\n\n// Exposing stuff.\n\nrtcninja.debug = require('debug');\nrtcninja.browser = browser;\n","'use strict';\n\n// Expose the 'version' field of package.json.\nmodule.exports = require('../package.json').version;\n\n","/*!\n  * Bowser - a browser detector\n  * https://github.com/ded/bowser\n  * MIT License | (c) Dustin Diaz 2014\n  */\n\n!function (name, definition) {\n  if (typeof module != 'undefined' && module.exports) module.exports['browser'] = definition()\n  else if (typeof define == 'function' && define.amd) define(definition)\n  else this[name] = definition()\n}('bowser', function () {\n  /**\n    * See useragents.js for examples of navigator.userAgent\n    */\n\n  var t = true\n\n  function detect(ua) {\n\n    function getFirstMatch(regex) {\n      var match = ua.match(regex);\n      return (match && match.length > 1 && match[1]) || '';\n    }\n\n    function getSecondMatch(regex) {\n      var match = ua.match(regex);\n      return (match && match.length > 1 && match[2]) || '';\n    }\n\n    var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()\n      , likeAndroid = /like android/i.test(ua)\n      , android = !likeAndroid && /android/i.test(ua)\n      , edgeVersion = getFirstMatch(/edge\\/(\\d+(\\.\\d+)?)/i)\n      , versionIdentifier = getFirstMatch(/version\\/(\\d+(\\.\\d+)?)/i)\n      , tablet = /tablet/i.test(ua)\n      , mobile = !tablet && /[^-]mobi/i.test(ua)\n      , result\n\n    if (/opera|opr/i.test(ua)) {\n      result = {\n        name: 'Opera'\n      , opera: t\n      , version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\\s\\/](\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/windows phone/i.test(ua)) {\n      result = {\n        name: 'Windows Phone'\n      , windowsphone: t\n      }\n      if (edgeVersion) {\n        result.msedge = t\n        result.version = edgeVersion\n      }\n      else {\n        result.msie = t\n        result.version = getFirstMatch(/iemobile\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/msie|trident/i.test(ua)) {\n      result = {\n        name: 'Internet Explorer'\n      , msie: t\n      , version: getFirstMatch(/(?:msie |rv:)(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/chrome.+? edge/i.test(ua)) {\n      result = {\n        name: 'Microsoft Edge'\n      , msedge: t\n      , version: edgeVersion\n      }\n    }\n    else if (/chrome|crios|crmo/i.test(ua)) {\n      result = {\n        name: 'Chrome'\n      , chrome: t\n      , version: getFirstMatch(/(?:chrome|crios|crmo)\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (iosdevice) {\n      result = {\n        name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'\n      }\n      // WTF: version is not part of user agent in web apps\n      if (versionIdentifier) {\n        result.version = versionIdentifier\n      }\n    }\n    else if (/sailfish/i.test(ua)) {\n      result = {\n        name: 'Sailfish'\n      , sailfish: t\n      , version: getFirstMatch(/sailfish\\s?browser\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/seamonkey\\//i.test(ua)) {\n      result = {\n        name: 'SeaMonkey'\n      , seamonkey: t\n      , version: getFirstMatch(/seamonkey\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/firefox|iceweasel/i.test(ua)) {\n      result = {\n        name: 'Firefox'\n      , firefox: t\n      , version: getFirstMatch(/(?:firefox|iceweasel)[ \\/](\\d+(\\.\\d+)?)/i)\n      }\n      if (/\\((mobile|tablet);[^\\)]*rv:[\\d\\.]+\\)/i.test(ua)) {\n        result.firefoxos = t\n      }\n    }\n    else if (/silk/i.test(ua)) {\n      result =  {\n        name: 'Amazon Silk'\n      , silk: t\n      , version : getFirstMatch(/silk\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (android) {\n      result = {\n        name: 'Android'\n      , version: versionIdentifier\n      }\n    }\n    else if (/phantom/i.test(ua)) {\n      result = {\n        name: 'PhantomJS'\n      , phantom: t\n      , version: getFirstMatch(/phantomjs\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/blackberry|\\bbb\\d+/i.test(ua) || /rim\\stablet/i.test(ua)) {\n      result = {\n        name: 'BlackBerry'\n      , blackberry: t\n      , version: versionIdentifier || getFirstMatch(/blackberry[\\d]+\\/(\\d+(\\.\\d+)?)/i)\n      }\n    }\n    else if (/(web|hpw)os/i.test(ua)) {\n      result = {\n        name: 'WebOS'\n      , webos: t\n      , version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\\/(\\d+(\\.\\d+)?)/i)\n      };\n      /touchpad\\//i.test(ua) && (result.touchpad = t)\n    }\n    else if (/bada/i.test(ua)) {\n      result = {\n        name: 'Bada'\n      , bada: t\n      , version: getFirstMatch(/dolfin\\/(\\d+(\\.\\d+)?)/i)\n      };\n    }\n    else if (/tizen/i.test(ua)) {\n      result = {\n        name: 'Tizen'\n      , tizen: t\n      , version: getFirstMatch(/(?:tizen\\s?)?browser\\/(\\d+(\\.\\d+)?)/i) || versionIdentifier\n      };\n    }\n    else if (/safari/i.test(ua)) {\n      result = {\n        name: 'Safari'\n      , safari: t\n      , version: versionIdentifier\n      }\n    }\n    else {\n      result = {\n        name: getFirstMatch(/^(.*)\\/(.*) /),\n        version: getSecondMatch(/^(.*)\\/(.*) /)\n     };\n   }\n\n    // set webkit or gecko flag for browsers based on these engines\n    if (!result.msedge && /(apple)?webkit/i.test(ua)) {\n      result.name = result.name || \"Webkit\"\n      result.webkit = t\n      if (!result.version && versionIdentifier) {\n        result.version = versionIdentifier\n      }\n    } else if (!result.opera && /gecko\\//i.test(ua)) {\n      result.name = result.name || \"Gecko\"\n      result.gecko = t\n      result.version = result.version || getFirstMatch(/gecko\\/(\\d+(\\.\\d+)?)/i)\n    }\n\n    // set OS flags for platforms that have multiple browsers\n    if (!result.msedge && (android || result.silk)) {\n      result.android = t\n    } else if (iosdevice) {\n      result[iosdevice] = t\n      result.ios = t\n    }\n\n    // OS version extraction\n    var osVersion = '';\n    if (result.windowsphone) {\n      osVersion = getFirstMatch(/windows phone (?:os)?\\s?(\\d+(\\.\\d+)*)/i);\n    } else if (iosdevice) {\n      osVersion = getFirstMatch(/os (\\d+([_\\s]\\d+)*) like mac os x/i);\n      osVersion = osVersion.replace(/[_\\s]/g, '.');\n    } else if (android) {\n      osVersion = getFirstMatch(/android[ \\/-](\\d+(\\.\\d+)*)/i);\n    } else if (result.webos) {\n      osVersion = getFirstMatch(/(?:web|hpw)os\\/(\\d+(\\.\\d+)*)/i);\n    } else if (result.blackberry) {\n      osVersion = getFirstMatch(/rim\\stablet\\sos\\s(\\d+(\\.\\d+)*)/i);\n    } else if (result.bada) {\n      osVersion = getFirstMatch(/bada\\/(\\d+(\\.\\d+)*)/i);\n    } else if (result.tizen) {\n      osVersion = getFirstMatch(/tizen[\\/\\s](\\d+(\\.\\d+)*)/i);\n    }\n    if (osVersion) {\n      result.osversion = osVersion;\n    }\n\n    // device type extraction\n    var osMajorVersion = osVersion.split('.')[0];\n    if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {\n      result.tablet = t\n    } else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {\n      result.mobile = t\n    }\n\n    // Graded Browser Support\n    // http://developer.yahoo.com/yui/articles/gbs\n    if (result.msedge ||\n        (result.msie && result.version >= 10) ||\n        (result.chrome && result.version >= 20) ||\n        (result.firefox && result.version >= 20.0) ||\n        (result.safari && result.version >= 6) ||\n        (result.opera && result.version >= 10.0) ||\n        (result.ios && result.osversion && result.osversion.split(\".\")[0] >= 6) ||\n        (result.blackberry && result.version >= 10.1)\n        ) {\n      result.a = t;\n    }\n    else if ((result.msie && result.version < 10) ||\n        (result.chrome && result.version < 20) ||\n        (result.firefox && result.version < 20.0) ||\n        (result.safari && result.version < 6) ||\n        (result.opera && result.version < 10.0) ||\n        (result.ios && result.osversion && result.osversion.split(\".\")[0] < 6)\n        ) {\n      result.c = t\n    } else result.x = t\n\n    return result\n  }\n\n  var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')\n\n  bowser.test = function (browserList) {\n    for (var i = 0; i < browserList.length; ++i) {\n      var browserItem = browserList[i];\n      if (typeof browserItem=== 'string') {\n        if (browserItem in bowser) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  /*\n   * Set our detect method to the main bowser object so we can\n   * reuse it to test other user agents.\n   * This is needed to implement future tests.\n   */\n  bowser._detect = detect;\n\n  return bowser\n});\n","/*!\r\n * @name JavaScript/NodeJS Merge v1.2.0\r\n * @author yeikos\r\n * @repository https://github.com/yeikos/js.merge\r\n\r\n * Copyright 2014 yeikos - MIT license\r\n * https://raw.github.com/yeikos/js.merge/master/LICENSE\r\n */\r\n\r\n;(function(isNode) {\r\n\r\n\t/**\r\n\t * Merge one or more objects \r\n\t * @param bool? clone\r\n\t * @param mixed,... arguments\r\n\t * @return object\r\n\t */\r\n\r\n\tvar Public = function(clone) {\r\n\r\n\t\treturn merge(clone === true, false, arguments);\r\n\r\n\t}, publicName = 'merge';\r\n\r\n\t/**\r\n\t * Merge two or more objects recursively \r\n\t * @param bool? clone\r\n\t * @param mixed,... arguments\r\n\t * @return object\r\n\t */\r\n\r\n\tPublic.recursive = function(clone) {\r\n\r\n\t\treturn merge(clone === true, true, arguments);\r\n\r\n\t};\r\n\r\n\t/**\r\n\t * Clone the input removing any reference\r\n\t * @param mixed input\r\n\t * @return mixed\r\n\t */\r\n\r\n\tPublic.clone = function(input) {\r\n\r\n\t\tvar output = input,\r\n\t\t\ttype = typeOf(input),\r\n\t\t\tindex, size;\r\n\r\n\t\tif (type === 'array') {\r\n\r\n\t\t\toutput = [];\r\n\t\t\tsize = input.length;\r\n\r\n\t\t\tfor (index=0;index<size;++index)\r\n\r\n\t\t\t\toutput[index] = Public.clone(input[index]);\r\n\r\n\t\t} else if (type === 'object') {\r\n\r\n\t\t\toutput = {};\r\n\r\n\t\t\tfor (index in input)\r\n\r\n\t\t\t\toutput[index] = Public.clone(input[index]);\r\n\r\n\t\t}\r\n\r\n\t\treturn output;\r\n\r\n\t};\r\n\r\n\t/**\r\n\t * Merge two objects recursively\r\n\t * @param mixed input\r\n\t * @param mixed extend\r\n\t * @return mixed\r\n\t */\r\n\r\n\tfunction merge_recursive(base, extend) {\r\n\r\n\t\tif (typeOf(base) !== 'object')\r\n\r\n\t\t\treturn extend;\r\n\r\n\t\tfor (var key in extend) {\r\n\r\n\t\t\tif (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {\r\n\r\n\t\t\t\tbase[key] = merge_recursive(base[key], extend[key]);\r\n\r\n\t\t\t} else {\r\n\r\n\t\t\t\tbase[key] = extend[key];\r\n\r\n\t\t\t}\r\n\r\n\t\t}\r\n\r\n\t\treturn base;\r\n\r\n\t}\r\n\r\n\t/**\r\n\t * Merge two or more objects\r\n\t * @param bool clone\r\n\t * @param bool recursive\r\n\t * @param array argv\r\n\t * @return object\r\n\t */\r\n\r\n\tfunction merge(clone, recursive, argv) {\r\n\r\n\t\tvar result = argv[0],\r\n\t\t\tsize = argv.length;\r\n\r\n\t\tif (clone || typeOf(result) !== 'object')\r\n\r\n\t\t\tresult = {};\r\n\r\n\t\tfor (var index=0;index<size;++index) {\r\n\r\n\t\t\tvar item = argv[index],\r\n\r\n\t\t\t\ttype = typeOf(item);\r\n\r\n\t\t\tif (type !== 'object') continue;\r\n\r\n\t\t\tfor (var key in item) {\r\n\r\n\t\t\t\tvar sitem = clone ? Public.clone(item[key]) : item[key];\r\n\r\n\t\t\t\tif (recursive) {\r\n\r\n\t\t\t\t\tresult[key] = merge_recursive(result[key], sitem);\r\n\r\n\t\t\t\t} else {\r\n\r\n\t\t\t\t\tresult[key] = sitem;\r\n\r\n\t\t\t\t}\r\n\r\n\t\t\t}\r\n\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\r\n\t}\r\n\r\n\t/**\r\n\t * Get type of variable\r\n\t * @param mixed input\r\n\t * @return string\r\n\t *\r\n\t * @see http://jsperf.com/typeofvar\r\n\t */\r\n\r\n\tfunction typeOf(input) {\r\n\r\n\t\treturn ({}).toString.call(input).slice(8, -1).toLowerCase();\r\n\r\n\t}\r\n\r\n\tif (isNode) {\r\n\r\n\t\tmodule.exports = Public;\r\n\r\n\t} else {\r\n\r\n\t\twindow[publicName] = Public;\r\n\r\n\t}\r\n\r\n})(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports);","module.exports={\n  \"name\": \"rtcninja\",\n  \"version\": \"0.6.2\",\n  \"description\": \"WebRTC API wrapper to deal with different browsers\",\n  \"author\": {\n    \"name\": \"Iñaki Baz Castillo\",\n    \"email\": \"inaki.baz@eface2face.com\",\n    \"url\": \"http://eface2face.com\"\n  },\n  \"contributors\": [\n    {\n      \"name\": \"Jesús Pérez\",\n      \"email\": \"jesus.perez@eface2face.com\"\n    }\n  ],\n  \"license\": \"MIT\",\n  \"main\": \"lib/rtcninja.js\",\n  \"homepage\": \"https://github.com/eface2face/rtcninja.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/eface2face/rtcninja.js.git\"\n  },\n  \"keywords\": [\n    \"webrtc\"\n  ],\n  \"engines\": {\n    \"node\": \">=0.10.32\"\n  },\n  \"dependencies\": {\n    \"bowser\": \"^0.7.3\",\n    \"debug\": \"^2.2.0\",\n    \"merge\": \"^1.2.0\"\n  },\n  \"devDependencies\": {\n    \"browserify\": \"^10.2.3\",\n    \"gulp\": \"git+https://github.com/gulpjs/gulp.git#4.0\",\n    \"gulp-expect-file\": \"0.0.7\",\n    \"gulp-filelog\": \"^0.4.1\",\n    \"gulp-header\": \"^1.2.2\",\n    \"gulp-jscs\": \"^1.6.0\",\n    \"gulp-jscs-stylish\": \"^1.1.0\",\n    \"gulp-jshint\": \"^1.11.0\",\n    \"gulp-rename\": \"^1.2.2\",\n    \"gulp-uglify\": \"^1.2.0\",\n    \"jshint-stylish\": \"^1.0.2\",\n    \"retire\": \"^1.1.0\",\n    \"shelljs\": \"^0.5.0\",\n    \"vinyl-source-stream\": \"^1.1.0\"\n  },\n  \"gitHead\": \"9ddf6664289d9ab9da786edcd2f8b61b0633f013\",\n  \"bugs\": {\n    \"url\": \"https://github.com/eface2face/rtcninja.js/issues\"\n  },\n  \"_id\": \"rtcninja@0.6.2\",\n  \"scripts\": {},\n  \"_shasum\": \"ac274f4184c64d2d98c1da2cca914a2725dfcf09\",\n  \"_from\": \"rtcninja@>=0.6.2 <0.7.0\",\n  \"_npmVersion\": \"2.5.1\",\n  \"_nodeVersion\": \"0.12.0\",\n  \"_npmUser\": {\n    \"name\": \"ibc\",\n    \"email\": \"ibc@aliax.net\"\n  },\n  \"dist\": {\n    \"shasum\": \"ac274f4184c64d2d98c1da2cca914a2725dfcf09\",\n    \"tarball\": \"http://registry.npmjs.org/rtcninja/-/rtcninja-0.6.2.tgz\"\n  },\n  \"maintainers\": [\n    {\n      \"name\": \"ibc\",\n      \"email\": \"ibc@aliax.net\"\n    }\n  ],\n  \"directories\": {},\n  \"_resolved\": \"https://registry.npmjs.org/rtcninja/-/rtcninja-0.6.2.tgz\",\n  \"readme\": \"ERROR: No README data found!\"\n}\n","var _global = (function() { return this; })();\nvar nativeWebSocket = _global.WebSocket || _global.MozWebSocket;\n\n\n/**\n * Expose a W3C WebSocket class with just one or two arguments.\n */\nfunction W3CWebSocket(uri, protocols) {\n\tvar native_instance;\n\n\tif (protocols) {\n\t\tnative_instance = new nativeWebSocket(uri, protocols);\n\t}\n\telse {\n\t\tnative_instance = new nativeWebSocket(uri);\n\t}\n\n\t/**\n\t * 'native_instance' is an instance of nativeWebSocket (the browser's WebSocket\n\t * class). Since it is an Object it will be returned as it is when creating an\n\t * instance of W3CWebSocket via 'new W3CWebSocket()'.\n\t *\n\t * ECMAScript 5: http://bclary.com/2004/11/07/#a-13.2.2\n\t */\n\treturn native_instance;\n}\n\n\n/**\n * Module exports.\n */\nmodule.exports = {\n    'w3cwebsocket' : nativeWebSocket ? W3CWebSocket : null,\n    'version'      : require('./version')\n};\n","module.exports = require('../package.json').version;\n","module.exports={\n  \"name\": \"websocket\",\n  \"description\": \"Websocket Client & Server Library implementing the WebSocket protocol as specified in RFC 6455.\",\n  \"keywords\": [\n    \"websocket\",\n    \"websockets\",\n    \"socket\",\n    \"networking\",\n    \"comet\",\n    \"push\",\n    \"RFC-6455\",\n    \"realtime\",\n    \"server\",\n    \"client\"\n  ],\n  \"author\": {\n    \"name\": \"Brian McKelvey\",\n    \"email\": \"brian@worlize.com\",\n    \"url\": \"https://www.worlize.com/\"\n  },\n  \"version\": \"1.0.19\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/theturtle32/WebSocket-Node.git\"\n  },\n  \"homepage\": \"https://github.com/theturtle32/WebSocket-Node\",\n  \"engines\": {\n    \"node\": \">=0.8.0\"\n  },\n  \"dependencies\": {\n    \"debug\": \"~2.1.0\",\n    \"nan\": \"1.8.x\",\n    \"typedarray-to-buffer\": \"~3.0.0\"\n  },\n  \"devDependencies\": {\n    \"buffer-equal\": \"0.0.1\",\n    \"faucet\": \"0.0.1\",\n    \"gulp\": \"git+https://github.com/gulpjs/gulp.git#4.0\",\n    \"gulp-jshint\": \"^1.9.0\",\n    \"jshint-stylish\": \"^1.0.0\",\n    \"tape\": \"^3.0.0\"\n  },\n  \"config\": {\n    \"verbose\": false\n  },\n  \"scripts\": {\n    \"install\": \"(node-gyp rebuild 2> builderror.log) || (exit 0)\",\n    \"test\": \"faucet test/unit\",\n    \"gulp\": \"gulp\"\n  },\n  \"main\": \"index\",\n  \"directories\": {\n    \"lib\": \"./lib\"\n  },\n  \"browser\": \"lib/browser.js\",\n  \"license\": \"Apache-2.0\",\n  \"gitHead\": \"da3bd5b04e9442c84881b2e9c13432cdbbae1f16\",\n  \"bugs\": {\n    \"url\": \"https://github.com/theturtle32/WebSocket-Node/issues\"\n  },\n  \"_id\": \"websocket@1.0.19\",\n  \"_shasum\": \"e62dbf1a3c5e0767425db7187cfa38f921dfb42c\",\n  \"_from\": \"websocket@>=1.0.19 <2.0.0\",\n  \"_npmVersion\": \"2.10.1\",\n  \"_nodeVersion\": \"0.12.4\",\n  \"_npmUser\": {\n    \"name\": \"theturtle32\",\n    \"email\": \"brian@worlize.com\"\n  },\n  \"maintainers\": [\n    {\n      \"name\": \"theturtle32\",\n      \"email\": \"brian@worlize.com\"\n    }\n  ],\n  \"dist\": {\n    \"shasum\": \"e62dbf1a3c5e0767425db7187cfa38f921dfb42c\",\n    \"tarball\": \"http://registry.npmjs.org/websocket/-/websocket-1.0.19.tgz\"\n  },\n  \"_resolved\": \"https://registry.npmjs.org/websocket/-/websocket-1.0.19.tgz\",\n  \"readme\": \"ERROR: No README data found!\"\n}\n"]}
\ No newline at end of file
diff --git a/resources/html/webrtcgateway/test/components/accountbox.jsx b/resources/html/webrtcgateway/test/components/accountbox.jsx
new file mode 100644
index 0000000..3686025
--- /dev/null
+++ b/resources/html/webrtcgateway/test/components/accountbox.jsx
@@ -0,0 +1,107 @@
+
+var AccountBox = React.createClass({
+    getInitialState: function() {
+        return {accountId: '',
+                password: '',
+                account: null,
+                registrationState: null,
+                addInProgress: false,
+                registerInProgress: false,
+        };
+    },
+
+    componentWillReceiveProps: function(nextProps) {
+        if (this.props.connection !== null && nextProps.connection === null) {
+            // we have been disconnected!
+            this.replaceState(this.getInitialState());
+        }
+    },
+
+    handleAccountIdChange: function(event) {
+        this.setState({accountId: event.target.value});
+    },
+
+    handlePasswordChange: function(event) {
+        this.setState({password: event.target.value});
+    },
+
+    registrationStateChanged: function(oldState, newState, data) {
+        this.setState({registrationState: newState});
+        switch (newState) {
+            case 'failed':
+            case 'registered':
+            case 'unregistered':
+            case null:
+                this.setState({registerInProgress: false});
+                break;
+            default:
+                break;
+        }
+    },
+
+    toggleAdd: function(event) {
+        event.preventDefault();
+        var conn = this.props.connection;
+        var self = this;
+        this.setState({addInProgress: true});
+        if (this.state.account === null) {
+            var options = {account: this.state.accountId, password: this.state.password};
+            conn.addAccount(options, function(error, account) {
+                if (!error) {
+                    account.on('registrationStateChanged', self.registrationStateChanged);
+                } else {
+                    console.log('Error adding account: ' + error);
+                }
+                self.setState({account: account, addInProgress: false});
+                self.props.setAccount(account);
+            });
+        } else {
+            conn.removeAccount(this.state.account, function(error) {
+                if (error) {
+                    console.log('Error removing account: ' + error);
+                }
+                self.replaceState(self.getInitialState());
+                self.props.setAccount(null);
+            });
+        }
+    },
+
+    toggleRegister: function(event) {
+        event.preventDefault();
+        this.setState({registerInProgress: true});
+        if (this.state.registrationState !== null) {
+            this.state.account.unregister();
+        } else {
+            this.state.account.register();
+        }
+    },
+
+    render: function() {
+        var connectionReady = this.props.connection !== null && this.props.connection.state === 'ready';
+        var accountReady = this.state.account !== null;
+        return (
+            <div className="account-box" style={{overflow: 'hidden'}}>
+                <form role="form">
+                    <div className="input-group">
+                        <span className="input-group-addon" id="account-input">Account</span>
+                        <input type="text" aria-describedby="account-input" className="form-control" disabled={!connectionReady || accountReady} value={this.state.accountId} placeholder="Enter SIP URI: user@domain" onChange={this.handleAccountIdChange}/>
+                    </div>
+                    <br/>
+                    <div className="input-group">
+                        <span className="input-group-addon" id="account-password-input">Password</span>
+                        <input type="password" aria-describedby="account-password-input" className="form-control" disabled={!connectionReady || accountReady} value={this.state.password} onChange={this.handlePasswordChange}/>
+                    </div>
+                    <br/>
+                    <div className="btn-group pull-right">
+                        <button type="submit" className="btn btn-primary" disabled={this.state.accountId.length === 0 || this.state.password.length === 0 || this.state.addInProgress} onClick={this.toggleAdd}>
+                            {this.state.account !== null ? "Remove" : "Add"}
+                        </button>
+                        <button type="button" className="btn btn-default" disabled={!accountReady || this.state.registerInProgress} onClick={this.toggleRegister}>
+                            {this.state.registrationState !== null ? "Unregister" : "Register"}
+                        </button>
+                    </div>
+                </form>
+            </div>
+        );
+    },
+});
diff --git a/resources/html/webrtcgateway/test/components/app.jsx b/resources/html/webrtcgateway/test/components/app.jsx
new file mode 100644
index 0000000..7e1a059
--- /dev/null
+++ b/resources/html/webrtcgateway/test/components/app.jsx
@@ -0,0 +1,162 @@
+
+var SylkTest = React.createClass({
+    getInitialState: function() {
+        return {
+            connection: null,
+            connectionState: null,
+            account: null,
+            registrationState: null,
+            currentCall: null,
+            callState: null,
+        };
+    },
+
+    getServerUrl: function() {
+        return window.location.protocol.replace('http', 'ws') + '//' + window.location.host + '/webrtcgateway/ws';
+    },
+
+    connectionStateChanged: function(oldState, newState) {
+        switch (newState) {
+            case 'closed':
+                this.setState({connection: null, connectionState: newState});
+                break;
+            case 'disconnected':
+                if (this.state.currentCall) {
+                    this.state.currentCall.terminate();
+                }
+                this.setState({account: null, registrationState: null, currentCall: null, callState: null, connectionState: newState});
+                break;
+            default:
+                this.setState({connectionState: newState});
+                break;
+        }
+    },
+
+    registrationStateChanged: function(oldState, newState, data) {
+        var state = newState;
+        if (newState === 'failed') {
+            state += ' (' + data.reason + ')'
+        }
+        this.setState({registrationState: state});
+    },
+
+    callStateChanged: function(oldState, newState, data) {
+        var state = newState;
+        var currentCall = this.state.currentCall;
+        if (newState === 'terminated') {
+            state += ' (' + data.reason + ')';
+            currentCall = null;
+            setTimeout(() => {
+                if (this.state.currentCall === null) {
+                    this.setState({callState: null});
+                }
+            }, 4000);
+        }
+        this.setState({currentCall: currentCall, callState: state});
+    },
+
+    setAccount: function(account) {
+        if (account !== null) {
+            account.on('registrationStateChanged', this.registrationStateChanged);
+            account.on('incomingCall', this.handleNewCall);
+            account.on('outgoingCall', this.handleNewCall);
+        }
+        this.setState({account: account});
+    },
+
+    handleNewCall: function(call) {
+        if (this.state.currentCall !== null) {
+            call.terminate();
+        } else {
+            call.on('stateChanged', this.callStateChanged);
+            this.setState({currentCall: call, callState: call.state});
+        }
+    },
+
+    toggleConnect: function(event) {
+        event.preventDefault();
+        if (this.state.connection === null) {
+            var connection = sylkrtc.createConnection({server: this.getServerUrl()});
+            connection.on('stateChanged', this.connectionStateChanged);
+            this.setState({connection: connection});
+        } else {
+            this.state.connection.close();
+        }
+    },
+
+    render: function() {
+        var accountBox;
+        var callBox;
+        var videoBox;
+        if (sylkrtc.isWebRTCSupported()) {
+            accountBox = <AccountBox connection={this.state.connectionState === 'ready' ? this.state.connection : null} setAccount={this.setAccount}/>;
+            if (this.state.callState === 'established') {
+                videoBox = <VideoBox call={this.state.currentCall}/>
+            } else {
+                if (this.state.currentCall === null || this.state.currentCall.direction === 'outgoing') {
+                    callBox = <OutgoingCall account={this.state.account}/>
+                } else {
+                    callBox = <IncomingCall call={this.state.currentCall}/>
+                }
+            }
+        } else {
+            accountBox = <div>
+                            <h2>Your browser does not support WebRTC</h2>
+                            <p>
+                                Too bad! Checkout the WebRTC support across browsers in the link below.
+                                <br/>
+                                <a href="http://iswebrtcreadyyet.com/">Is WebRTC ready yet?</a>
+                            </p>
+                        </div>;
+        }
+        return (
+            <div>
+                <nav className="navbar navbar-inverse">
+                    <div className="container">
+                        <div className="navbar-header">
+                            <a href="#" className="navbar-brand">SylkServer WebRTC Test App</a>
+                        </div>
+                        <button type="button" className="btn btn-primary navbar-btn pull-right" onClick={this.toggleConnect}>
+                            {this.state.connection ? "Disconnect" : "Connect"}
+                        </button>
+                    </div>
+                </nav>
+                <div className="container">
+                    <br/>
+                    <div className="panel panel-default">
+                        <div className="panel-body">
+                            {accountBox}
+                            <hr/>
+                            {videoBox}
+                            {callBox}
+                            <br/>
+                            <br/>
+                        </div>
+                        <div className="panel-footer">
+                            WebSocket server &#8212; {this.getServerUrl()}
+                        </div>
+                    </div>
+                </div>
+                <nav className="navbar navbar-inverse navbar-fixed-bottom">
+                    <div className="container">
+                        <p className="navbar-text">
+                            <span className="label label-default">Connection</span>
+                            &nbsp;
+                            <span className="label label-info">{this.state.connectionState || "disconnected"}</span>
+                        </p>
+                        <p className="navbar-text">
+                            <span className="label label-default">Registration</span>
+                            &nbsp;
+                            <span className="label label-info"> {this.state.registrationState || "unregistered"}</span>
+                        </p>
+                        <p className="navbar-text">
+                            <span className="label label-default">Call status</span>
+                            &nbsp;
+                            <span className="label label-info">{this.state.callState || "idle"}</span>
+                        </p>
+                    </div>
+                </nav>
+            </div>
+        );
+    }
+});
diff --git a/resources/html/webrtcgateway/test/components/incomingcall.jsx b/resources/html/webrtcgateway/test/components/incomingcall.jsx
new file mode 100644
index 0000000..5a60620
--- /dev/null
+++ b/resources/html/webrtcgateway/test/components/incomingcall.jsx
@@ -0,0 +1,33 @@
+
+var IncomingCall = React.createClass({
+    answerClicked: function(event) {
+        event.preventDefault();
+        var callOptions = {pcConfig: {iceServers: [{urls: 'stun:stun.l.google.com:19302'}]}};
+        this.props.call.answer(callOptions);
+    },
+
+    hangupClicked: function(event) {
+        event.preventDefault();
+        this.props.call.terminate();
+    },
+
+    render: function() {
+        return (
+            <div className="call-box" style={{overflow: 'hidden'}}>
+                <form role="form">
+                    <div className="input-group">
+                        <input type="text" id="target-input" className="form-control" disabled value={"Incoming call from " + this.props.call.remoteIdentity}/>
+                        <span className="input-group-btn">
+                            <button type="submit" className="btn btn-success" onClick={this.answerClicked}>
+                                Answer
+                            </button>
+                            <button type="submit" className="btn btn-danger" onClick={this.hangupClicked}>
+                                Hangup
+                            </button>
+                        </span>
+                    </div>
+                </form>
+            </div>
+        );
+    },
+});
diff --git a/resources/html/webrtcgateway/test/components/outgoingcall.jsx b/resources/html/webrtcgateway/test/components/outgoingcall.jsx
new file mode 100644
index 0000000..04b06f8
--- /dev/null
+++ b/resources/html/webrtcgateway/test/components/outgoingcall.jsx
@@ -0,0 +1,59 @@
+
+var OutgoingCall = React.createClass({
+    getInitialState: function() {
+        return {
+            call: null,
+            targetUri: '',
+        };
+    },
+
+    callStateChanged: function(oldState, newState, data) {
+        if (!this.isMounted()) {
+            // we might get here when the component has been unmounted
+            return;
+        }
+        if (newState === 'terminated') {
+            this.replaceState(this.getInitialState());
+        }
+    },
+
+    toggleCall: function(event) {
+        event.preventDefault();
+        if (this.state.call === null) {
+            var callOptions = {pcConfig: {iceServers: [{urls: 'stun:stun.l.google.com:19302'}]}};
+            var call = this.props.account.call(this.state.targetUri, callOptions);
+            call.on('stateChanged', this.callStateChanged);
+            this.setState({call: call});
+        } else {
+            this.state.call.terminate();
+        }
+    },
+
+    handleTargetChange: function(event) {
+        this.setState({targetUri: event.target.value});
+    },
+
+    render: function() {
+        var ready = this.props.account !== null;
+        var buttonClasses = 'btn';
+        if (this.state.call) {
+            buttonClasses += ' btn-danger';
+        } else {
+            buttonClasses += ' btn-success';
+        }
+        return (
+            <div className="call-box" style={{overflow: 'hidden'}}>
+                <form role="form">
+                    <div className="input-group">
+                        <input type="text" id="target-input" className="form-control" disabled={!ready || this.state.call !== null} onChange={this.handleTargetChange} value={this.state.targetUri} placeholder="Call SIP URI (user@domain format)"/>
+                        <span className="input-group-btn">
+                            <button type="submit" className={buttonClasses} onClick={this.toggleCall} disabled={!ready || this.state.targetUri.length === 0}>
+                                {this.state.call ? "Hangup": "Call"}
+                            </button>
+                        </span>
+                    </div>
+                </form>
+            </div>
+        );
+    },
+});
diff --git a/resources/html/webrtcgateway/test/components/videobox.jsx b/resources/html/webrtcgateway/test/components/videobox.jsx
new file mode 100644
index 0000000..326cc00
--- /dev/null
+++ b/resources/html/webrtcgateway/test/components/videobox.jsx
@@ -0,0 +1,33 @@
+
+var VideoBox = React.createClass({
+    componentDidMount: function() {
+        var localStream = this.props.call.getLocalStreams()[0];
+        var localVideoElement = React.findDOMNode(this.refs.localVideo);
+        var remoteStream = this.props.call.getRemoteStreams()[0];
+        var remoteVideoElement = React.findDOMNode(this.refs.callVideo);
+        sylkrtc.attachMediaStream(localVideoElement, localStream);
+        sylkrtc.attachMediaStream(remoteVideoElement, remoteStream);
+    },
+
+    hangupClicked: function(event) {
+        event.preventDefault();
+        this.props.call.terminate();
+    },
+
+    render: function() {
+        return (
+            <div className="video-box" style={{overflow: 'hidden'}}>
+                <div style={{position: 'relative'}}>
+                    <video id="call-video" ref="callVideo" autoPlay width="100%"/>
+                    <video id="local-video" ref="localVideo" autoPlay width="200px" style={{position: 'absolute', left: '8px', top: '8px'}}/>
+                </div>
+                <br/>
+                <div>
+                    <button type="button" className="btn btn-danger btn-block" onClick={this.hangupClicked}>
+                        Hangup
+                    </button>
+                </div>
+            </div>
+        );
+    },
+});
diff --git a/resources/html/webrtcgateway/test/index.html b/resources/html/webrtcgateway/test/index.html
new file mode 100644
index 0000000..12d7994
--- /dev/null
+++ b/resources/html/webrtcgateway/test/index.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>SylkServer WebRTC Test Application</title>
+    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
+    <script src="../js/sylkrtc.js"></script>
+    <script type="text/jsx" src="components/accountbox.jsx"></script>
+    <script type="text/jsx" src="components/incomingcall.jsx"></script>
+    <script type="text/jsx" src="components/outgoingcall.jsx"></script>
+    <script type="text/jsx" src="components/videobox.jsx"></script>
+    <script type="text/jsx" src="components/app.jsx"></script>
+  </head>
+  <body>
+    <script type="text/jsx">
+      React.render(<SylkTest/>, document.body);
+    </script>
+  </body>
+</html>
diff --git a/setup.py b/setup.py
index 00b2ae3..5b5ad1b 100755
--- a/setup.py
+++ b/setup.py
@@ -1,43 +1,43 @@
 #!/usr/bin/python
 
 # Copyright (C) 2010-2012 AG Projects. See LICENSE for details
 #
 
 import glob
 import os
 import re
 
 from distutils.core import setup
 
 
 def get_version():
     return re.search(r"""__version__\s+=\s+(?P<quote>['"])(?P<version>.+?)(?P=quote)""", open('sylk/__init__.py').read()).group('version')
 
 def find_packages(toplevel):
     return [directory.replace(os.path.sep, '.') for directory, subdirs, files in os.walk(toplevel) if '__init__.py' in files]
 
 def get_resource_files(resource):
     for root, dirs, files in os.walk(os.path.join('resources', resource)):
         yield (os.path.join('share/sylkserver', root[10:]), [os.path.join(root, f) for f in files])
 
 setup(name         = "sylkserver",
       version      = get_version(),
       author       = "AG Projects",
       author_email = "support@ag-projects.com",
       url          = "http://sylkserver.com",
       description  = "SylkServer - An Extensible SIP Application Server",
       classifiers  = [
             "Development Status :: 5 - Production/Stable",
             "Intended Audience :: Service Providers",
             "License :: GNU General Public License 3",
             "Operating System :: OS Independent",
             "Programming Language :: Python"
                      ],
       packages     = find_packages('sylk'),
       scripts      = ['sylk-server'],
       data_files   = [('/var/lib/sylkserver', []),
                       ('/etc/sylkserver', glob.glob('*.ini.sample')),
                       ('/etc/sylkserver/tls', glob.glob('resources/tls/*.crt'))] + \
-                      list(get_resource_files('sounds'))
+                      list(get_resource_files('sounds')) + list(get_resource_files('html'))
       )
 
diff --git a/sylk/applications/webrtcgateway/__init__.py b/sylk/applications/webrtcgateway/__init__.py
new file mode 100644
index 0000000..6429346
--- /dev/null
+++ b/sylk/applications/webrtcgateway/__init__.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
+from sylk.applications import SylkApplication
+from sylk.applications.webrtcgateway.logger import log
+from sylk.applications.webrtcgateway.util import IdentityFormatter
+from sylk.applications.webrtcgateway.web import WebHandler
+
+
+class WebRTCGatewayApplication(SylkApplication):
+    def __init__(self):
+        self.ws = WebHandler()
+
+    def start(self):
+        self.ws.start()
+
+    def stop(self):
+        self.ws.stop()
+
+    def incoming_session(self, session):
+        log.msg(u'New incoming session %s from %s rejected' % (session.call_id, IdentityFormatter.format(session.remote_identity)))
+        session.reject(403)
+
+    def incoming_subscription(self, request, data):
+        request.reject(405)
+
+    def incoming_referral(self, request, data):
+        request.reject(405)
+
+    def incoming_message(self, request, data):
+        request.reject(405)
+
diff --git a/sylk/applications/webrtcgateway/configuration.py b/sylk/applications/webrtcgateway/configuration.py
new file mode 100644
index 0000000..69f5579
--- /dev/null
+++ b/sylk/applications/webrtcgateway/configuration.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
+from application.configuration import ConfigSection, ConfigSetting
+from application.configuration.datatypes import StringList
+
+from sylk.configuration.datatypes import SIPProxyAddress
+
+
+class GeneralConfig(ConfigSection):
+    __cfgfile__ = 'webrtcgateway.ini'
+    __section__ = 'General'
+
+    web_origins = ConfigSetting(type=StringList, value=['*'])
+    sip_domains = ConfigSetting(type=StringList, value=['*'])
+    outbound_sip_proxy = ConfigSetting(type=SIPProxyAddress, value=None)
+    trace_websocket = False
+
+
+class JanusConfig(ConfigSection):
+    __cfgfile__ = 'webrtcgateway.ini'
+    __section__ = 'Janus'
+
+    api_url = 'ws://127.0.0.1:8188'
+    trace_janus = False
+
diff --git a/sylk/applications/webrtcgateway/janus/__init__.py b/sylk/applications/webrtcgateway/janus/__init__.py
new file mode 100644
index 0000000..9303e2a
--- /dev/null
+++ b/sylk/applications/webrtcgateway/janus/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
diff --git a/sylk/applications/webrtcgateway/janus/backend.py b/sylk/applications/webrtcgateway/janus/backend.py
new file mode 100644
index 0000000..a801774
--- /dev/null
+++ b/sylk/applications/webrtcgateway/janus/backend.py
@@ -0,0 +1,264 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
+import functools
+import json
+import uuid
+
+from application.notification import IObserver, NotificationCenter, NotificationData
+from application.python import Null
+from application.python.types import Singleton
+from autobahn.twisted.websocket import connectWS, WebSocketClientFactory, WebSocketClientProtocol
+from sipsimple.util import ISOTimestamp
+from twisted.internet import reactor, defer
+from twisted.internet.protocol import ReconnectingClientFactory
+from twisted.python.failure import Failure
+from zope.interface import implements
+
+from sylk import __version__ as SYLK_VERSION
+from sylk.applications.webrtcgateway.configuration import JanusConfig
+from sylk.applications.webrtcgateway.janus.logger import Logger as JanusLogger
+from sylk.applications.webrtcgateway.logger import log
+
+
+class JanusRequest(object):
+    def __init__(self, request_type, **kwargs):
+        self.janus = request_type
+        self.transaction = uuid.uuid4().hex
+        self.__dict__.update(**kwargs)
+
+    @property
+    def type(self):
+        return self.janus
+
+    @property
+    def transaction_id(self):
+        return self.transaction
+
+    def as_dict(self):
+        return self.__dict__.copy()
+
+
+class JanusError(Exception):
+    def __init__(self, code, reason):
+        super(JanusError, self).__init__(reason)
+        self.code = code
+        self.reason = reason
+
+
+class JanusClientProtocol(WebSocketClientProtocol):
+    _janus_event_handlers = None
+    _janus_pending_transactions = None
+    _janus_keepalive_timers = None
+    _janus_keepalive_timeout = 45
+
+    def onOpen(self):
+        notification_center = NotificationCenter()
+        notification_center.post_notification('JanusBackendConnected', sender=self)
+        self._janus_pending_transactions = {}
+        self._janus_keepalive_timers = {}
+        self._janus_event_handlers = {}
+
+    def onMessage(self, payload, isBinary):
+        if isBinary:
+            log.warn('Unexpected binary payload received')
+            return
+        if JanusConfig.trace_janus:
+            self.factory.janus_logger.msg("IN", ISOTimestamp.now(), payload)
+        try:
+            data = json.loads(payload)
+        except Exception, e:
+            log.warn('Error decoding payload: %s' % e)
+            return
+        try:
+            message_type = data.pop('janus')
+        except KeyError:
+            log.warn('Received payload lacks message type: %s' % payload)
+            return
+        transaction_id = data.pop('transaction', None)
+        if message_type == 'event' or transaction_id is None:
+            # This is an event. Janus is not very consistent here, some 'events'
+            # do have the transaction id set. So we check for the message type as well.
+            handle_id = data.pop('sender', -1)
+            handler = self._janus_event_handlers.get(handle_id, Null)
+            try:
+                handler(handle_id, message_type, data)
+            except Exception:
+                log.err()
+            return
+        try:
+            req, d = self._janus_pending_transactions.pop(transaction_id)
+        except KeyError:
+            log.warn('Discarding unexpected response: %s' % payload)
+            return
+        if message_type == 'error':
+            code = data['error']['code']
+            reason = data['error']['reason']
+            d.errback(JanusError(code, reason))
+        else:
+            if req.type == 'info':
+                result = data
+            elif req.type == 'create':
+                result = data['data']['id']
+            elif req.type == 'destroy':
+                result = None
+            elif req.type == 'attach':
+                result = data['data']['id']
+            elif req.type == 'detach':
+                result = None
+            elif req.type == 'keepalive':
+                result = None
+            elif req.type == 'ack':
+                result = None
+            else:
+                result = data
+            d.callback(result)
+
+    def connectionLost(self, reason):
+        super(JanusClientProtocol, self).connectionLost(reason)
+        notification_center = NotificationCenter()
+        notification_center.post_notification('JanusBackendDisconnected', sender=self, data=NotificationData(reason=reason.getErrorMessage()))
+
+    def disconnect(self, code=1000, reason=u''):
+        self.sendClose(code, reason)
+
+    def janus_set_event_handler(self, handle_id, event_handler):
+        if event_handler is None:
+            self._janus_event_handlers.pop(handle_id, None)
+        else:
+            assert callable(event_handler)
+            self._janus_event_handlers[handle_id] = event_handler
+
+    def _janus_send_request(self, req):
+        data = json.dumps(req.as_dict())
+        if JanusConfig.trace_janus:
+            self.factory.janus_logger.msg("OUT", ISOTimestamp.now(), data)
+        self.sendMessage(data)
+        d = defer.Deferred()
+        self._janus_pending_transactions[req.transaction_id] = (req, d)
+        return d
+
+    def janus_info(self):
+        req = JanusRequest('info')
+        return self._janus_send_request(req)
+
+    def janus_create_session(self):
+        req = JanusRequest('create')
+        return self._janus_send_request(req)
+
+    def janus_destroy_session(self, session_id):
+        req = JanusRequest('destroy', session_id=session_id)
+        return self._janus_send_request(req)
+
+    def janus_attach(self, session_id, plugin):
+        req = JanusRequest('attach', session_id=session_id, plugin=plugin)
+        return self._janus_send_request(req)
+
+    def janus_detach(self, session_id, handle_id):
+        req = JanusRequest('detach', session_id=session_id, handle_id=handle_id)
+        return self._janus_send_request(req)
+
+    def janus_message(self, session_id, handle_id, body, jsep=None):
+        req = JanusRequest('message', session_id=session_id, handle_id=handle_id, body=body)
+        if jsep is not None:
+            req.jsep = jsep
+        return self._janus_send_request(req)
+
+    def janus_trickle(self, session_id, handle_id, candidates):
+        req = JanusRequest('trickle', session_id=session_id, handle_id=handle_id)
+        if candidates:
+            if len(candidates) == 1:
+                req.candidate = candidates[0]
+            else:
+                req.candidates = candidates
+        else:
+            req.candidate = {'completed': True}
+        return self._janus_send_request(req)
+
+    def _janus_keepalive_cb(self, session_id, result):
+        if isinstance(result, Failure):
+            self.janus_stop_keepalive(session_id)
+            return
+        self._janus_keepalive_timers[session_id] = reactor.callLater(self._janus_keepalive_timeout, self._janus_send_keepalive, session_id)
+
+    def _janus_send_keepalive(self, session_id):
+        req = JanusRequest('keepalive', session_id=session_id)
+        d = self._janus_send_request(req)
+        d.addBoth(functools.partial(self._janus_keepalive_cb, session_id))
+        return d
+
+    def janus_start_keepalive(self, session_id):
+        self.janus_stop_keepalive(session_id)
+        self._janus_keepalive_timers[session_id] = reactor.callLater(self._janus_keepalive_timeout, self._janus_send_keepalive, session_id)
+
+    def janus_stop_keepalive(self, session_id):
+        timer = self._janus_keepalive_timers.pop(session_id, None)
+        if timer is not None and timer.active():
+            timer.cancel()
+
+
+class JanusClientFactory(ReconnectingClientFactory, WebSocketClientFactory):
+    noisy = False
+    protocol = JanusClientProtocol
+
+
+class JanusBackend(object):
+    __metaclass__ = Singleton
+    implements(IObserver)
+
+    def __init__(self):
+        self.janus_logger = JanusLogger()
+        self.factory = JanusClientFactory(url=JanusConfig.api_url,
+                                          protocols=['janus-protocol'],
+                                          useragent='SylkServer/%s' % SYLK_VERSION)
+        self.factory.janus_logger = self.janus_logger
+        self.connector = None
+        self.connection = Null
+        self._stopped = False
+
+    def __getattr__(self, attr):
+        if attr.startswith('janus_'):
+            return getattr(self.connection, attr)
+        return self.attr
+
+    @property
+    def ready(self):
+        return self.connection is not Null
+
+    def start(self):
+        self.janus_logger.start()
+        notification_center = NotificationCenter()
+        notification_center.add_observer(self, name='JanusBackendConnected')
+        notification_center.add_observer(self, name='JanusBackendDisconnected')
+        self.connector = connectWS(self.factory)
+
+    def stop(self):
+        if self._stopped:
+            return
+        self._stopped = True
+        self.janus_logger.stop()
+        self.factory.stopTrying()
+        notification_center = NotificationCenter()
+        notification_center.discard_observer(self, name='JanusBackendConnected')
+        notification_center.discard_observer(self, name='JanusBackendDisconnected')
+        if self.connector is not None:
+            self.connector.disconnect()
+            self.connector = None
+        if self.connection is not None:
+            self.connection.disconnect()
+            self.connection = Null
+
+    def handle_notification(self, notification):
+        handler = getattr(self, '_NH_%s' % notification.name, Null)
+        handler(notification)
+
+    def _NH_JanusBackendConnected(self, notification):
+        assert self.connection is Null
+        self.connection = notification.sender
+        log.msg('Janus backend connection up')
+        self.factory.resetDelay()
+
+    def _NH_JanusBackendDisconnected(self, notification):
+        log.msg('Janus backend connection down: %s' % notification.data.reason)
+        self.connection = Null
+
diff --git a/sylk/applications/webrtcgateway/janus/logger.py b/sylk/applications/webrtcgateway/janus/logger.py
new file mode 100644
index 0000000..67db9ba
--- /dev/null
+++ b/sylk/applications/webrtcgateway/janus/logger.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details.
+#
+
+"""
+Logging support for Janus traffic.
+"""
+
+__all__ = ["Logger"]
+
+import os
+import sys
+
+from application.system import makedirs
+from sipsimple.configuration.settings import SIPSimpleSettings
+from sipsimple.threading import run_in_thread
+
+
+class Logger(object):
+    def __init__(self):
+        self.stopped = False
+        self._janustrace_filename = None
+        self._janustrace_file = None
+        self._janustrace_error = False
+        self._janustrace_start_time = None
+        self._janustrace_packet_count = 0
+
+        self._log_directory_error = False
+
+    def start(self):
+        # try to create the log directory
+        try:
+            self._init_log_directory()
+            self._init_log_file()
+        except Exception:
+            pass
+        self.stopped = False
+
+    def stop(self):
+        self.stopped = True
+        if self._janustrace_file is not None:
+            self._janustrace_file.close()
+            self._janustrace_file = None
+
+    def msg(self, direction, timestamp, packet):
+        if self._janustrace_start_time is None:
+            self._janustrace_start_time = timestamp
+        self._janustrace_packet_count += 1
+        buf = ["%s: Packet %d, +%s" % (direction, self._janustrace_packet_count, (timestamp - self._janustrace_start_time))]
+        buf.append(packet)
+        buf.append('--')
+        message = '\n'.join(buf)
+        self._process_log((message, timestamp))
+
+    @run_in_thread('log-io')
+    def _process_log(self, record):
+        if self.stopped:
+            return
+        message, timestamp = record
+        try:
+            self._init_log_file()
+        except Exception:
+            pass
+        else:
+            self._janustrace_file.write('%s [%s %d]: %s\n' % (timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+            self._janustrace_file.flush()
+
+    def _init_log_directory(self):
+        settings = SIPSimpleSettings()
+        log_directory = settings.logs.directory.normalized
+        try:
+            makedirs(log_directory)
+        except Exception, e:
+            if not self._log_directory_error:
+                print "failed to create logs directory '%s': %s" % (log_directory, e)
+                self._log_directory_error = True
+            self._janustrace_error = True
+            raise
+        else:
+            self._log_directory_error = False
+            if self._janustrace_filename is None:
+                self._janustrace_filename = os.path.join(log_directory, 'janus_trace.log')
+                self._janustrace_error = False
+
+    def _init_log_file(self):
+        if self._janustrace_file is None:
+            self._init_log_directory()
+            filename = self._janustrace_filename
+            try:
+                self._janustrace_file = open(filename, 'a')
+            except Exception, e:
+                if not self._janustrace_error:
+                    print "failed to create log file '%s': %s" % (filename, e)
+                    self._janustrace_error = True
+                raise
+            else:
+                self._janustrace_error = False
+
diff --git a/sylk/applications/webrtcgateway/logger.py b/sylk/applications/webrtcgateway/logger.py
new file mode 100644
index 0000000..1fc09c6
--- /dev/null
+++ b/sylk/applications/webrtcgateway/logger.py
@@ -0,0 +1,9 @@
+# Copyright (C) 2013 AG Projects. See LICENSE for details
+#
+
+__all__ = ['log']
+
+from sylk.applications import ApplicationLogger
+
+log = ApplicationLogger.for_package(__package__)
+
diff --git a/sylk/applications/webrtcgateway/util.py b/sylk/applications/webrtcgateway/util.py
new file mode 100644
index 0000000..3ce1a52
--- /dev/null
+++ b/sylk/applications/webrtcgateway/util.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
+from eventlib.coros import event
+
+
+class IdentityFormatter(object):
+    @classmethod
+    def format(cls, identity):
+        if identity.display_name:
+            return u'%s <sip:%s@%s>' % (identity.display_name, identity.uri.user, identity.uri.host)
+        else:
+            return u'sip:%s@%s' % (identity.uri.user, identity.uri.host)
+
+
+class GreenEvent(object):
+    def __init__(self):
+        self._event = event()
+
+    def set(self):
+        if self._event.ready():
+            return
+        self._event.send(True)
+
+    def is_set(self):
+        return self._event.ready()
+
+    def clear(self):
+        if self._event.ready():
+            self._event.reset()
+
+    def wait(self):
+        return self._event.wait()
+
diff --git a/sylk/applications/webrtcgateway/web/__init__.py b/sylk/applications/webrtcgateway/web/__init__.py
new file mode 100644
index 0000000..48df4fb
--- /dev/null
+++ b/sylk/applications/webrtcgateway/web/__init__.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
+from application.python.types import Singleton
+from autobahn.twisted.resource import WebSocketResource
+
+from sylk import __version__ as sylk_version
+from sylk.applications.webrtcgateway.configuration import GeneralConfig, JanusConfig
+from sylk.applications.webrtcgateway.janus.backend import JanusBackend
+from sylk.applications.webrtcgateway.logger import log
+from sylk.applications.webrtcgateway.web.api import SYLK_WS_PROTOCOL, SylkWebSocketServerFactory
+from sylk.applications.webrtcgateway.websocket_logger import Logger as WSLogger
+from sylk.resources import Resources
+from sylk.web import Klein, StaticFileResource, server
+
+
+class WebRTCGatewayWeb(object):
+    __metaclass__ = Singleton
+
+    app = Klein()
+    _resource = None
+    _ws_resource = None
+
+    def __init__(self, ws_factory):
+        self._ws_resource = WebSocketResource(ws_factory)
+
+    def resource(self):
+        if self._resource is None:
+            self._resource = self.app.resource()
+        return self._resource
+
+    @app.route('/')
+    def index(self, request):
+        path = Resources.get('html/webrtcgateway/index.html')
+        r = StaticFileResource(path)
+        r.isLeaf = True
+        return r
+
+    @app.route('/ws')
+    def ws(self, request):
+        return self._ws_resource
+
+    @app.route('/js/', branch=True)
+    def js(self, request):
+        path = Resources.get('html/webrtcgateway/js/')
+        return StaticFileResource(path)
+
+    @app.route('/test/', branch=True)
+    def test(self, request):
+        path = Resources.get('html/webrtcgateway/test/')
+        return StaticFileResource(path)
+
+
+class WebHandler(object):
+    def __init__(self):
+        self.backend = None
+        self.factory = None
+        self.resource = None
+        self.web = None
+        self.ws_logger = WSLogger()
+
+    def start(self):
+        ws_url = 'ws' + server.url[4:] + '/webrtcgateway/ws'
+        self.factory = SylkWebSocketServerFactory(ws_url, protocols=[SYLK_WS_PROTOCOL], server='SylkServer/%s' % sylk_version, debug=False)
+        self.factory.setProtocolOptions(allowedOrigins=GeneralConfig.web_origins)
+        self.factory.ws_logger = self.ws_logger
+
+        self.web = WebRTCGatewayWeb(self.factory)
+        server.register_resource('webrtcgateway', self.web.resource())
+
+        log.msg('WebSocket handler started at %s' % ws_url)
+        log.msg('Allowed web origins: %s' % ', '.join(GeneralConfig.web_origins))
+        log.msg('Allowed SIP domains: %s' % ', '.join(GeneralConfig.sip_domains))
+        log.msg('Using Janus API: %s' % JanusConfig.api_url)
+
+        self.ws_logger.start()
+
+        self.backend = JanusBackend()
+        self.backend.start()
+
+        self.factory.backend = self.backend
+
+    def stop(self):
+        if self.factory is not None:
+            for conn in self.factory.connections.copy():
+                conn.failConnection()
+            self.factory = None
+        if self.backend is not None:
+            self.backend.stop()
+            self.backend = None
+        self.ws_logger.stop()
diff --git a/sylk/applications/webrtcgateway/web/api.py b/sylk/applications/webrtcgateway/web/api.py
new file mode 100644
index 0000000..a3a38de
--- /dev/null
+++ b/sylk/applications/webrtcgateway/web/api.py
@@ -0,0 +1,641 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details
+#
+
+import json
+import random
+import re
+import uuid
+
+from application.python import Null
+from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory
+from autobahn.websocket import http
+from eventlib import coros, proc
+from eventlib.twistedutil import block_on
+from sipsimple.configuration.settings import SIPSimpleSettings
+from sipsimple.core import SIPURI, SIPCoreError
+from sipsimple.lookup import DNSLookup, DNSLookupError
+from sipsimple.threading.green import run_in_green_thread
+from sipsimple.util import ISOTimestamp
+from twisted.internet import reactor
+
+from sylk.applications.webrtcgateway.configuration import GeneralConfig
+from sylk.applications.webrtcgateway.logger import log
+from sylk.applications.webrtcgateway.util import GreenEvent
+
+
+SYLK_WS_PROTOCOL = 'sylkRTC-1'
+SIP_PREFIX_RE = re.compile('^sips?:')
+
+
+class AccountInfo(object):
+    def __init__(self, id, password):
+        self.id = id
+        self.password = password
+        self.registration_state = None
+        self.janus_handle_id = None
+
+    @property
+    def uri(self):
+        return 'sip:%s' % self.id
+
+
+class SessionInfoBase(object):
+    type = None    # override in subclass
+
+    def __init__(self, id):
+        self.id = id
+        self.direction = None
+        self.state = None
+        self.account_id = None
+        self.remote_identity = None
+
+    def init_outgoing(self, account_id, destination):
+        self.account_id = account_id
+        self.direction = 'outgoing'
+        self.state = 'connecting'
+        self.remote_identity = destination
+
+    def init_incoming(self, account_id, originator):
+        self.account_id = account_id
+        self.direction = 'incoming'
+        self.state = 'connecting'
+        self.remote_identity = originator
+
+    @property
+    def local_identity(self):
+        return self.account_id
+
+
+class JanusSessionInfo(SessionInfoBase):
+    type = 'janus'
+
+    def __init__(self, id):
+        super(JanusSessionInfo, self).__init__(id)
+        self.janus_handle_id = None
+
+
+class Operation(object):
+    __slots__ = ('name', 'data')
+
+    def __init__(self, name, data):
+        self.name = name
+        self.data = data
+
+
+class SylkWebSocketServerProtocol(WebSocketServerProtocol):
+    janus_session_id = None
+    accounts_map = None           # account ID -> account
+    account_handles_map = None    # Janus handle ID -> account
+    sessions_map = None           # session ID -> session
+    session_handles_map = None    # Janus handle ID -> session
+    ready_event = None
+    resolver = None
+    proc = None
+    operations_queue = None
+
+    def onConnect(self, request):
+        log.msg('Incoming connection from %s (origin %s)' % (request.peer, request.origin))
+        if SYLK_WS_PROTOCOL not in request.protocols:
+            log.msg('Rejecting connection, remote does not support our sub-protocol')
+            raise http.HttpException(http.NOT_ACCEPTABLE[0], 'No compatible protocol specified')
+        if not self.backend.ready:
+            log.msg('Rejecting connection, backend is not connected')
+            raise http.HttpException(http.SERVICE_UNAVAILABLE[0], 'Backend is not connected')
+        return SYLK_WS_PROTOCOL
+
+    def onOpen(self):
+        self.factory.connections.add(self)
+        self.accounts_map = {}
+        self.account_handles_map = {}
+        self.sessions_map = {}
+        self.session_handles_map = {}
+        self.ready_event = GreenEvent()
+        self.resolver = DNSLookup()
+        self.proc = proc.spawn(self._operations_handler)
+        self.operations_queue = coros.queue()
+        self._create_janus_session()
+
+    def onMessage(self, payload, isBinary):
+        if isBinary:
+            log.warn('Received invalid binary message')
+            return
+        if GeneralConfig.trace_websocket:
+            self.factory.ws_logger.msg("IN", ISOTimestamp.now(), payload)
+        try:
+            data = json.loads(payload)
+        except Exception, e:
+            log.warn('Error parsing WebSocket payload: %s' % e)
+            return
+        try:
+            request_type = data.pop('sylkrtc')
+        except KeyError:
+            log.warn('Error getting WebSocket message type')
+            return
+        self.ready_event.wait()
+        op = Operation(request_type.lower(), data)
+        self.operations_queue.send(op)
+
+    def onClose(self, wasClean, code, reason):
+        if self.ready_event is None:
+            # Very early connection closed, onOpen wasn't even called
+            return
+        log.msg('Connection from %s closed' % self.transport.getPeer())
+        self.factory.connections.discard(self)
+        if self.ready_event.is_set():
+            assert self.janus_session_id is not None
+            self.backend.janus_stop_keepalive(self.janus_session_id)
+            self.backend.janus_destroy_session(self.janus_session_id)
+        if self.proc is not None:
+            self.proc.kill()
+            self.proc = None
+        # cleanup
+        self.ready_event.clear()
+        self.accounts_map.clear()
+        self.account_handles_map.clear()
+        self.sessions_map.clear()
+        self.session_handles_map.clear()
+        self.janus_session_id = None
+
+    def disconnect(self, code=1000, reason=u''):
+        self.sendClose(code, reason)
+
+    def _send_data(self, data):
+        if GeneralConfig.trace_websocket:
+            self.factory.ws_logger.msg("OUT", ISOTimestamp.now(), data)
+        self.sendMessage(data, False)
+
+    def _cleanup_session(self, session):
+        @run_in_green_thread
+        def do_cleanup():
+            if self.janus_session_id is None:
+                # The connection was closed, there is noting to do here
+                return
+            self.sessions_map.pop(session.id)
+            if session.direction == 'outgoing':
+                # Destroy plugin handle for outgoing sessions. For incoming ones it's the
+                # same as the account handle, so don't
+                block_on(self.backend.janus_detach(self.janus_session_id, session.janus_handle_id))
+                self.backend.janus_set_event_handler(session.janus_handle_id, None)
+            self.session_handles_map.pop(session.janus_handle_id)
+
+        # give it some time to receive other hangup events
+        reactor.callLater(2, do_cleanup)
+
+    @run_in_green_thread
+    def _create_janus_session(self):
+        if self.ready_event.is_set():
+            data = dict(sylkrtc='event', event='ready')
+            self._send_data(json.dumps(data))
+            return
+        try:
+            self.janus_session_id = block_on(self.backend.janus_create_session())
+            self.backend.janus_start_keepalive(self.janus_session_id)
+        except Exception, e:
+            log.warn('Error creating session, disconnecting: %s' % e)
+            self.disconnect(3000, unicode(e))
+            return
+        data = dict(sylkrtc='event', event='ready')
+        self._send_data(json.dumps(data))
+        self.ready_event.set()
+
+    def _lookup_sip_proxy(self, account):
+        sip_uri = SIPURI.parse('sip:%s' % account)
+
+        # The proxy dance: Sofia-SIP seems to do a DNS lookup per SIP message when a domain is passed
+        # as the proxy, so do the resolution ourselves and give it pre-resolver proxy URL. Since we use
+        # caching to avoid long delays, we randomize the results matching the highest priority route's
+        # transport.
+        proxy = GeneralConfig.outbound_sip_proxy
+        if proxy is not None:
+            proxy_uri = SIPURI(host=proxy.host,
+                               port=proxy.port,
+                               parameters={'transport': proxy.transport})
+        else:
+            proxy_uri = SIPURI(host=sip_uri.host)
+        settings = SIPSimpleSettings()
+        try:
+            routes = self.resolver.lookup_sip_proxy(proxy_uri, settings.sip.transport_list).wait()
+        except DNSLookupError, e:
+            raise DNSLookupError('DNS lookup error: %s' % e)
+        if not routes:
+            raise DNSLookupError('DNS lookup error: no results found')
+
+        # Get all routes with the highest priority transport and randomly pick one
+        route = random.choice([r for r in routes if r.transport==routes[0].transport])
+
+        # Build a proxy URI Sofia-SIP likes
+        return '%s:%s:%d%s' % ('sips' if route.transport=='tls' else 'sip',
+                               route.address,
+                               route.port,
+                               ';transport=%s' % route.transport if route.transport != 'tls' else '')
+
+    def _handle_janus_event(self, handle_id, event_type, event):
+        op = Operation('janus-event', data=dict(handle_id=handle_id, event_type=event_type, event=event))
+        self.operations_queue.send(op)
+
+    def _operations_handler(self):
+        while True:
+            op = self.operations_queue.wait()
+            handler = getattr(self, '_OH_%s' % op.name.replace('-', '_'), Null)
+            try:
+                handler(op.data)
+            except Exception:
+                log.err()
+            del op
+            del handler
+
+    def _OH_account_add(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            account = data['account']
+            password = data['password']
+
+            if account in self.accounts_map:
+                log.warn('Account %s already added' % account)
+                data = dict(sylkrtc='error', transaction=transaction, error='Account already added')
+                self._send_data(json.dumps(data))
+                return
+
+            # Validate URI
+            uri = 'sip:%s' % account
+            try:
+                sip_uri = SIPURI.parse(uri)
+            except SIPCoreError:
+                raise ValueError('Invalid account specified: %s' % account)
+            if not {'*', sip_uri.host}.intersection(GeneralConfig.sip_domains):
+                raise ValueError('SIP domain not allowed: %s' % sip_uri.host)
+
+            # Create and store our mapping
+            account_info = AccountInfo(account, password)
+            self.accounts_map[account_info.id] = account_info
+
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('Account %s added' % account)
+        except Exception, e:
+            log.error('account_add: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_account_remove(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            account = data['account']
+            account_info = self.accounts_map[account]
+            self.accounts_map.pop(account_info.id)
+
+            handle_id = account_info.janus_handle_id
+            if handle_id is not None:
+                block_on(self.backend.janus_detach(self.janus_session_id, handle_id))
+                self.backend.janus_set_event_handler(handle_id, None)
+                self.account_handles_map.pop(handle_id)
+
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('Account %s removed' % account)
+        except Exception, e:
+            log.error('account_remove: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_account_register(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            account = data['account']
+            account_info = self.accounts_map[account]
+
+            proxy = self._lookup_sip_proxy(account)
+
+            handle_id = account_info.janus_handle_id
+            if handle_id is not None:
+                # Destroy the existing plugin handle
+                block_on(self.backend.janus_detach(self.janus_session_id, handle_id))
+                self.backend.janus_set_event_handler(handle_id, None)
+                self.account_handles_map.pop(handle_id)
+                account_info.janus_handle_id = None
+
+            # Create a plugin handle
+            handle_id = block_on(self.backend.janus_attach(self.janus_session_id, 'janus.plugin.sip'))
+            self.backend.janus_set_event_handler(handle_id, self._handle_janus_event)
+            account_info.janus_handle_id = handle_id
+            self.account_handles_map[handle_id] = account_info
+
+            data = {'request': 'register',
+                    'username': account_info.uri,
+                    'ha1_secret': account_info.password,
+                    'proxy': proxy}
+            block_on(self.backend.janus_message(self.janus_session_id, handle_id, data))
+
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('Account %s will register' % account)
+        except Exception, e:
+            log.error('account-register: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_account_unregister(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            account = data['account']
+            account_info = self.accounts_map[account]
+
+            handle_id = account_info.janus_handle_id
+            if handle_id is not None:
+                block_on(self.backend.janus_detach(self.janus_session_id, handle_id))
+                self.backend.janus_set_event_handler(handle_id, None)
+                account_info.janus_handle_id = None
+                self.account_handles_map.pop(handle_id)
+
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('Account %s will unregister' % account)
+        except Exception, e:
+            log.error('account-unregister: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_session_create(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            account = data['account']
+            session = data['session']
+            uri = data['uri']
+            sdp = data['sdp']
+
+            account_info = self.accounts_map[account]
+            if session in self.sessions_map:
+                raise ValueError('Session ID already in use')
+
+            # Create a new plugin handle and 'register' it, without actually doing so
+            handle_id = block_on(self.backend.janus_attach(self.janus_session_id, 'janus.plugin.sip'))
+            self.backend.janus_set_event_handler(handle_id, self._handle_janus_event)
+            try:
+                proxy = self._lookup_sip_proxy(account_info.id)
+            except DNSLookupError:
+                block_on(self.backend.janus_detach(self.janus_session_id, handle_id))
+                self.backend.janus_set_event_handler(handle_id, None)
+                raise
+            account_uri = 'sip:%s' % account_info.id
+            data = {'request': 'register', 'username': account_uri, 'ha1_secret': account_info.password, 'proxy': proxy, 'send_register': False}
+            block_on(self.backend.janus_message(self.janus_session_id, handle_id, data))
+
+            session_info = JanusSessionInfo(session)
+            session_info.janus_handle_id = handle_id
+            session_info.init_outgoing(account, uri)
+            self.sessions_map[session_info.id] = session_info
+            self.session_handles_map[handle_id] = session_info
+
+            data = {'request': 'call', 'uri': 'sip:%s' % SIP_PREFIX_RE.sub('', uri)}
+            jsep = {'type': 'offer', 'sdp': sdp}
+            block_on(self.backend.janus_message(self.janus_session_id, handle_id, data, jsep))
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('Outgoing session %s from %s to %s created' % (session, account, uri))
+        except Exception, e:
+            log.error('session-create: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_session_answer(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            session = data['session']
+            session_info = self.sessions_map[session]
+            if session_info.direction != 'incoming':
+                raise RuntimeError('Cannot answer outgoing session')
+            if session_info.state != 'connecting':
+                raise RuntimeError('Invalid state for session answer')
+            sdp = data['sdp']
+            data = {'request': 'accept'}
+            jsep = {'type': 'answer', 'sdp': sdp}
+            block_on(self.backend.janus_message(self.janus_session_id, session_info.janus_handle_id, data, jsep))
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('%s answered session %s' % (session_info.account_id, session))
+        except Exception, e:
+            log.error('session-answer: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_session_trickle(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            candidates = data['candidates']
+            session = data['session']
+            session_info = self.sessions_map[session]
+            if session_info.state == 'terminated':
+                raise RuntimeError('Session is terminated')
+            block_on(self.backend.janus_trickle(self.janus_session_id, session_info.janus_handle_id, candidates))
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('Trickled ICE candidate(s) for session %s' % session)
+        except Exception, e:
+            log.error('session-trickle: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    def _OH_session_terminate(self, data):
+        transaction = data.get('transaction', None)
+        if transaction is None:
+            log.warn('Transaction not specified!')
+            return
+
+        try:
+            session = data['session']
+            session_info = self.sessions_map[session]
+            if session_info.state not in ('connecting', 'progress', 'accepted', 'established'):
+                raise RuntimeError('Invalid state for session terminate: \"%s\"' % session_info.state)
+            if session_info.direction == 'incoming' and session_info.state == 'connecting':
+                data = {'request': 'decline', 'code': 486}
+            else:
+                data = {'request': 'hangup'}
+            block_on(self.backend.janus_message(self.janus_session_id, session_info.janus_handle_id, data))
+            data = dict(sylkrtc='ack', transaction=transaction)
+            self._send_data(json.dumps(data))
+            log.msg('%s terminated session %s' % (session_info.account_id, session))
+        except Exception, e:
+            log.error('session-terminate: %s' % e)
+            data = dict(sylkrtc='error', transaction=transaction, error=str(e))
+            self._send_data(json.dumps(data))
+
+    # Event handlers
+
+    def _OH_janus_event(self, data):
+        handle_id = data['handle_id']
+        event_type = data['event_type']
+        event = data['event']
+
+        if event_type == 'event':
+            event_data = event['plugindata']['data']
+            if 'result' in event_data:
+                jsep = event.get('jsep', None)
+                event_type = event_data['result']['event']
+                if event_type in ('registering', 'registered', 'registration_failed', 'incomingcall'):
+                    # skip 'registered' events from session handles
+                    if event_type == 'registered' and event_data['result']['register_sent'] in (False, 'false'):
+                        return
+                    # account event
+                    try:
+                        account_info = self.account_handles_map[handle_id]
+                    except KeyError:
+                        log.warn('Could not find account for handle ID %s' % handle_id)
+                        return
+                    if event_type == 'incomingcall':
+                        originator = SIP_PREFIX_RE.sub('', event_data['result']['username'])
+                        jsep = event.get('jsep', None)
+                        assert jsep is not None
+                        session_id = uuid.uuid4().hex
+                        session = JanusSessionInfo(session_id)
+                        session.janus_handle_id = handle_id
+                        session.init_incoming(account_info.id, originator)
+                        self.sessions_map[session_id] = session
+                        self.session_handles_map[handle_id] = session
+                        data = dict(sylkrtc='account_event',
+                                    account=account_info.id,
+                                    session=session_id,
+                                    event='incoming_session',
+                                    data=dict(originator=originator, sdp=jsep['sdp']))
+                        log.msg('Incoming session %s %s <-> %s created' % (session.id, session.remote_identity, session.local_identity))
+                    else:
+                        registration_state = event_type
+                        if registration_state == 'registration_failed':
+                            registration_state = 'failed'
+                        if account_info.registration_state == registration_state:
+                            return
+                        account_info.registration_state = registration_state
+                        registration_data = dict(state=registration_state)
+                        if registration_state == 'failed':
+                            code = event_data['result']['code']
+                            reason = event_data['result']['reason']
+                            registration_data['reason'] = '%d %s' % (code, reason)
+                        data = dict(sylkrtc='account_event',
+                                    account=account_info.id,
+                                    event='registration_state',
+                                    data=registration_data)
+                        log.msg('Account %s registration state changed to %s' % (account_info.id, registration_state))
+                    self._send_data(json.dumps(data))
+                elif event_type in ('calling', 'accepted', 'hangup'):
+                    # session event
+                    try:
+                        session_info = self.session_handles_map[handle_id]
+                    except KeyError:
+                        log.warn('Could not find session for handle ID %s' % handle_id)
+                        return
+                    if event_type == 'hangup' and session_info.state == 'terminated':
+                        return
+                    if event_type == 'calling':
+                        session_info.state = 'progress'
+                    elif event_type == 'accepted':
+                        session_info.state = 'accepted'
+                    elif event_type == 'hangup':
+                        session_info.state = 'terminated'
+                    data = dict(sylkrtc='session_event',
+                                session=session_info.id,
+                                event='state',
+                                data=dict(state=session_info.state))
+                    log.msg('%s session %s state: %s' % (session_info.direction.title(), session_info.id, session_info.state))
+                    if session_info.state == 'accepted' and session_info.direction == 'outgoing':
+                        assert jsep is not None
+                        data['data']['sdp'] = jsep['sdp']
+                    elif session_info.state == 'terminated':
+                        code = event_data['result'].get('code', 0)
+                        reason = event_data['result'].get('reason', 'Unknown')
+                        reason = '%d %s' % (code, reason)
+                        data['data']['reason'] = reason
+                    self._send_data(json.dumps(data))
+                    if session_info.state == 'terminated':
+                        self._cleanup_session(session_info)
+                        log.msg('%s session %s %s <-> %s terminated (%s)' % (session_info.direction.title(),
+                                                                             session_info.id,
+                                                                             session_info.local_identity,
+                                                                             session_info.remote_identity,
+                                                                             reason))
+                elif event_type in ('ack', 'hangingup'):
+                    # ignore
+                    pass
+                else:
+                    log.warn('Unexpected event type: %s' % event_type)
+            else:
+                log.warn('Unexpected event: %s' % event)
+        elif event_type == 'webrtcup':
+            try:
+                session_info = self.session_handles_map[handle_id]
+            except KeyError:
+                log.msg('Could not find session for handle ID %s' % handle_id)
+                return
+            session_info.state = 'established'
+            data = dict(sylkrtc='session_event',
+                        session=session_info.id,
+                        event='state',
+                        data=dict(state=session_info.state))
+            log.msg('%s session %s state: %s' % (session_info.direction.title(), session_info.id, session_info.state))
+            self._send_data(json.dumps(data))
+        elif event_type == 'hangup':
+            try:
+                session_info = self.session_handles_map[handle_id]
+            except KeyError:
+                log.msg('Could not find session for handle ID %s' % handle_id)
+                return
+            if session_info.state != 'terminated':
+                session_info.state = 'terminated'
+                code = event.get('code', 0)
+                reason = event.get('reason', 'Unknown')
+                reason = '%d %s' % (code, reason)
+                data = dict(sylkrtc='session_event',
+                            session=session_info.id,
+                            event='state',
+                            data=dict(state=session_info.state, reason=reason))
+                log.msg('%s session %s state: %s' % (session_info.direction.title(), session_info.id, session_info.state))
+                self._send_data(json.dumps(data))
+                self._cleanup_session(session_info)
+                log.msg('%s session %s %s <-> %s terminated (%s)' % (session_info.direction.title(),
+                                                                     session_info.id,
+                                                                     session_info.local_identity,
+                                                                     session_info.remote_identity,
+                                                                     reason))
+        elif event_type in ('media', 'declining'):
+            # ignore
+            pass
+        else:
+            log.warn('Received unexpected event type: %s' % event_type)
+
+
+class SylkWebSocketServerFactory(WebSocketServerFactory):
+    protocol = SylkWebSocketServerProtocol
+    connections = set()
+    backend = None
+
+    def buildProtocol(self, addr):
+        protocol = self.protocol()
+        protocol.factory = self
+        protocol.backend = self.backend
+        return protocol
diff --git a/sylk/applications/webrtcgateway/websocket_logger.py b/sylk/applications/webrtcgateway/websocket_logger.py
new file mode 100644
index 0000000..d12cc95
--- /dev/null
+++ b/sylk/applications/webrtcgateway/websocket_logger.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2015 AG Projects. See LICENSE for details.
+#
+
+"""
+Logging support for WebSocket traffic.
+"""
+
+__all__ = ["Logger"]
+
+import os
+import sys
+
+from application.system import makedirs
+from sipsimple.configuration.settings import SIPSimpleSettings
+from sipsimple.threading import run_in_thread
+
+
+class Logger(object):
+    def __init__(self):
+        self.stopped = False
+        self._wstrace_filename = None
+        self._wstrace_file = None
+        self._wstrace_error = False
+        self._wstrace_start_time = None
+        self._wstrace_packet_count = 0
+
+        self._log_directory_error = False
+
+    def start(self):
+        # try to create the log directory
+        try:
+            self._init_log_directory()
+            self._init_log_file()
+        except Exception:
+            pass
+        self.stopped = False
+
+    def stop(self):
+        self.stopped = True
+        if self._wstrace_file is not None:
+            self._wstrace_file.close()
+            self._wstrace_file = None
+
+    def msg(self, direction, timestamp, packet):
+        if self._wstrace_start_time is None:
+            self._wstrace_start_time = timestamp
+        self._wstrace_packet_count += 1
+        buf = ["%s: Packet %d, +%s" % (direction, self._wstrace_packet_count, (timestamp - self._wstrace_start_time))]
+        buf.append(packet)
+        buf.append('--')
+        message = '\n'.join(buf)
+        self._process_log((message, timestamp))
+
+    @run_in_thread('log-io')
+    def _process_log(self, record):
+        if self.stopped:
+            return
+        message, timestamp = record
+        try:
+            self._init_log_file()
+        except Exception:
+            pass
+        else:
+            self._wstrace_file.write('%s [%s %d]: %s\n' % (timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message))
+            self._wstrace_file.flush()
+
+    def _init_log_directory(self):
+        settings = SIPSimpleSettings()
+        log_directory = settings.logs.directory.normalized
+        try:
+            makedirs(log_directory)
+        except Exception, e:
+            if not self._log_directory_error:
+                print "failed to create logs directory '%s': %s" % (log_directory, e)
+                self._log_directory_error = True
+            self._wstrace_error = True
+            raise
+        else:
+            self._log_directory_error = False
+            if self._wstrace_filename is None:
+                self._wstrace_filename = os.path.join(log_directory, 'webrtcgateway_trace.log')
+                self._wstrace_error = False
+
+    def _init_log_file(self):
+        if self._wstrace_file is None:
+            self._init_log_directory()
+            filename = self._wstrace_filename
+            try:
+                self._wstrace_file = open(filename, 'a')
+            except Exception, e:
+                if not self._wstrace_error:
+                    print "failed to create log file '%s': %s" % (filename, e)
+                    self._wstrace_error = True
+                raise
+            else:
+                self._wstrace_error = False
+
diff --git a/sylk/server.py b/sylk/server.py
index 8acff6d..ea09b5f 100644
--- a/sylk/server.py
+++ b/sylk/server.py
@@ -1,254 +1,256 @@
 # Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
 #
 
 import os
 import sys
 
 from threading import Event
 from uuid import uuid4
 
 from application import log
 from application.notification import NotificationCenter
 from application.python import Null
 from application.system import makedirs
 from eventlib import proc
 from sipsimple.account import Account, BonjourAccount, AccountManager
 from sipsimple.application import SIPApplication
 from sipsimple.audio import AudioDevice, RootAudioBridge
 from sipsimple.configuration.settings import SIPSimpleSettings
 from sipsimple.core import AudioMixer
 from sipsimple.lookup import DNSManager
 from sipsimple.storage import MemoryStorage
 from sipsimple.threading import ThreadManager
 from sipsimple.threading.green import run_in_green_thread
 from sipsimple.video import VideoDevice
 from twisted.internet import reactor
 
 # Load stream extensions needed for integration with SIP SIMPLE SDK
 import sylk.streams
 del sylk.streams
 
 from sylk.accounts import DefaultAccount
 from sylk.applications import IncomingRequestHandler
 from sylk.configuration import ServerConfig, SIPConfig, ThorNodeConfig
 from sylk.configuration.settings import AccountExtension, BonjourAccountExtension, SylkServerSettingsExtension
 from sylk.log import Logger
 from sylk.session import SessionManager
 from sylk.web import WebServer
 
 
 class SylkServer(SIPApplication):
     def __init__(self):
         self.request_handler = Null
         self.thor_interface = Null
         self.web_server = Null
 
         self.logger = Logger()
 
         self.stopping_event = Event()
         self.stop_event = Event()
 
     def start(self, options):
         self.options = options
         if self.options.enable_bonjour:
             ServerConfig.enable_bonjour = True
 
         notification_center = NotificationCenter()
         notification_center.add_observer(self, sender=self)
         notification_center.add_observer(self, name='ThorNetworkGotFatalError')
 
         Account.register_extension(AccountExtension)
         BonjourAccount.register_extension(BonjourAccountExtension)
         SIPSimpleSettings.register_extension(SylkServerSettingsExtension)
 
         try:
             super(SylkServer, self).start(MemoryStorage())
         except Exception, e:
             log.fatal("Error starting SIP Application: %s" % e)
             sys.exit(1)
 
     def _initialize_core(self):
         # SylkServer needs to listen for extra events and request types
 
         notification_center = NotificationCenter()
         settings = SIPSimpleSettings()
 
         # initialize core
         options = dict(# general
                        ip_address=SIPConfig.local_ip,
                        user_agent=settings.user_agent,
                        # SIP
                        detect_sip_loops=False,
                        udp_port=settings.sip.udp_port if 'udp' in settings.sip.transport_list else None,
                        tcp_port=settings.sip.tcp_port if 'tcp' in settings.sip.transport_list else None,
                        tls_port=None,
                        # TLS
                        tls_verify_server=False,
                        tls_ca_file=None,
                        tls_cert_file=None,
                        tls_privkey_file=None,
                        # rtp
                        rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
                        # audio
                        codecs=list(settings.rtp.audio_codec_list),
                        # video
                        video_codecs=list(settings.rtp.video_codec_list),
                        enable_colorbar_device=True,
                        # logging
                        log_level=settings.logs.pjsip_level if settings.logs.trace_pjsip else 0,
                        trace_sip=settings.logs.trace_sip,
                        # events and requests to handle
                        events={'conference': ['application/conference-info+xml'],
                                'presence': ['application/pidf+xml'],
                                'refer': ['message/sipfrag;version=2.0']},
                        incoming_events=set(['conference', 'presence']),
                        incoming_requests=set(['MESSAGE']))
         notification_center.add_observer(self, sender=self.engine)
         self.engine.start(**options)
 
     @run_in_green_thread
     def _initialize_subsystems(self):
         account_manager = AccountManager()
         dns_manager = DNSManager()
         notification_center = NotificationCenter()
         session_manager = SessionManager()
         settings = SIPSimpleSettings()
 
         notification_center.post_notification('SIPApplicationWillStart', sender=self)
         if self.state == 'stopping':
             reactor.stop()
             return
 
         # Initialize default account
         default_account = DefaultAccount()
         account_manager.default_account = default_account
 
         # initialize TLS
         self._initialize_tls()
 
         # initialize PJSIP internal resolver
         self.engine.set_nameservers(dns_manager.nameservers)
 
         # initialize audio objects
         voice_mixer = AudioMixer(None, None, settings.audio.sample_rate, 0, 9999)
         self.voice_audio_device = AudioDevice(voice_mixer)
         self.voice_audio_bridge = RootAudioBridge(voice_mixer)
         self.voice_audio_bridge.add(self.voice_audio_device)
 
         # initialize video objects
         self.video_device = VideoDevice(u'Colorbar generator', settings.video.resolution, settings.video.framerate)
 
         # initialize instance id
         settings.instance_id = uuid4().urn
         settings.save()
 
         # initialize ZRTP cache
         makedirs(ServerConfig.spool_dir.normalized)
         self.engine.zrtp_cache = os.path.join(ServerConfig.spool_dir.normalized, 'zrtp.db')
 
         # initialize middleware components
         dns_manager.start()
         account_manager.start()
         session_manager.start()
 
         notification_center.add_observer(self, name='CFGSettingsObjectDidChange')
 
         self.state = 'started'
         notification_center.post_notification('SIPApplicationDidStart', sender=self)
 
         # start SylkServer components
         self.web_server = WebServer()
         self.web_server.start()
         self.request_handler = IncomingRequestHandler()
         self.request_handler.start()
         if ThorNodeConfig.enabled:
             from sylk.interfaces.sipthor import ConferenceNode
             self.thor_interface = ConferenceNode()
             thor_roles = []
             if 'conference' in self.request_handler.applications:
                 thor_roles.append('conference_server')
             if 'xmppgateway' in self.request_handler.applications:
                 thor_roles.append('xmpp_gateway')
+            if 'webrtcgateway' in self.request_handler.applications:
+                thor_roles.append('webrtc_gateway')
             self.thor_interface.start(thor_roles)
 
     @run_in_green_thread
     def _shutdown_subsystems(self):
         dns_manager = DNSManager()
         account_manager = AccountManager()
         session_manager = SessionManager()
 
         # terminate all sessions
         p = proc.spawn(session_manager.stop)
         p.wait()
 
         # shutdown SylkServer components
         procs = [proc.spawn(self.web_server.stop), proc.spawn(self.request_handler.stop), proc.spawn(self.thor_interface.stop)]
         proc.waitall(procs)
 
         # shutdown other middleware components
         procs = [proc.spawn(dns_manager.stop), proc.spawn(account_manager.stop)]
         proc.waitall(procs)
 
         # shutdown engine
         self.engine.stop()
         self.engine.join()
 
         # stop threads
         thread_manager = ThreadManager()
         thread_manager.stop()
 
         # stop the reactor
         reactor.stop()
 
     def _NH_AudioDevicesDidChange(self, notification):
         pass
 
     def _NH_DefaultAudioDeviceDidChange(self, notification):
         pass
 
     def _NH_SIPApplicationFailedToStartTLS(self, notification):
         log.fatal("Couldn't set TLS options: %s" % notification.data.error)
 
     def _NH_SIPApplicationWillStart(self, notification):
         self.logger.start()
         settings = SIPSimpleSettings()
         if settings.logs.trace_sip and self.logger._siptrace_filename is not None:
             log.msg('Logging SIP trace to file "%s"' % self.logger._siptrace_filename)
         if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None:
             log.msg('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename)
         if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None:
             log.msg('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename)
         if settings.logs.trace_notifications and self.logger._notifications_filename is not None:
             log.msg('Logging notifications trace to file "%s"' % self.logger._notifications_filename)
 
     def _NH_SIPApplicationDidStart(self, notification):
         settings = SIPSimpleSettings()
         local_ip = SIPConfig.local_ip
         log.msg("SylkServer started, listening on:")
         for transport in settings.sip.transport_list:
             try:
                 log.msg("%s:%d (%s)" % (local_ip, getattr(self.engine, '%s_port' % transport), transport.upper()))
             except TypeError:
                 pass
 
     def _NH_SIPApplicationWillEnd(self, notification):
         self.stopping_event.set()
 
     def _NH_SIPApplicationDidEnd(self, notification):
         log.msg('SIP application ended')
         self.logger.stop()
         if not self.stopping_event.is_set():
             log.warning('SIP application ended without shutting down all subsystems')
             self.stopping_event.set()
         self.stop_event.set()
 
     def _NH_SIPEngineGotException(self, notification):
         log.error('An exception occured within the SIP core:\n%s\n' % notification.data.traceback)
 
     def _NH_SIPEngineDidFail(self, notification):
         log.error('SIP engine failed')
         super(SylkServer, self)._NH_SIPEngineDidFail(notification)
 
     def _NH_ThorNetworkGotFatalError(self, notification):
         log.error("All Thor Event Servers have unrecoverable errors.")
 
diff --git a/webrtcgateway.ini.sample b/webrtcgateway.ini.sample
new file mode 100644
index 0000000..eddf6b5
--- /dev/null
+++ b/webrtcgateway.ini.sample
@@ -0,0 +1,29 @@
+; SylkServer WebRTC gateway configuration file
+;
+; For the gateway to work Janus needs to be properly installed and configured,
+; please refer to README.webrtc for detailed instructions
+;
+[General]
+; List of allowed web origins. The connection origin (Origin header in the
+; HTTP request) will be checked against the list defined here, if the domain
+; is no allowed the connection will be refused.
+; * (the default) means any
+; web_origins = *
+
+; Proxy used for outbound SIP traffic
+; outbound_sip_proxy =
+
+; List of allowed SIP domains for managing accounts
+; sip_domains = *
+
+; Boolean indicating if the WebSocket messages sent to/from clients should be logged
+; to a file
+; trace_websocket = False
+
+[Janus]
+; URL pointing to the Janus API endpoint (only WebSocket is supported)
+; api_url = ws://localhost:8188
+
+; Boolean indicating if the messages between SylkServer and Janus should be logged to
+; a file
+; trace_janus = False
